diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0a3cb25 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential clang cmake pkg-config \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \ + libx11-xcb-dev libxcb1-dev libfontconfig1-dev libvulkan-dev libssl-dev + + - uses: Swatinem/rust-cache@v2 + + - name: Test + run: cargo test --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4801a5..29727d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -25,80 +20,68 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf \ - libssl-dev + build-essential clang cmake pkg-config \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \ + libx11-xcb-dev libxcb1-dev libfontconfig1-dev libvulkan-dev libssl-dev \ + patchelf - - name: Install frontend dependencies - run: npm ci + - uses: Swatinem/rust-cache@v2 - - name: Build frontend - run: npm run build + - name: Test + run: cargo test --locked - - name: Build Rust backend - working-directory: src-tauri - run: cargo build --release + - name: Build + run: cargo build --release --locked --bin chelete - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + - name: Create tarball + run: | + NAME="chelete_${{ steps.version.outputs.VERSION }}_linux_x86_64" + mkdir -p "dist/$NAME" + cp target/release/chelete "dist/$NAME/" + cp assets/chelete.desktop "dist/$NAME/" + cp assets/icons/128x128.png "dist/$NAME/chelete.png" + cp LICENSE README.md "dist/$NAME/" + tar -C dist -czf "dist/$NAME.tar.gz" "$NAME" + - name: Create AppImage - working-directory: src-tauri/target/release + working-directory: dist run: | - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - cp chelete AppDir/usr/bin/ - cp ../../icons/128x128.png AppDir/chelete.png - cp ../../icons/128x128.png AppDir/usr/share/icons/hicolor/256x256/apps/chelete.png - cat > AppDir/chelete.desktop << EOF - [Desktop Entry] - Name=Chelete - Exec=chelete - Icon=chelete - Type=Application - Categories=Office;Finance; - EOF - cp AppDir/chelete.desktop AppDir/usr/share/applications/chelete.desktop + mkdir -p AppDir/usr/bin AppDir/usr/share/applications AppDir/usr/share/icons/hicolor/256x256/apps + cp ../target/release/chelete AppDir/usr/bin/ + cp ../assets/icons/128x128.png AppDir/chelete.png + cp ../assets/icons/128x128.png AppDir/usr/share/icons/hicolor/256x256/apps/chelete.png + cp ../assets/chelete.desktop AppDir/chelete.desktop + cp ../assets/chelete.desktop AppDir/usr/share/applications/chelete.desktop cat > AppDir/AppRun << 'APPEOF' #!/bin/sh HERE="$(dirname "$(readlink -f "${0}")")" exec "${HERE}/usr/bin/chelete" "$@" APPEOF - chmod +x AppDir/AppRun - chmod +x AppDir/usr/bin/chelete + chmod +x AppDir/AppRun AppDir/usr/bin/chelete wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage ./linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage - name: Package .deb - working-directory: src-tauri/target/release + working-directory: dist run: | DEB_DIR="chelete_${{ steps.version.outputs.VERSION }}_amd64" - mkdir -p "$DEB_DIR/DEBIAN" - mkdir -p "$DEB_DIR/usr/bin" - mkdir -p "$DEB_DIR/usr/share/applications" - mkdir -p "$DEB_DIR/usr/share/icons/hicolor/256x256/apps" - cp chelete "$DEB_DIR/usr/bin/" - cp ../../icons/128x128.png "$DEB_DIR/usr/share/icons/hicolor/256x256/apps/chelete.png" - cat > "$DEB_DIR/chelete.desktop" << EOF - [Desktop Entry] - Name=Chelete - Exec=chelete - Icon=chelete - Type=Application - Categories=Finance; - EOF + mkdir -p "$DEB_DIR/DEBIAN" "$DEB_DIR/usr/bin" "$DEB_DIR/usr/share/applications" "$DEB_DIR/usr/share/icons/hicolor/256x256/apps" + cp ../target/release/chelete "$DEB_DIR/usr/bin/" + cp ../assets/chelete.desktop "$DEB_DIR/usr/share/applications/" + cp ../assets/icons/128x128.png "$DEB_DIR/usr/share/icons/hicolor/256x256/apps/chelete.png" cat > "$DEB_DIR/DEBIAN/control" << EOF Package: chelete Version: ${{ steps.version.outputs.VERSION }} Architecture: amd64 - Depends: libgtk-3-0, libwebkit2gtk-4.1-0, libappindicator3-1 + Maintainer: Victor Phos + Depends: libvulkan1, libxkbcommon0, libxkbcommon-x11-0, libwayland-client0, libfontconfig1 Description: Personal finance tracker + A fast, keyboard-driven personal finance tracker built for Omarchy. EOF dpkg-deb --build "$DEB_DIR" @@ -107,5 +90,6 @@ jobs: with: generate_release_notes: true files: | - src-tauri/target/release/*.deb - src-tauri/target/release/*.AppImage + dist/*.tar.gz + dist/*.deb + dist/*.AppImage diff --git a/.gitignore b/.gitignore index a5027e6..b8e7488 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ node_modules/ # Build dist/ target/ -*.lock *.tsbuildinfo # Environment @@ -25,5 +24,3 @@ target/ .DS_Store Thumbs.db -# Tauri -src-tauri/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..396f03d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,9153 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf 0.13.1", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee19bd99b43e3691acbad4e840420a4881cea6c0b66a208125a824f8fd53f5a1" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.21", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base62" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[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 = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chelete" +version = "0.6.0" +dependencies = [ + "chrono", + "csv", + "dirs 5.0.1", + "gpui-kit", + "gpui-omarchy", + "rusqlite", + "serde", + "serde_json", + "toml 0.8.23", + "uuid", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.13.2", + "block", + "cocoa-foundation 0.2.1", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "compression-codecs" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98fc98460ba0ad5317075d3632b8dfc45d0be8c4a49347c2a38272019717614a" +dependencies = [ + "bzip2", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" + +[[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 = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "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.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" +dependencies = [ + "bitflags 2.13.2", + "block", + "cfg-if", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" +dependencies = [ + "block", + "core-foundation 0.10.1", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.2", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[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 = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[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 = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[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 0.5.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[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.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "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 = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +dependencies = [ + "getrandom 0.4.3", +] + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "fastrand", + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[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 = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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 = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[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" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[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 = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows 0.62.2", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.2", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "gpui-base" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb52b29a8dcdc8d3f595b8b0839c076f237572ab601ba4577d3da8ae085728" +dependencies = [ + "aho-corasick", + "anyhow", + "ashpd", + "async-channel", + "chrono", + "data-url", + "futures", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "html5ever", + "instant", + "itertools 0.13.0", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "raw-window-handle", + "regex", + "ropey", + "schemars", + "serde", + "serde_json", + "smallvec", + "smol", + "tracing", + "unicode-segmentation", + "web-time", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac246c9f3d9771e56fbf90bd7d8656d0992d83940b580debf2efab4c44fe91df" +dependencies = [ + "anyhow", + "chrono", + "core-text", + "enum-iterator", + "gpui-base", + "gpui-component-macros", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "instant", + "itertools 0.13.0", + "log", + "lsp-types", + "markdown", + "notify", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "paste", + "raw-window-handle", + "regex", + "resvg 0.45.1", + "ropey", + "rust-i18n", + "schemars", + "serde", + "serde_json", + "serde_repr", + "smallvec", + "smol", + "tracing", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b3574bd6e108b9a09c9d584e60626c9140add6d8fe839bf57627577b7977e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-kit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8edb2a8eafdb6e65ad1a80625347b93f8e54cbf3fac82a584e4924cda674d5c2" +dependencies = [ + "gpui-base", + "gpui-component", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-platform", + "gpui-pre-web", +] + +[[package]] +name = "gpui-kit-assets" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885a2abd416e29a76ef1553aea06eb2645a9290cf5e1d3fa02d218ecac6d318f" +dependencies = [ + "anyhow", + "gpui-pre", + "gpui-pre-reqwest", + "log", + "rust-embed", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "gpui-omarchy" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e527176d8b0fddd1e67abfdaaef128db907c43d0e03a56b5deaeb523da721110" +dependencies = [ + "async-channel", + "chrono", + "gpui-kit", + "itertools 0.13.0", + "notify", + "num-traits", + "toml 0.8.23", +] + +[[package]] +name = "gpui-pre" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e87a42bb37c7cb4e76dd1ac0ce88851e46e976e0373a47ab3e0757abffee54d" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bindgen", + "bitflags 2.13.2", + "chrono", + "core-video", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "gpui-pre-util-macros", + "heapless", + "image", + "inventory", + "itertools 0.14.0", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "taffy", + "thiserror 2.0.21", + "ttf-parser", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-font-kit", + "zed-scap", +] + +[[package]] +name = "gpui-pre-apple" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00053551517815ab169fda28fff78dbebf2712722593c7671c511785cb3f79d5" +dependencies = [ + "anyhow", + "block2 0.6.2", + "cbindgen", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-video", + "derive_more", + "etagere", + "foreign-types", + "gpui-pre", + "image", + "log", + "metal", + "objc", + "objc2 0.6.4", + "parking_lot", +] + +[[package]] +name = "gpui-pre-collections" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79760925c31ed06924d28d68981b5b22b2ce25665f58f0a33dfea11170e6e28" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea3f941581a6a9dd545ecd351ef2d336fe6e2a1a4555cb6414944a4984260" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddce27e989e877a3a1a243f97d7ea4f1ade2532abec418d5a0be9cca802702c7" +dependencies = [ + "anyhow", + "async-compression", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aac7022e347409454777701082201742710052813964a1e25160f7e22c965ad" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bitflags 2.13.2", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-pre-macos" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a43af845b260b09393e923c4f1e1a67a10fcfc847b4815192bbfd02ed9fe725" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "async-task", + "block2 0.6.2", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "libc", + "log", + "mach2", + "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-media", + "objc2-foundation 0.3.2", + "objc2-screen-capture-kit", + "objc2-user-notifications", + "parking_lot", + "pathfinder_geometry", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-macros" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1627c5dd3c3351e39e0621d3f3e31dbbe7c7658df90b1d2ec12bfe95d54cd37" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-perf" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01954497dd02ba96ab4c252a8668643b0cc3085e7a75dca62e7ba0e3dcd83b02" +dependencies = [ + "gpui-pre-collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0112105c2ac6757fbf527c9c92a753be9bcbce9f4cf26959a11eaa1db6565aa5" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "gpui-pre", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-wgpu", + "gpui-pre-windows", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd14639e5dfab906bbaeabfe0f94289120dcf2e6ed37217deb13964867e89a5" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8049963b716df01b67b58f86000597f222d5b31b15b35069b23a3b07f3c68352" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef6314bdf5ab162c1913712ec4a91b013ce2a9cfb0b557ecdfdd5793c8b61d3" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui-pre-sum-tree" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cca7e5fad9d53b2d265860fefdffc5f171966cb99c3fd99ecac15c033f19209" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "log", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82e06359ae76f8bb07ed713c54adb80cea6a9a2d3dc715f41aec30cd2aadb4d" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui-pre-util-macros" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a0da4143bb0e7eb926eed7edd3a61c9d17344207543dcc25fb398f0cb96b2f" +dependencies = [ + "gpui-pre-perf", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3870cc909471bb830c3d79496770c754401aad6cfa122147ccb13e8908ad08" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "futures", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui-pre-wgpu" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b02657b56b09140ce542f5e4434fb96d9fdaba0fd0035ba7dba13c171ddb9b" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-windows" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05592a6f9e3e6bb7a9f2a0d4779271cf747a948db1c07b02448de777ffff8f3" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "dunce", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-pre-zlog" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7506302091e66d7fb5e04a5b3480e90cff0bbe3157458d7df91a41c59238c48" +dependencies = [ + "anyhow", + "chrono", + "gpui-pre-collections", + "log", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed1bc6d53b3ca93785a8573d1066d555b67bc475b344fb28871f9811eb6f218" +dependencies = [ + "gpui-pre-zlog", + "gpui-pre-ztracing-macro", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829258540ea51bb9ea4a71686beffe5c99586901ba9f39944d06e4a180ab63fe" + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[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" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[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 = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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 = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "libc", + "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 = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "tiff", + "zune-core 0.5.3", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.1", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[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 = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[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 = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "normpath" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.13.2", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-rust" +version = "4.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe430c879247371c47fdedd328ad4e301e6b30b533c5fd5db1c06d75253d0e46" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "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-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "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.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-screen-capture-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-core-media", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-location", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest 0.10.7", + "endi", + "futures-lite", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "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 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[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.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "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", + "phf_shared 0.13.1", + "serde", +] + +[[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_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.8", +] + +[[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.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.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[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 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +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 = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[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.119", +] + +[[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.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.2", + "num-traits", + "proptest-macro", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.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_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[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_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.21", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.5", + "once_cell", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif 0.14.2", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.16.1", + "tiny-skia", + "usvg 0.46.0", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[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 = "ropey" +version = "2.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74" +dependencies = [ + "str_indices", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "globset", + "sha2 0.11.0", + "walkdir", +] + +[[package]] +name = "rust-i18n" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8502e73e6461c738659455c5d2cf95bc132760a6f203c717e66ee72b78906d7f" +dependencies = [ + "globwalk", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54a28dd9d46108d56b9f0c85b220908844233e561c03087890325104f90f2c6" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "rust-i18n-support" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e806c2a9701cbd6043992cd935c30cb1fdb128fa15e7b506f3b46f97045737c0" +dependencies = [ + "arc-swap", + "base62", + "foldhash 0.2.0", + "globwalk", + "itertools 0.11.0", + "normpath", + "serde", + "serde-saphyr", + "serde_json", + "siphasher", + "toml 0.8.23", + "triomphe", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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 = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.6", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs 6.0.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[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.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[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_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 = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sval" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81b254da21fe1fcc4e3a74fe39b46e25e3a863078f8b71c954d47f84889dbc6" + +[[package]] +name = "sval_buffer" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50be352d2822ffafb59e3e2ddac9d5ee60f2eeadbb7b5a2a951b9f3651e87a6f" +dependencies = [ + "sval", + "sval_ref", + "zerocopy", +] + +[[package]] +name = "sval_dynamic" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048ca293b998d9a45659159f94a64063791e74cdc670164943dbb434405573d" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b5888e40f80568733217f27b7317b845f463400ced36c424b1a804730e53b2" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17664d6bb6b74947afaab9d7c991caa9bf5638d4dee16fcbef637f440796049" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c059969ca5ca163ea7fef6c9661758973d17691aba92abdcf5c428f4ec122c" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d6b29ff568c85c87561807f51d2adfff4b6016c6363133f7cd1652a12548f3" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33ec9edc42b12764d5c90ca0a1d84189c6bde81ed27507f1e661c6e4e05853" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo 0.13.1", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" +dependencies = [ + "arrayvec", + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36" +dependencies = [ + "thiserror 2.0.21", + "windows 0.62.2", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "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 = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[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.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[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-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 = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[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 = "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 = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.14.0", + "kurbo 0.13.1", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.16.1", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[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.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0941feceafbe7a8f59ea1096d45b97002884a41306315ad797b3684b63a81d8c" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839752af8179287d27eb2b94164641b1ede9e60ab7424163388dc21ebd0508cd" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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 = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[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.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[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 = "wasm_thread" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +dependencies = [ + "futures", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "log", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[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 = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.21", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[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-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "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 0.60.2", + "windows-interface 0.59.3", + "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 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[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-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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.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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[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.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +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 = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[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-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.2", + "libc", + "quick-xml", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.8", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.3", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/src-tauri/Cargo.toml b/Cargo.toml similarity index 73% rename from src-tauri/Cargo.toml rename to Cargo.toml index 1c04857..639ce2e 100644 --- a/src-tauri/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-run = "chelete" [lib] name = "chelete_lib" -crate-type = ["staticlib", "cdylib", "rlib"] +path = "src/lib.rs" [[bin]] name = "chelete" @@ -17,8 +17,6 @@ name = "seed" path = "src/bin/seed.rs" [dependencies] -tauri = { version = "2", features = [] } -tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" rusqlite = { version = "0.32", features = ["bundled"] } @@ -26,11 +24,9 @@ uuid = { version = "1", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } toml = "0.8" dirs = "5" -tauri-plugin-dialog = "2.7.3" csv = "1.4.0" - -[build-dependencies] -tauri-build = { version = "2", features = [] } +gpui-kit = { version = "=0.7.0" } +gpui-omarchy = "0.2.0" [profile.release] panic = "abort" @@ -38,3 +34,6 @@ codegen-units = 1 lto = true opt-level = "s" strip = true + +[dev-dependencies] +gpui-kit = { version = "=0.7.0", features = ["test-support"] } diff --git a/Dockerfile.build b/Dockerfile.build index 52ae3be..ab8b40e 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,9 +1,9 @@ FROM rust:1.98 RUN apt-get update && apt-get install -y \ - gcc pkg-config libssl-dev \ - libgtk-3-dev libwebkit2gtk-4.1-dev \ - libappindicator3-dev librsvg2-dev patchelf \ + build-essential clang cmake pkg-config \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \ + libx11-xcb-dev libxcb1-dev libfontconfig1-dev libvulkan-dev libssl-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/Makefile b/Makefile index c46469e..f658fda 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,16 @@ -.PHONY: dev build test test-watch seed release-patch release-minor release-major aur-update clean +.PHONY: dev build test seed release-patch release-minor release-major aur-update clean dev: - npm run tauri dev + cargo run build: - npm run build - cd src-tauri && cargo build --release + cargo build --release test: - npm run test - cd src-tauri && cargo test - -test-watch: - npm run test:watch + cargo test seed: - npm run seed + cargo run --bin seed release-patch: ./scripts/bump-version.sh patch @@ -30,4 +25,4 @@ aur-update: cd aur && makepkg --printsrcinfo > .SRCINFO clean: - rm -rf dist src-tauri/target node_modules + cargo clean diff --git a/README.md b/README.md index dd04736..4f85018 100644 --- a/README.md +++ b/README.md @@ -36,23 +36,35 @@ Chelete adapts to whichever theme is set in Omarchy. Switch your system theme an ## Tech Stack -- [Tauri](https://tauri.app/) — native desktop shell -- [React](https://react.dev/) — UI framework -- [TypeScript](https://www.typescriptlang.org/) — type safety -- [SQLite](https://www.sqlite.org/) — local database -- [Lucide](https://lucide.dev/) — icons +- [Rust](https://www.rust-lang.org/) — the whole app, UI included +- [GPUI](https://www.gpui.rs/) through [gpui-kit](https://gpui-kit.com/) — GPU-rendered native UI +- [gpui-omarchy](https://github.com/huacnlee/gpui-omarchy) — Omarchy-styled controls and live theme following +- [SQLite](https://www.sqlite.org/) (rusqlite) — local database +- [Lucide](https://lucide.dev/) — icons, from the gpui-kit asset bundle + +## Requirements + +Build dependencies on Linux: a Rust toolchain, `clang`, `cmake`, `pkg-config`, and the development packages for Wayland, xkbcommon, fontconfig and Vulkan. + +On Arch / Omarchy: + +```bash +sudo pacman -S --needed rust clang cmake pkgconf wayland libxkbcommon libxkbcommon-x11 fontconfig vulkan-icd-loader +``` ## Development ```bash -npm install -npm run tauri dev +make dev # cargo run +make test # cargo test (library and headless UI tests) ``` +The UI tests run the real screens headlessly with simulated keystrokes, so they need no display. + ## Build ```bash -npm run tauri build +make build # cargo build --release ``` ## Usage @@ -66,6 +78,19 @@ make release-major # Bump 0.1.0 -> 1.0.0 + git tag git push origin main --tags # Triggers GitHub Actions release ``` +Data lives in `~/.local/share/com.chelete.app/` (the same place earlier versions used, so existing data carries over). + +### Keyboard + +| Keys | Action | +|:-----|:-------| +| `Ctrl+O` `T` `A` `C` `U` `P` `S` | Overview, Transactions, Accounts, Categories, Subscriptions, Projections, Settings | +| `Ctrl+K` | Command palette | +| `Ctrl+B` | Collapse the sidebar | +| `Ctrl+=` `Ctrl+-` `Ctrl+0` | Larger text, smaller text, reset (also in Settings) | +| `Esc` / `Enter` | Close a dialog / submit a form | +| `Ctrl+Q` | Quit | + ## Special Thanks - **Victor Phos** — The Architect & QA diff --git a/assets/chelete.desktop b/assets/chelete.desktop new file mode 100644 index 0000000..be22d40 --- /dev/null +++ b/assets/chelete.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Chelete +Comment=Keyboard-driven personal finance tracker +Exec=chelete +Icon=chelete +Type=Application +Categories=Office;Finance; +StartupWMClass=chelete diff --git a/src-tauri/icons/128x128.png b/assets/icons/128x128.png similarity index 100% rename from src-tauri/icons/128x128.png rename to assets/icons/128x128.png diff --git a/src-tauri/icons/128x128@2x.png b/assets/icons/128x128@2x.png similarity index 100% rename from src-tauri/icons/128x128@2x.png rename to assets/icons/128x128@2x.png diff --git a/src-tauri/icons/32x32.png b/assets/icons/32x32.png similarity index 100% rename from src-tauri/icons/32x32.png rename to assets/icons/32x32.png diff --git a/src-tauri/icons/icon.icns b/assets/icons/icon.icns similarity index 100% rename from src-tauri/icons/icon.icns rename to assets/icons/icon.icns diff --git a/src-tauri/icons/icon.ico b/assets/icons/icon.ico similarity index 100% rename from src-tauri/icons/icon.ico rename to assets/icons/icon.ico diff --git a/aur/PKGBUILD b/aur/PKGBUILD index 664a407..3458c16 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -1,27 +1,23 @@ # Maintainer: phoscoder pkgname=chelete-bin -pkgver=0.1.0 +pkgver=0.6.0 pkgrel=1 -pkgdesc='Personal finance tracker' +pkgdesc='Keyboard-driven personal finance tracker for Omarchy' arch=('x86_64') url='https://github.com/phoscoder/chelete' license=('MIT') -depends=('gtk3' 'webkit2gtk' 'libappindicator-gtk3' 'librsvg') +depends=('vulkan-icd-loader' 'libxkbcommon' 'libxkbcommon-x11' 'wayland' 'fontconfig' 'libx11' 'libxcb') provides=('chelete') conflicts=('chelete') -source=("${url}/releases/download/v${pkgver}/chelete_${pkgver}_amd64.deb") +source=("${url}/releases/download/v${pkgver}/chelete_${pkgver}_linux_x86_64.tar.gz") sha256sums=('SKIP') package() { - # Extract the deb package - bsdtar -xf "${srcdir}/chelete_${pkgver}_amd64.deb" -C "${pkgdir}" - - # Move files to correct locations - mv "${pkgdir}/usr/bin/"* "${pkgdir}/usr/bin/" 2>/dev/null || true - mv "${pkgdir}/usr/share/"* "${pkgdir}/usr/share/" 2>/dev/null || true - - # Remove the DEBIAN directory (not needed after extraction) - rm -rf "${pkgdir}/DEBIAN" + cd "${srcdir}/chelete_${pkgver}_linux_x86_64" + install -Dm755 chelete "${pkgdir}/usr/bin/chelete" + install -Dm644 chelete.desktop "${pkgdir}/usr/share/applications/chelete.desktop" + install -Dm644 chelete.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/chelete.png" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } diff --git a/index.html b/index.html deleted file mode 100644 index 98261d6..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Chelete - - -
- - - diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b440ab2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3648 +0,0 @@ -{ - "name": "chelete", - "version": "0.6.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "chelete", - "version": "0.6.0", - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - "lucide-react": "^1.37.0", - "react": "^18.3.1", - "react-datepicker": "^6.9.0", - "react-dom": "^18.3.1", - "recharts": "^3.10.1" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/react": "^18.3.12", - "@types/react-datepicker": "^6.2.0", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.4", - "jsdom": "^30.0.1", - "typescript": "~5.6.2", - "vite": "^6.0.0", - "vitest": "^3.2.7" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", - "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.3.0", - "@csstools/css-color-parser": "^4.1.10", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", - "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", - "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.1", - "@csstools/css-calc": "^3.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.9.tgz", - "integrity": "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.8.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", - "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", - "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", - "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", - "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", - "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", - "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", - "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", - "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", - "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", - "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", - "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", - "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", - "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", - "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", - "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", - "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", - "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", - "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", - "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", - "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", - "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", - "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", - "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", - "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", - "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", - "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.4", - "@tauri-apps/cli-darwin-x64": "2.11.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", - "@tauri-apps/cli-linux-arm64-musl": "2.11.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-musl": "2.11.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", - "@tauri-apps/cli-win32-x64-msvc": "2.11.4" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-shell": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", - "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.10.1" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-datepicker": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", - "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.2", - "@types/react": "*", - "date-fns": "^3.3.1" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", - "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", - "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.416", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz", - "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-toolkit": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", - "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks", - "tests/types", - "tests/browser-compat" - ] - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/immer": { - "version": "11.1.18", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", - "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", - "@exodus/bytes": "^1.15.1", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.2", - "undici": "^8.9.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "canvas": "^3.2.3" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.37.0.tgz", - "integrity": "sha512-LPsB4rD1TD6wZu1djKOf9vUnS1jTNaHbolXebXDgiTdb6jeA1agIJhJsIybCmjKmQClcOaal1o1OaiYahEftyQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-datepicker": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-6.9.0.tgz", - "integrity": "sha512-QTxuzeem7BUfVFWv+g5WuvzT0c5BPo+XTCNbMTZKSZQLU+cMMwSUHwspaxuIcDlwNcOH0tiJ+bh1fJ2yxOGYWA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.2", - "clsx": "^2.1.0", - "date-fns": "^3.3.1", - "prop-types": "^15.7.2", - "react-onclickoutside": "^6.13.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17 || ^18", - "react-dom": "^16.9.0 || ^17 || ^18" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-onclickoutside": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", - "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" - }, - "peerDependencies": { - "react": "^15.5.x || ^16.x || ^17.x || ^18.x", - "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" - } - }, - "node_modules/react-redux": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", - "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/recharts": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", - "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", - "license": "MIT", - "workspaces": [ - "www" - ], - "dependencies": { - "@reduxjs/toolkit": "^1.9.0 || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^11.1.8", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.2.0", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reselect": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", - "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", - "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.1", - "@rollup/rollup-android-arm64": "4.63.1", - "@rollup/rollup-darwin-arm64": "4.63.1", - "@rollup/rollup-darwin-x64": "4.63.1", - "@rollup/rollup-freebsd-arm64": "4.63.1", - "@rollup/rollup-freebsd-x64": "4.63.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", - "@rollup/rollup-linux-arm-musleabihf": "4.63.1", - "@rollup/rollup-linux-arm64-gnu": "4.63.1", - "@rollup/rollup-linux-arm64-musl": "4.63.1", - "@rollup/rollup-linux-loong64-gnu": "4.63.1", - "@rollup/rollup-linux-loong64-musl": "4.63.1", - "@rollup/rollup-linux-ppc64-gnu": "4.63.1", - "@rollup/rollup-linux-ppc64-musl": "4.63.1", - "@rollup/rollup-linux-riscv64-gnu": "4.63.1", - "@rollup/rollup-linux-riscv64-musl": "4.63.1", - "@rollup/rollup-linux-s390x-gnu": "4.63.1", - "@rollup/rollup-linux-x64-gnu": "4.63.1", - "@rollup/rollup-linux-x64-musl": "4.63.1", - "@rollup/rollup-openbsd-x64": "4.63.1", - "@rollup/rollup-openharmony-arm64": "4.63.1", - "@rollup/rollup-win32-arm64-msvc": "4.63.1", - "@rollup/rollup-win32-ia32-msvc": "4.63.1", - "@rollup/rollup-win32-x64-gnu": "4.63.1", - "@rollup/rollup-win32-x64-msvc": "4.63.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tabbable": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", - "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", - "license": "MIT" - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", - "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.11" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", - "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", - "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", - "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^22.14.0 || >=24.0.0" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 68d18d6..0000000 --- a/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "chelete", - "private": true, - "version": "0.6.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview", - "tauri": "tauri", - "test": "vitest run", - "test:watch": "vitest", - "seed": "cd src-tauri && cargo run --bin seed" - }, - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - "lucide-react": "^1.37.0", - "react": "^18.3.1", - "react-datepicker": "^6.9.0", - "react-dom": "^18.3.1", - "recharts": "^3.10.1" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/react": "^18.3.12", - "@types/react-datepicker": "^6.2.0", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.4", - "jsdom": "^30.0.1", - "typescript": "~5.6.2", - "vite": "^6.0.0", - "vitest": "^3.2.7" - } -} diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index 91cf06f..0000000 --- a/public/favicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 9e54cf5..357578c 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -8,8 +8,8 @@ if [[ "$VERSION_BUMP" != "patch" && "$VERSION_BUMP" != "minor" && "$VERSION_BUMP exit 1 fi -# Get current version from package.json -CURRENT=$(node -p "require('./package.json').version") +# Current version from the package table in Cargo.toml. +CURRENT=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "([^"]+)"/\1/') IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" case "$VERSION_BUMP" in @@ -30,17 +30,12 @@ esac NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" echo "Bumping version: $CURRENT -> $NEW_VERSION" -# Update package.json -sed -i "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW_VERSION\"/" package.json - -# Update tauri.conf.json -sed -i "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW_VERSION\"/" src-tauri/tauri.conf.json - -# Update Cargo.toml -sed -i "s/^version = \"$CURRENT\"/version = \"$NEW_VERSION\"/" src-tauri/Cargo.toml +# Update Cargo.toml (first match only: the package version) and the lockfile entry. +sed -i "0,/^version = \"$CURRENT\"/s//version = \"$NEW_VERSION\"/" Cargo.toml +cargo update --offline --workspace >/dev/null 2>&1 || cargo update --workspace >/dev/null # Create git commit and tag -git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml +git add Cargo.toml Cargo.lock git commit -m "chore: release v${NEW_VERSION}" git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}" diff --git a/src-tauri/build.rs b/src-tauri/build.rs deleted file mode 100644 index d860e1e..0000000 --- a/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build() -} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json deleted file mode 100644 index 9ea9de6..0000000 --- a/src-tauri/capabilities/default.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "identifier": "default", - "description": "Default application permissions", - "windows": ["main"], - "permissions": [ - "core:default", - "shell:allow-open", - "dialog:allow-open", - "dialog:allow-save" - ] -} diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json deleted file mode 100644 index 9a4e12e..0000000 --- a/src-tauri/gen/schemas/acl-manifests.json +++ /dev/null @@ -1 +0,0 @@ -{"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","allow-supports-multiple-windows"]},"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-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"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-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"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-icon-with-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-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_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-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_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-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"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-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"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-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"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-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"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},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json deleted file mode 100644 index 04a998e..0000000 --- a/src-tauri/gen/schemas/capabilities.json +++ /dev/null @@ -1 +0,0 @@ -{"default":{"identifier":"default","description":"Default application permissions","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-open","dialog:allow-save"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json deleted file mode 100644 index 6eba1dc..0000000 --- a/src-tauri/gen/schemas/desktop-schema.json +++ /dev/null @@ -1,2678 +0,0 @@ -{ - "$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": [ - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write command without any pre-configured scope." - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - }, - "deny": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "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`\n- `allow-supports-multiple-windows`", - "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`\n- `allow-supports-multiple-windows`" - }, - { - "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 supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows 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 supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows 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-icon-with-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-icon-with-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_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_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_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_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-activity-name`\n- `allow-scene-identifier`\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-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "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 scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier 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 activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name 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 scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier 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." - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", - "type": "string", - "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" - }, - { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message", - "markdownDescription": "Enables the message command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save", - "markdownDescription": "Enables the save command without any pre-configured scope." - }, - { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message", - "markdownDescription": "Denies the message command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save", - "markdownDescription": "Denies the save command without any pre-configured scope." - }, - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write 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" - ] - } - ] - }, - "ShellScopeEntryAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "raw": { - "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", - "default": false, - "type": "boolean" - }, - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellScopeEntryAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellScopeEntryAllowedArg" - } - } - ] - } - } -} \ No newline at end of file diff --git a/src-tauri/gen/schemas/linux-schema.json b/src-tauri/gen/schemas/linux-schema.json deleted file mode 100644 index 6eba1dc..0000000 --- a/src-tauri/gen/schemas/linux-schema.json +++ /dev/null @@ -1,2678 +0,0 @@ -{ - "$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": [ - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write command without any pre-configured scope." - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - }, - "deny": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "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`\n- `allow-supports-multiple-windows`", - "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`\n- `allow-supports-multiple-windows`" - }, - { - "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 supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows 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 supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows 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-icon-with-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-icon-with-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_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_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_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_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-activity-name`\n- `allow-scene-identifier`\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-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "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 scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier 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 activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name 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 scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier 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." - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", - "type": "string", - "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" - }, - { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message", - "markdownDescription": "Enables the message command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save", - "markdownDescription": "Enables the save command without any pre-configured scope." - }, - { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message", - "markdownDescription": "Denies the message command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save", - "markdownDescription": "Denies the save command without any pre-configured scope." - }, - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write 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" - ] - } - ] - }, - "ShellScopeEntryAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "raw": { - "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", - "default": false, - "type": "boolean" - }, - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellScopeEntryAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellScopeEntryAllowedArg" - } - } - ] - } - } -} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs deleted file mode 100644 index e5730be..0000000 --- a/src-tauri/src/lib.rs +++ /dev/null @@ -1,53 +0,0 @@ -mod commands; -mod database; -mod import; -pub mod seed; -mod theme; - -use database::DbState; -use tauri::Manager; - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .setup(|app| { - let app_handle = app.handle().clone(); - let db_state = database::init_database(&app_handle)?; - app.handle().manage(DbState(db_state)); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - commands::get_accounts, - commands::create_account, - commands::update_account, - commands::delete_account, - commands::get_transactions, - commands::create_transaction, - commands::update_transaction, - commands::delete_transaction, - commands::delete_transactions, - commands::get_categories, - commands::create_category, - commands::update_category, - commands::delete_category, - commands::get_subscriptions, - commands::create_subscription, - commands::update_subscription, - commands::delete_subscription, - commands::delete_subscriptions, - commands::transfer, - commands::get_overview, - commands::get_omarchy_theme, - commands::open_csv_file_dialog, - commands::preview_csv_import, - commands::import_transactions, - commands::export_data, - commands::save_file_dialog, - commands::write_export_file, - seed::seed_database_command, - ]) - .run(tauri::generate_context!()) - .expect("error while running Chelete"); -} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs deleted file mode 100644 index 7748f03..0000000 --- a/src-tauri/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - chelete_lib::run() -} diff --git a/src-tauri/src/theme/mod.rs b/src-tauri/src/theme/mod.rs deleted file mode 100644 index a2553a8..0000000 --- a/src-tauri/src/theme/mod.rs +++ /dev/null @@ -1,256 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct OmarchyTheme { - pub name: String, - pub background: String, - pub foreground: String, - pub accent: String, - pub colors: HashMap, -} - -#[derive(Debug, Deserialize)] -struct ColorsToml { - mode: Option, - accent: Option, - selection: Option, - muted: Option, - background: Option, - dark_background: Option, - darker_background: Option, - lighter_background: Option, - foreground: Option, - dark_foreground: Option, - light_foreground: Option, - bright_foreground: Option, - red: Option, - yellow: Option, - orange: Option, - green: Option, - cyan: Option, - blue: Option, - magenta: Option, - brown: Option, - bright_red: Option, - bright_yellow: Option, - bright_green: Option, - bright_cyan: Option, - bright_blue: Option, - bright_magenta: Option, -} - -pub fn detect_theme() -> Result { - let home = dirs::home_dir().ok_or("Could not find home directory")?; - - // Check the standard Omarchy theme location - let theme_dir = home.join(".local/state/omarchy/current/theme"); - let theme_name_file = home.join(".local/state/omarchy/current/theme.name"); - let colors_file = theme_dir.join("colors.toml"); - - if colors_file.exists() { - // Read theme name - let theme_name = if theme_name_file.exists() { - fs::read_to_string(&theme_name_file) - .unwrap_or_else(|_| "Omarchy".to_string()) - .trim() - .to_string() - } else { - "Omarchy".to_string() - }; - - return parse_omarchy_theme(&colors_file, &theme_name); - } - - // Fallback: generate a default dark theme - Ok(default_theme()) -} - -fn parse_omarchy_theme(path: &PathBuf, theme_name: &str) -> Result { - let content = fs::read_to_string(path).map_err(|e| format!("Failed to read theme file: {}", e))?; - - let parsed: ColorsToml = - toml::from_str(&content).map_err(|e| format!("Failed to parse theme file: {}", e))?; - - let mut color_map = HashMap::new(); - - // Insert all colors from the TOML file - if let Some(v) = &parsed.background { - color_map.insert("background".to_string(), v.clone()); - } - if let Some(v) = &parsed.foreground { - color_map.insert("foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.accent { - color_map.insert("accent".to_string(), v.clone()); - } - if let Some(v) = &parsed.selection { - color_map.insert("selection".to_string(), v.clone()); - } - if let Some(v) = &parsed.muted { - color_map.insert("muted".to_string(), v.clone()); - } - if let Some(v) = &parsed.dark_background { - color_map.insert("dark_background".to_string(), v.clone()); - } - if let Some(v) = &parsed.darker_background { - color_map.insert("darker_background".to_string(), v.clone()); - } - if let Some(v) = &parsed.lighter_background { - color_map.insert("lighter_background".to_string(), v.clone()); - } - if let Some(v) = &parsed.dark_foreground { - color_map.insert("dark_foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.light_foreground { - color_map.insert("light_foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_foreground { - color_map.insert("bright_foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.red { - color_map.insert("red".to_string(), v.clone()); - color_map.insert("danger".to_string(), v.clone()); - } - if let Some(v) = &parsed.yellow { - color_map.insert("yellow".to_string(), v.clone()); - color_map.insert("warning".to_string(), v.clone()); - } - if let Some(v) = &parsed.orange { - color_map.insert("orange".to_string(), v.clone()); - } - if let Some(v) = &parsed.green { - color_map.insert("green".to_string(), v.clone()); - color_map.insert("success".to_string(), v.clone()); - } - if let Some(v) = &parsed.cyan { - color_map.insert("cyan".to_string(), v.clone()); - color_map.insert("info".to_string(), v.clone()); - } - if let Some(v) = &parsed.blue { - color_map.insert("blue".to_string(), v.clone()); - } - if let Some(v) = &parsed.magenta { - color_map.insert("magenta".to_string(), v.clone()); - } - if let Some(v) = &parsed.brown { - color_map.insert("brown".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_red { - color_map.insert("bright_red".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_yellow { - color_map.insert("bright_yellow".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_green { - color_map.insert("bright_green".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_cyan { - color_map.insert("bright_cyan".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_blue { - color_map.insert("bright_blue".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_magenta { - color_map.insert("bright_magenta".to_string(), v.clone()); - } - - // Derive colors from background - let background = parsed.background.unwrap_or_else(|| "#121212".to_string()); - let foreground = parsed.foreground.unwrap_or_else(|| "#bebebe".to_string()); - let accent = parsed.accent.unwrap_or_else(|| "#e68e0d".to_string()); - - let bg_rgb = hex_to_rgb(&background); - let light = is_light(bg_rgb); - - let (surface, surface_hover, border) = if light { - ( - rgb_to_hex(bg_rgb.0.saturating_sub(8), bg_rgb.1.saturating_sub(8), bg_rgb.2.saturating_sub(8)), - rgb_to_hex(bg_rgb.0.saturating_sub(15), bg_rgb.1.saturating_sub(15), bg_rgb.2.saturating_sub(15)), - rgb_to_hex(bg_rgb.0.saturating_sub(18), bg_rgb.1.saturating_sub(18), bg_rgb.2.saturating_sub(18)), - ) - } else { - ( - rgb_to_hex((bg_rgb.0 as u32 + 12).min(255) as u8, (bg_rgb.1 as u32 + 12).min(255) as u8, (bg_rgb.2 as u32 + 12).min(255) as u8), - rgb_to_hex((bg_rgb.0 as u32 + 20).min(255) as u8, (bg_rgb.1 as u32 + 20).min(255) as u8, (bg_rgb.2 as u32 + 20).min(255) as u8), - rgb_to_hex((bg_rgb.0 as u32 + 28).min(255) as u8, (bg_rgb.1 as u32 + 28).min(255) as u8, (bg_rgb.2 as u32 + 28).min(255) as u8), - ) - }; - color_map.insert("surface".to_string(), surface); - color_map.insert("surface_hover".to_string(), surface_hover); - color_map.insert("border".to_string(), border); - - let fg_rgb = hex_to_rgb(&foreground); - let muted = rgb_to_hex( - (fg_rgb.0 as u32 * 60 / 100) as u8, - (fg_rgb.1 as u32 * 60 / 100) as u8, - (fg_rgb.2 as u32 * 60 / 100) as u8, - ); - color_map.insert("foreground_muted".to_string(), muted); - - let subtle = rgb_to_hex( - (fg_rgb.0 as u32 * 40 / 100) as u8, - (fg_rgb.1 as u32 * 40 / 100) as u8, - (fg_rgb.2 as u32 * 40 / 100) as u8, - ); - color_map.insert("foreground_subtle".to_string(), subtle); - - Ok(OmarchyTheme { - name: theme_name.to_string(), - background, - foreground, - accent, - colors: color_map, - }) -} - -fn default_theme() -> OmarchyTheme { - let mut colors = HashMap::new(); - colors.insert("background".to_string(), "#1a1b26".to_string()); - colors.insert("foreground".to_string(), "#c0caf5".to_string()); - colors.insert("surface".to_string(), "#24283b".to_string()); - colors.insert("surface_hover".to_string(), "#2f3349".to_string()); - colors.insert("border".to_string(), "#3b4261".to_string()); - colors.insert("accent".to_string(), "#7aa2f7".to_string()); - colors.insert("success".to_string(), "#9ece6a".to_string()); - colors.insert("danger".to_string(), "#f7768e".to_string()); - colors.insert("warning".to_string(), "#e0af68".to_string()); - colors.insert("info".to_string(), "#7dcfff".to_string()); - colors.insert("foreground_muted".to_string(), "#737aa2".to_string()); - colors.insert("foreground_subtle".to_string(), "#565f89".to_string()); - colors.insert("black".to_string(), "#15161e".to_string()); - colors.insert("red".to_string(), "#f7768e".to_string()); - colors.insert("green".to_string(), "#9ece6a".to_string()); - colors.insert("yellow".to_string(), "#e0af68".to_string()); - colors.insert("blue".to_string(), "#7aa2f7".to_string()); - colors.insert("magenta".to_string(), "#bb9af7".to_string()); - colors.insert("cyan".to_string(), "#7dcfff".to_string()); - colors.insert("white".to_string(), "#a9b1d6".to_string()); - - OmarchyTheme { - name: "Tokyo Night".to_string(), - background: "#1a1b26".to_string(), - foreground: "#c0caf5".to_string(), - accent: "#7aa2f7".to_string(), - colors, - } -} - -fn hex_to_rgb(hex: &str) -> (u8, u8, u8) { - let hex = hex.trim_start_matches('#'); - let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0); - let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0); - let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0); - (r, g, b) -} - -fn rgb_to_hex(r: u8, g: u8, b: u8) -> String { - format!("#{:02x}{:02x}{:02x}", r, g, b) -} - -fn is_light(rgb: (u8, u8, u8)) -> bool { - let luminance = rgb.0 as u32 * 299 + rgb.1 as u32 * 587 + rgb.2 as u32 * 114; - luminance / 1000 > 128 -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json deleted file mode 100644 index 2414941..0000000 --- a/src-tauri/tauri.conf.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "productName": "Chelete", - "version": "0.6.0", - "identifier": "com.chelete.app", - "build": { - "beforeDevCommand": "npm run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm run build", - "frontendDist": "../dist" - }, - "app": { - "windows": [ - { - "title": "Chelete", - "width": 1100, - "height": 700, - "decorations": false - } - ], - "security": { - "csp": null, - "capabilities": ["default"] - } - }, - "bundle": { - "active": true, - "targets": ["appimage", "deb"], - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } -} diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index e14e402..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useTheme } from "./hooks/useTheme"; -import { OverviewScreen } from "./features/overview/OverviewScreen"; -import { TransactionsScreen } from "./features/transactions/TransactionsScreen"; -import { AccountsScreen } from "./features/accounts/AccountsScreen"; -import { CategoriesScreen } from "./features/categories/CategoriesScreen"; -import { SubscriptionsScreen } from "./features/subscriptions/SubscriptionsScreen"; -import { ProjectionsScreen } from "./features/projections/ProjectionsScreen"; -import { SettingsScreen } from "./features/settings/SettingsScreen"; -import { CommandPalette } from "./components/CommandPalette"; -import type { View } from "./types"; -import { - LayoutDashboard, - ArrowLeftRight, - Wallet, - Tag, - Repeat, - LineChart, - Settings, - Command, - PanelLeftClose, - PanelLeftOpen, - type LucideIcon, -} from "lucide-react"; - -const NAV_ITEMS: { view: View; label: string; shortcut: string; icon: LucideIcon }[] = [ - { view: "overview", label: "Overview", shortcut: "Ctrl+O", icon: LayoutDashboard }, - { view: "transactions", label: "Transactions", shortcut: "Ctrl+T", icon: ArrowLeftRight }, - { view: "accounts", label: "Accounts", shortcut: "Ctrl+A", icon: Wallet }, - { view: "categories", label: "Categories", shortcut: "Ctrl+C", icon: Tag }, - { view: "subscriptions", label: "Subscriptions", shortcut: "Ctrl+U", icon: Repeat }, - { view: "projections", label: "Projections", shortcut: "Ctrl+P", icon: LineChart }, - { view: "settings", label: "Settings", shortcut: "Ctrl+S", icon: Settings }, -]; - -export default function App() { - const [view, setView] = useState("overview"); - const [commandPalette, setCommandPalette] = useState(false); - const [collapsed, setCollapsed] = useState(() => { - return localStorage.getItem("sidebar-collapsed") === "true"; - }); - - useTheme(); - - // Apply saved corner radius preference - useEffect(() => { - const saved = localStorage.getItem("chelete-radius"); - const radius = saved === "square" ? "0px" : "4px"; - document.documentElement.style.setProperty("--chelete-radius", radius); - }, []); - - const toggleCollapsed = () => { - setCollapsed((prev) => { - localStorage.setItem("sidebar-collapsed", String(!prev)); - return !prev; - }); - }; - - // Global keyboard shortcuts - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - const ctrl = e.ctrlKey || e.metaKey; - - // Command palette: Ctrl+K - if (ctrl && e.key === "k") { - e.preventDefault(); - setCommandPalette((o) => !o); - return; - } - - // Toggle sidebar: Ctrl+B - if (ctrl && e.key === "b") { - e.preventDefault(); - toggleCollapsed(); - return; - } - - // Escape closes command palette - if (e.key === "Escape") { - setCommandPalette(false); - return; - } - - // Skip if typing in an input - const target = e.target as HTMLElement; - if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") { - return; - } - - if (ctrl) { - const viewMap: Record = { - o: "overview", - t: "transactions", - a: "accounts", - c: "categories", - u: "subscriptions", - p: "projections", - s: "settings", - }; - if (viewMap[e.key]) { - e.preventDefault(); - setView(viewMap[e.key]); - } - } - }, - [] - ); - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [handleKeyDown]); - - const renderScreen = () => { - switch (view) { - case "overview": - return ; - case "transactions": - return ; - case "accounts": - return ; - case "categories": - return ; - case "subscriptions": - return ; - case "projections": - return ; - case "settings": - return ; - } - }; - - return ( -
- - -
{renderScreen()}
- - setCommandPalette(false)} - onNavigate={(v) => { - setView(v); - setCommandPalette(false); - }} - onAction={(action) => { - setCommandPalette(false); - if (action === "add-transaction") setView("transactions"); - else if (action === "import-transactions") { - setView("transactions"); - window.dispatchEvent(new CustomEvent("chelete-open-import")); - } - else if (action === "add-account") setView("accounts"); - else if (action === "add-category") setView("categories"); - else if (action === "add-subscription") setView("subscriptions"); - }} - /> -
- ); -} diff --git a/src-tauri/src/bin/seed.rs b/src/bin/seed.rs similarity index 100% rename from src-tauri/src/bin/seed.rs rename to src/bin/seed.rs diff --git a/src-tauri/src/commands/mod.rs b/src/commands/mod.rs similarity index 85% rename from src-tauri/src/commands/mod.rs rename to src/commands/mod.rs index 11bbf8f..a7a7632 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,8 +2,6 @@ use crate::database::DbState; use crate::import::{CsvMapping, CsvPreview, ImportResult, parse_csv_preview, parse_csv_rows}; use rusqlite::params; use serde::{Deserialize, Serialize}; -use tauri::State; -use tauri_plugin_dialog::DialogExt; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Account { @@ -180,8 +178,7 @@ fn generate_id() -> String { // ── Accounts ───────────────────────────────────────────────────── -#[tauri::command] -pub fn get_accounts(state: State<'_, DbState>) -> Result, String> { +pub fn get_accounts(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -213,9 +210,8 @@ pub fn get_accounts(state: State<'_, DbState>) -> Result, String> { Ok(accounts) } -#[tauri::command] pub fn create_account( - state: State<'_, DbState>, + state: &DbState, request: CreateAccountRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -251,9 +247,8 @@ pub fn create_account( }) } -#[tauri::command] pub fn update_account( - state: State<'_, DbState>, + state: &DbState, request: UpdateAccountRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -291,8 +286,7 @@ pub fn update_account( Ok(account) } -#[tauri::command] -pub fn delete_account(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_account(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE accounts SET deleted_at = datetime('now') WHERE id = ?1", @@ -328,8 +322,7 @@ fn get_account_by_id(conn: &rusqlite::Connection, id: &str) -> Result) -> Result, String> { +pub fn get_transactions(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -362,9 +355,8 @@ pub fn get_transactions(state: State<'_, DbState>) -> Result, S Ok(transactions) } -#[tauri::command] pub fn create_transaction( - state: State<'_, DbState>, + state: &DbState, request: CreateTransactionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -416,9 +408,8 @@ pub fn create_transaction( }) } -#[tauri::command] pub fn update_transaction( - state: State<'_, DbState>, + state: &DbState, request: UpdateTransactionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -513,14 +504,12 @@ pub fn update_transaction( get_transaction_by_id(&conn, &request.id) } -#[tauri::command] -pub fn delete_transaction(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_transaction(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; delete_transactions_inner(&conn, &[id]) } -#[tauri::command] -pub fn delete_transactions(state: State<'_, DbState>, ids: Vec) -> Result<(), String> { +pub fn delete_transactions(state: &DbState, ids: Vec) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; delete_transactions_inner(&conn, &ids) } @@ -605,8 +594,7 @@ fn get_transaction_by_id( // ── Categories ─────────────────────────────────────────────────── -#[tauri::command] -pub fn get_categories(state: State<'_, DbState>) -> Result, String> { +pub fn get_categories(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -636,9 +624,8 @@ pub fn get_categories(state: State<'_, DbState>) -> Result, String Ok(categories) } -#[tauri::command] pub fn create_category( - state: State<'_, DbState>, + state: &DbState, request: CreateCategoryRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -671,9 +658,8 @@ pub fn create_category( }) } -#[tauri::command] pub fn update_category( - state: State<'_, DbState>, + state: &DbState, request: UpdateCategoryRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -711,8 +697,7 @@ pub fn update_category( Ok(category) } -#[tauri::command] -pub fn delete_category(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_category(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE categories SET deleted_at = datetime('now') WHERE id = ?1", @@ -746,8 +731,7 @@ fn get_category_by_id(conn: &rusqlite::Connection, id: &str) -> Result) -> Result { +pub fn get_overview(state: &DbState) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; // Total balance from all active accounts @@ -874,8 +858,7 @@ pub fn get_overview(state: State<'_, DbState>) -> Result { // ── Subscriptions ──────────────────────────────────────────────── -#[tauri::command] -pub fn get_subscriptions(state: State<'_, DbState>) -> Result, String> { +pub fn get_subscriptions(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -907,9 +890,8 @@ pub fn get_subscriptions(state: State<'_, DbState>) -> Result, Ok(subscriptions) } -#[tauri::command] pub fn create_subscription( - state: State<'_, DbState>, + state: &DbState, request: CreateSubscriptionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -946,9 +928,8 @@ pub fn create_subscription( }) } -#[tauri::command] pub fn update_subscription( - state: State<'_, DbState>, + state: &DbState, request: UpdateSubscriptionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -1014,8 +995,7 @@ pub fn update_subscription( get_subscription_by_id(&conn, &request.id) } -#[tauri::command] -pub fn delete_subscription(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_subscription(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE subscriptions SET deleted_at = datetime('now') WHERE id = ?1", @@ -1025,8 +1005,7 @@ pub fn delete_subscription(state: State<'_, DbState>, id: String) -> Result<(), Ok(()) } -#[tauri::command] -pub fn delete_subscriptions(state: State<'_, DbState>, ids: Vec) -> Result<(), String> { +pub fn delete_subscriptions(state: &DbState, ids: Vec) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; if ids.is_empty() { return Ok(()); @@ -1041,9 +1020,8 @@ pub fn delete_subscriptions(state: State<'_, DbState>, ids: Vec) -> Resu Ok(()) } -#[tauri::command] pub fn transfer( - state: State<'_, DbState>, + state: &DbState, from_account_id: String, to_account_id: String, amount: i64, @@ -1155,24 +1133,6 @@ fn get_subscription_by_id(conn: &rusqlite::Connection, id: &str) -> Result Result, String> { - let file_path = app - .dialog() - .file() - .add_filter("CSV files", &["csv"]) - .blocking_pick_file(); - - match file_path { - Some(path) => { - let path_buf = path.into_path().map_err(|e| e.to_string())?; - Ok(Some(path_buf.to_string_lossy().to_string())) - } - None => Ok(None), - } -} - -#[tauri::command] pub fn preview_csv_import( path: String, mapping: CsvMapping, @@ -1180,9 +1140,8 @@ pub fn preview_csv_import( parse_csv_preview(&path, &mapping) } -#[tauri::command] pub fn import_transactions( - state: State<'_, DbState>, + state: &DbState, path: String, mapping: CsvMapping, options: ImportOptions, @@ -1194,7 +1153,7 @@ pub fn import_transactions( let accounts = load_accounts(&tx)?; let categories = load_categories(&tx)?; - let existing = load_existing_transactions(&tx)?; + let mut existing = load_existing_transactions(&tx)?; let mut imported = 0usize; let mut skipped = 0usize; @@ -1278,6 +1237,8 @@ pub fn import_transactions( params![balance_change, account_id], ).map_err(|e| e.to_string())?; + // Later rows in the same file are duplicates of this one too. + existing.push((account_id, date, amount_cents, description)); imported += 1; } @@ -1424,38 +1385,12 @@ pub struct ExportData { pub subscriptions: Vec, } -#[tauri::command] -pub async fn save_file_dialog( - app: tauri::AppHandle, - default_name: String, - extension: String, -) -> Result, String> { - let file_path = app - .dialog() - .file() - .set_file_name(default_name) - .add_filter(&format!("{} files", extension.to_uppercase()), - &[extension.trim_start_matches('.')], - ) - .blocking_save_file(); - - match file_path { - Some(path) => { - let path_buf = path.into_path().map_err(|e| e.to_string())?; - Ok(Some(path_buf.to_string_lossy().to_string())) - } - None => Ok(None), - } -} - -#[tauri::command] pub fn write_export_file(path: String, contents: String) -> Result<(), String> { std::fs::write(&path, contents).map_err(|e| format!("Failed to write file: {}", e))?; Ok(()) } -#[tauri::command] -pub fn export_data(state: State<'_, DbState>) -> Result { +pub fn export_data(state: &DbState) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; Ok(ExportData { accounts: load_accounts_for_export(&conn)?, @@ -1586,9 +1521,173 @@ fn load_subscriptions_for_export(conn: &rusqlite::Connection) -> Result DbState { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + crate::database::run_migrations(&conn).unwrap(); + crate::seed::seed_database(&conn).unwrap(); + DbState(Mutex::new(conn)) + } + + fn csv(name: &str, body: &str) -> String { + let dir = std::env::temp_dir().join(format!("chelete-cmd-{name}-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("in.csv"); + std::fs::write(&path, body).unwrap(); + path.to_string_lossy().to_string() + } + + fn mapping_for(path: &str, account_id: &str) -> CsvMapping { + let preview = crate::import::parse_csv_preview(path, &CsvMapping::default()).unwrap(); + let mut mapping = CsvMapping::from_headers(&preview.headers); + mapping.default_account_id = Some(account_id.to_string()); + mapping + } + + fn options(account_id: &str, skip: bool) -> ImportOptions { + ImportOptions { + default_account_id: Some(account_id.into()), + default_category_id: None, + default_currency: Some("USD".into()), + skip_duplicates: skip, + } + } + + const REPEATS: &str = "Date,Description,Debit,Credit\n2026-03-01,Coffee,4.50,\n2026-03-01,Coffee,4.50,\n2026-03-02,Salary,,2000.00\n"; + + #[test] + fn import_skips_repeats_within_the_same_file() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + let path = csv("within", REPEATS); + let result = import_transactions(&db, path.clone(), mapping_for(&path, &account.id), options(&account.id, true)).unwrap(); + assert_eq!((result.imported, result.skipped), (2, 1)); + let after = get_accounts(&db).unwrap().into_iter().find(|a| a.id == account.id).unwrap(); + assert_eq!(after.balance, account.balance - 450 + 200000, "only one coffee hit the balance"); + } + + #[test] + fn import_skips_rows_already_in_the_database() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + let path = csv("existing", REPEATS); + let m = mapping_for(&path, &account.id); + import_transactions(&db, path.clone(), m.clone(), options(&account.id, true)).unwrap(); + let again = import_transactions(&db, path, m, options(&account.id, true)).unwrap(); + assert_eq!((again.imported, again.skipped), (0, 3)); + } + + #[test] + fn import_without_skipping_keeps_every_row() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + let before = get_transactions(&db).unwrap().len(); + let path = csv("keepall", REPEATS); + let result = import_transactions(&db, path.clone(), mapping_for(&path, &account.id), options(&account.id, false)).unwrap(); + assert_eq!((result.imported, result.skipped), (3, 0)); + assert_eq!(get_transactions(&db).unwrap().len(), before + 3); + } + + #[test] + fn same_row_on_a_different_account_is_not_a_duplicate() { + let db = seeded(); + let accounts = get_accounts(&db).unwrap(); + let path = csv("twoaccounts", "Date,Description,Debit,Credit\n2026-03-01,Coffee,4.50,\n"); + let first = import_transactions(&db, path.clone(), mapping_for(&path, &accounts[0].id), options(&accounts[0].id, true)).unwrap(); + let second = import_transactions(&db, path.clone(), mapping_for(&path, &accounts[1].id), options(&accounts[1].id, true)).unwrap(); + assert_eq!((first.imported, second.imported), (1, 1)); + } + + fn new_txn(account_id: &str, kind: &str, amount: i64) -> CreateTransactionRequest { + CreateTransactionRequest { + account_id: account_id.into(), + category_id: None, + transaction_type: kind.into(), + amount, + currency: "USD".into(), + description: "t".into(), + merchant: None, + notes: None, + transaction_date: "2026-03-01".into(), + } + } + + fn balance(db: &DbState, id: &str) -> i64 { + get_accounts(db).unwrap().into_iter().find(|a| a.id == id).unwrap().balance + } -#[tauri::command] -pub fn get_omarchy_theme() -> Result { - crate::theme::detect_theme() + #[test] + fn transactions_move_the_account_balance_and_deleting_reverses_it() { + let db = seeded(); + let id = get_accounts(&db).unwrap().remove(0).id; + let start = balance(&db, &id); + let expense = create_transaction(&db, new_txn(&id, "expense", 1000)).unwrap(); + let income = create_transaction(&db, new_txn(&id, "income", 2500)).unwrap(); + assert_eq!(balance(&db, &id), start - 1000 + 2500); + delete_transactions(&db, vec![expense.id, income.id]).unwrap(); + assert_eq!(balance(&db, &id), start); + } + + #[test] + fn editing_an_amount_adjusts_the_balance_by_the_difference() { + let db = seeded(); + let id = get_accounts(&db).unwrap().remove(0).id; + let t = create_transaction(&db, new_txn(&id, "expense", 1000)).unwrap(); + let after_create = balance(&db, &id); + update_transaction( + &db, + UpdateTransactionRequest { + id: t.id, + account_id: None, + category_id: None, + transaction_type: None, + amount: Some(1600), + currency: None, + description: None, + merchant: None, + notes: None, + transaction_date: None, + }, + ) + .unwrap(); + assert_eq!(balance(&db, &id), after_create - 600); + } + + #[test] + fn transfer_moves_money_and_rejects_the_same_account() { + let db = seeded(); + let accounts = get_accounts(&db).unwrap(); + let (a, b) = (accounts[0].id.clone(), accounts[1].id.clone()); + let (sa, sb) = (balance(&db, &a), balance(&db, &b)); + transfer(&db, a.clone(), b.clone(), 700, "USD".into(), None).unwrap(); + assert_eq!((balance(&db, &a), balance(&db, &b)), (sa - 700, sb + 700)); + assert!(transfer(&db, a.clone(), a, 100, "USD".into(), None).is_err()); + } + + #[test] + fn deleted_accounts_and_categories_disappear_from_lists() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + delete_account(&db, account.id.clone()).unwrap(); + assert!(get_accounts(&db).unwrap().iter().all(|a| a.id != account.id)); + let category = get_categories(&db).unwrap().remove(0); + delete_category(&db, category.id.clone()).unwrap(); + assert!(get_categories(&db).unwrap().iter().all(|c| c.id != category.id)); + } + + #[test] + fn overview_totals_match_the_transactions() { + let db = seeded(); + let overview = get_overview(&db).unwrap(); + let accounts_total: i64 = get_accounts(&db).unwrap().iter().map(|a| a.balance).sum(); + assert_eq!(overview.total_balance, accounts_total); + assert!(overview.total_income > 0 && overview.total_expenses > 0); + } } diff --git a/src/components/CategoryIcons.tsx b/src/components/CategoryIcons.tsx deleted file mode 100644 index 292399c..0000000 --- a/src/components/CategoryIcons.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - ShoppingCart, - Car, - Briefcase, - Film, - ShoppingBag, - Zap, - Heart, - BookOpen, - Coffee, - Plane, - Home, - Utensils, - Wifi, - Smartphone, - Music, - Gamepad2, - Gift, - Stethoscope, - GraduationCap, - Building2, - PiggyBank, - CreditCard, - Wallet, - Wrench, - Bot, - Code, - Laptop, - Tv, - Dumbbell, - Dog, - Cat, - Leaf, - Shirt, - Scissors, - Bus, - Train, - Hotel, - Wine, - Landmark, - Shield, - Cloud, - CircleQuestionMark, - UserRound, - type LucideIcon, -} from "lucide-react"; - -export interface CategoryIconDef { - name: string; - icon: LucideIcon; - label: string; -} - -export const CATEGORY_ICONS: CategoryIconDef[] = [ - { name: "utensils", icon: Utensils, label: "Food" }, - { name: "car", icon: Car, label: "Car" }, - { name: "briefcase", icon: Briefcase, label: "Work" }, - { name: "film", icon: Film, label: "Film" }, - { name: "shopping-bag", icon: ShoppingBag, label: "Shopping" }, - { name: "zap", icon: Zap, label: "Bills" }, - { name: "heart", icon: Heart, label: "Health" }, - { name: "book-open", icon: BookOpen, label: "Education" }, - { name: "coffee", icon: Coffee, label: "Coffee" }, - { name: "plane", icon: Plane, label: "Travel" }, - { name: "home", icon: Home, label: "Home" }, - { name: "shopping-cart", icon: ShoppingCart, label: "Cart" }, - { name: "wifi", icon: Wifi, label: "Internet" }, - { name: "smartphone", icon: Smartphone, label: "Phone" }, - { name: "music", icon: Music, label: "Music" }, - { name: "gamepad-2", icon: Gamepad2, label: "Gaming" }, - { name: "gift", icon: Gift, label: "Gift" }, - { name: "stethoscope", icon: Stethoscope, label: "Medical" }, - { name: "graduation-cap", icon: GraduationCap, label: "School" }, - { name: "building-2", icon: Building2, label: "Office" }, - { name: "piggy-bank", icon: PiggyBank, label: "Savings" }, - { name: "credit-card", icon: CreditCard, label: "Credit" }, - { name: "wallet", icon: Wallet, label: "Wallet" }, - { name: "wrench", icon: Wrench, label: "Repairs" }, - { name: "bot", icon: Bot, label: "AI" }, - { name: "code", icon: Code, label: "Code" }, - { name: "laptop", icon: Laptop, label: "Laptop" }, - { name: "tv", icon: Tv, label: "Streaming" }, - { name: "dumbbell", icon: Dumbbell, label: "Fitness" }, - { name: "dog", icon: Dog, label: "Dog" }, - { name: "cat", icon: Cat, label: "Cat" }, - { name: "leaf", icon: Leaf, label: "Nature" }, - { name: "shirt", icon: Shirt, label: "Clothing" }, - { name: "scissors", icon: Scissors, label: "Grooming" }, - { name: "bus", icon: Bus, label: "Bus" }, - { name: "train", icon: Train, label: "Train" }, - { name: "hotel", icon: Hotel, label: "Hotel" }, - { name: "wine", icon: Wine, label: "Drinks" }, - { name: "landmark", icon: Landmark, label: "Taxes" }, - { name: "shield", icon: Shield, label: "Insurance" }, - { name: "cloud", icon: Cloud, label: "Cloud" }, - { name: "circle-question-mark", icon: CircleQuestionMark, label: "Unaccounted" }, - { name: "user-round", icon: UserRound, label: "Woman" }, -]; - -export function CategoryIcon({ - name, - size = 14, - style, -}: { - name: string; - size?: number; - style?: React.CSSProperties; -}) { - const iconEntry = CATEGORY_ICONS.find((i) => i.name === name); - if (iconEntry) { - const Icon = iconEntry.icon; - return ; - } - return null; -} diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx deleted file mode 100644 index 4758973..0000000 --- a/src/components/CommandPalette.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useEffect, useState, useRef } from "react"; -import type { View } from "../types"; - -interface Command { - id: string; - label: string; - shortcut?: string; - action: () => void; -} - -export function CommandPalette({ - open, - onClose, - onNavigate, - onAction, -}: { - open: boolean; - onClose: () => void; - onNavigate: (view: View) => void; - onAction: (action: string) => void; -}) { - const [query, setQuery] = useState(""); - const [selected, setSelected] = useState(0); - const inputRef = useRef(null); - - const commands: Command[] = [ - { id: "overview", label: "Go to Overview", shortcut: "Ctrl+O", action: () => onNavigate("overview") }, - { id: "transactions", label: "Go to Transactions", shortcut: "Ctrl+T", action: () => onNavigate("transactions") }, - { id: "accounts", label: "Go to Accounts", shortcut: "Ctrl+A", action: () => onNavigate("accounts") }, - { id: "categories", label: "Go to Categories", shortcut: "Ctrl+C", action: () => onNavigate("categories") }, - { id: "subscriptions", label: "Go to Subscriptions", shortcut: "Ctrl+U", action: () => onNavigate("subscriptions") }, - { id: "projections", label: "Go to Projections", shortcut: "Ctrl+P", action: () => onNavigate("projections") }, - { id: "settings", label: "Go to Settings", shortcut: "Ctrl+S", action: () => onNavigate("settings") }, - { id: "add-transaction", label: "Add Transaction", action: () => onAction("add-transaction") }, - { id: "import-transactions", label: "Import Transactions (CSV)", action: () => onAction("import-transactions") }, - { id: "add-account", label: "Add Account", action: () => onAction("add-account") }, - { id: "add-category", label: "Add Category", action: () => onAction("add-category") }, - { id: "add-subscription", label: "Add Subscription", action: () => onAction("add-subscription") }, - ]; - - const filtered = commands.filter((c) => - c.label.toLowerCase().includes(query.toLowerCase()) - ); - - useEffect(() => { - if (open) { - setQuery(""); - setSelected(0); - setTimeout(() => inputRef.current?.focus(), 50); - } - }, [open]); - - useEffect(() => { - setSelected(0); - }, [query]); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelected((s) => Math.min(s + 1, filtered.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelected((s) => Math.max(s - 1, 0)); - } else if (e.key === "Enter") { - e.preventDefault(); - if (filtered[selected]) { - filtered[selected].action(); - onClose(); - } - } else if (e.key === "Escape") { - onClose(); - } - }; - - if (!open) return null; - - return ( -
-
e.stopPropagation()}> - setQuery(e.target.value)} - onKeyDown={handleKeyDown} - /> -
- {filtered.map((cmd, i) => ( -
{ - cmd.action(); - onClose(); - }} - onMouseEnter={() => setSelected(i)} - > - {cmd.label} - {cmd.shortcut && {cmd.shortcut}} -
- ))} - {filtered.length === 0 && ( -
- No matching commands -
- )} -
-
-
- ); -} diff --git a/src/components/DateFilter.tsx b/src/components/DateFilter.tsx deleted file mode 100644 index bccd66c..0000000 --- a/src/components/DateFilter.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { ChevronDown, Calendar, Check, X } from "lucide-react"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; - -export type DateFilterValue = - | "today" - | "yesterday" - | "this_week" - | "last_week" - | "this_month" - | "last_month" - | "this_year" - | "last_year" - | "all" - | "custom"; - -interface DateFilterProps { - value: DateFilterValue; - onChange: (value: DateFilterValue) => void; - customStart: string; - customEnd: string; - onCustomStartChange: (v: string) => void; - onCustomEndChange: (v: string) => void; -} - -const FILTERS: { value: DateFilterValue; label: string }[] = [ - { value: "today", label: "Today" }, - { value: "yesterday", label: "Yesterday" }, - { value: "this_week", label: "This Week" }, - { value: "last_week", label: "Last Week" }, - { value: "this_month", label: "This Month" }, - { value: "last_month", label: "Last Month" }, - { value: "this_year", label: "This Year" }, - { value: "last_year", label: "Last Year" }, - { value: "all", label: "All Time" }, - { value: "custom", label: "Custom Range" }, -]; - -export function DateFilter({ - value, - onChange, - customStart, - customEnd, - onCustomStartChange, - onCustomEndChange, -}: DateFilterProps) { - const [menuOpen, setMenuOpen] = useState(false); - const [calendarOpen, setCalendarOpen] = useState(false); - const ref = useRef(null); - - const label = FILTERS.find((f) => f.value === value)?.label || "All Time"; - - useEffect(() => { - const handler = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) setMenuOpen(false); - }; - document.addEventListener("mousedown", handler); - return () => document.removeEventListener("mousedown", handler); - }, []); - - const startDate = customStart ? new Date(customStart + "T00:00:00") : null; - const endDate = customEnd ? new Date(customEnd + "T00:00:00") : null; - - return ( -
-
- - - {menuOpen && ( -
- {FILTERS.map((f) => ( - - ))} -
- )} -
- - {value === "custom" && ( - - )} - - {calendarOpen && ( -
setCalendarOpen(false)}> -
e.stopPropagation()}> -
- Select Date Range - -
-
- { - if (date) onCustomStartChange(formatDate(date)); - }} - selectsStart - startDate={startDate} - endDate={endDate} - inline - calendarClassName="df-calendar" - dayClassName={(d) => { - if (!startDate || !endDate) return "df-day"; - const ds = formatDate(d); - if (ds >= formatDate(startDate) && ds <= formatDate(endDate)) return "df-day df-day-inrange"; - return "df-day"; - }} - /> - { - if (date) onCustomEndChange(formatDate(date)); - }} - selectsEnd - startDate={startDate} - endDate={endDate} - minDate={startDate || undefined} - inline - calendarClassName="df-calendar" - dayClassName={(d) => { - if (!startDate || !endDate) return "df-day"; - const ds = formatDate(d); - if (ds >= formatDate(startDate) && ds <= formatDate(endDate)) return "df-day df-day-inrange"; - return "df-day"; - }} - /> -
-
- - -
-
-
- )} -
- ); -} - -function formatDate(d: Date): string { - const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, "0"); - const day = String(d.getDate()).padStart(2, "0"); - return `${y}-${m}-${day}`; -} - -export function getDateRange(filter: DateFilterValue, customStart: string, customEnd: string): { start: string; end: string } { - const now = new Date(); - const today = now.toISOString().split("T")[0]; - const toStr = (d: Date) => d.toISOString().split("T")[0]; - - const startOfWeek = new Date(now); startOfWeek.setDate(now.getDate() - now.getDay()); - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const startOfYear = new Date(now.getFullYear(), 0, 1); - - const endOfLastWeek = new Date(now); endOfLastWeek.setDate(now.getDate() - now.getDay() - 1); - const startOfLastWeek = new Date(endOfLastWeek); startOfLastWeek.setDate(endOfLastWeek.getDate() - 6); - - const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0); - - const startOfLastYear = new Date(now.getFullYear() - 1, 0, 1); - const endOfLastYear = new Date(now.getFullYear() - 1, 11, 31); - - switch (filter) { - case "today": return { start: today, end: today }; - case "yesterday": { const y = toStr(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)); return { start: y, end: y }; } - case "this_week": return { start: toStr(startOfWeek), end: today }; - case "last_week": return { start: toStr(startOfLastWeek), end: toStr(endOfLastWeek) }; - case "this_month": return { start: toStr(startOfMonth), end: today }; - case "last_month": return { start: toStr(startOfLastMonth), end: toStr(endOfLastMonth) }; - case "this_year": return { start: toStr(startOfYear), end: today }; - case "last_year": return { start: toStr(startOfLastYear), end: toStr(endOfLastYear) }; - case "all": return { start: "0000-01-01", end: "9999-12-31" }; - case "custom": return { start: customStart || "0000-01-01", end: customEnd || "9999-12-31" }; - } -} - -export function isDateInRange(dateStr: string, start: string, end: string): boolean { - return dateStr >= start && dateStr <= end; -} diff --git a/src/components/DonutChart.tsx b/src/components/DonutChart.tsx deleted file mode 100644 index 686235f..0000000 --- a/src/components/DonutChart.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useMemo } from "react"; - -interface DonutChartItem { - label: string; - value: number; - color?: string | null; -} - -interface DonutChartProps { - title: string; - data: DonutChartItem[]; - emptyMessage?: string; -} - -const FALLBACK_COLORS = [ - "#7aa2f7", - "#9ece6a", - "#f7768e", - "#e0af68", - "#7dcfff", - "#bb9af7", - "#ff9e64", - "#73daca", - "#b4f9f8", - "#565f89", -]; - -export function DonutChart({ - title, - data, - emptyMessage = "No data", -}: DonutChartProps) { - const total = useMemo( - () => data.reduce((sum, item) => sum + item.value, 0), - [data] - ); - - const segments = useMemo(() => { - if (total === 0) return []; - let startAngle = 0; - return data.map((item, index) => { - const fraction = item.value / total; - const angle = fraction * 360; - const endAngle = startAngle + angle; - const color = item.color || FALLBACK_COLORS[index % FALLBACK_COLORS.length]; - const segment = { - ...item, - fraction, - startAngle, - endAngle, - color, - }; - startAngle = endAngle; - return segment; - }); - }, [data, total]); - - return ( -
-
{title}
-
- {total === 0 ? ( -
{emptyMessage}
- ) : ( - <> - - - {segments.map((segment, index) => ( - - ))} - - - {title} - - - {formatTotal(total)} - - -
- {segments.map((segment, index) => ( -
- - - {segment.label} - - - {Math.round(segment.fraction * 100)}% - -
- ))} -
- - )} -
-
- ); -} - -function DonutSegment({ - cx, - cy, - innerRadius, - outerRadius, - startAngle, - endAngle, - color, -}: { - cx: number; - cy: number; - innerRadius: number; - outerRadius: number; - startAngle: number; - endAngle: number; - color: string; -}) { - const sweep = endAngle - startAngle; - - // SVG arcs cannot represent a full 360° circle in one path, so split it. - if (sweep >= 360) { - return ( - <> - {donutArcPath(cx, cy, innerRadius, outerRadius, startAngle, startAngle + 180, color)} - {donutArcPath(cx, cy, innerRadius, outerRadius, startAngle + 180, endAngle, color)} - - ); - } - - return donutArcPath(cx, cy, innerRadius, outerRadius, startAngle, endAngle, color); -} - -function donutArcPath( - cx: number, - cy: number, - innerRadius: number, - outerRadius: number, - startAngle: number, - endAngle: number, - color: string -) { - const start = polarToCartesian(cx, cy, outerRadius, endAngle); - const end = polarToCartesian(cx, cy, outerRadius, startAngle); - const innerStart = polarToCartesian(cx, cy, innerRadius, endAngle); - const innerEnd = polarToCartesian(cx, cy, innerRadius, startAngle); - - const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; - - const path = [ - `M ${start.x} ${start.y}`, - `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 0 ${end.x} ${end.y}`, - `L ${innerEnd.x} ${innerEnd.y}`, - `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 1 ${innerStart.x} ${innerStart.y}`, - "Z", - ].join(" "); - - return ; -} - -function polarToCartesian( - cx: number, - cy: number, - radius: number, - angleInDegrees: number -) { - const angleInRadians = (angleInDegrees * Math.PI) / 180; - return { - x: cx + radius * Math.cos(angleInRadians), - y: cy + radius * Math.sin(angleInRadians), - }; -} - -function formatTotal(amount: number): string { - const dollars = amount / 100; - if (Math.abs(dollars) >= 1000) { - return `$${(dollars / 1000).toFixed(1)}k`; - } - return `$${dollars.toFixed(0)}`; -} diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx deleted file mode 100644 index 68ee1f6..0000000 --- a/src/components/Pagination.tsx +++ /dev/null @@ -1,42 +0,0 @@ -export function paginate(items: T[], page: number, perPage: number): T[] { - const start = (page - 1) * perPage; - return items.slice(start, start + perPage); -} - -export function Pagination({ - page, - perPage, - total, - onPageChange, -}: { - page: number; - perPage: number; - total: number; - onPageChange: (page: number) => void; -}) { - const totalPages = Math.max(1, Math.ceil(total / perPage)); - const start = (page - 1) * perPage + 1; - const end = Math.min(page * perPage, total); - - return ( -
- - - {start}–{end} of {total} - - -
- ); -} diff --git a/src-tauri/src/database/mod.rs b/src/database/mod.rs similarity index 92% rename from src-tauri/src/database/mod.rs rename to src/database/mod.rs index 4d0ad12..d298ca2 100644 --- a/src-tauri/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,16 +1,18 @@ use rusqlite::{Connection, Result}; use std::fs; use std::sync::Mutex; -use tauri::AppHandle; -use tauri::Manager; pub struct DbState(pub Mutex); -pub fn init_database(app: &AppHandle) -> Result> { - let app_dir = app - .path() - .app_data_dir() - .expect("failed to get app data dir"); +/// Same directory the Tauri build used, so existing data keeps loading. +pub fn data_dir() -> std::path::PathBuf { + dirs::data_dir() + .expect("failed to get data dir") + .join("com.chelete.app") +} + +pub fn init_database() -> Result> { + let app_dir = data_dir(); fs::create_dir_all(&app_dir).expect("failed to create app data dir"); let db_path = app_dir.join("chelete.db"); diff --git a/src/date_filter.rs b/src/date_filter.rs new file mode 100644 index 0000000..32d3d9f --- /dev/null +++ b/src/date_filter.rs @@ -0,0 +1,202 @@ +//! Date presets for list filters. Ranges are inclusive ISO date strings, so +//! they compare directly against `transaction_date`. +use chrono::{Datelike, Duration, NaiveDate}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DateFilter { + Today, + Yesterday, + ThisWeek, + LastWeek, + ThisMonth, + LastMonth, + ThisYear, + LastYear, + All, + Custom, +} + +impl DateFilter { + pub const ALL_FILTERS: [DateFilter; 10] = [ + DateFilter::Today, + DateFilter::Yesterday, + DateFilter::ThisWeek, + DateFilter::LastWeek, + DateFilter::ThisMonth, + DateFilter::LastMonth, + DateFilter::ThisYear, + DateFilter::LastYear, + DateFilter::All, + DateFilter::Custom, + ]; + + pub fn value(self) -> &'static str { + match self { + DateFilter::Today => "today", + DateFilter::Yesterday => "yesterday", + DateFilter::ThisWeek => "this_week", + DateFilter::LastWeek => "last_week", + DateFilter::ThisMonth => "this_month", + DateFilter::LastMonth => "last_month", + DateFilter::ThisYear => "this_year", + DateFilter::LastYear => "last_year", + DateFilter::All => "all", + DateFilter::Custom => "custom", + } + } + + pub fn label(self) -> &'static str { + match self { + DateFilter::Today => "Today", + DateFilter::Yesterday => "Yesterday", + DateFilter::ThisWeek => "This Week", + DateFilter::LastWeek => "Last Week", + DateFilter::ThisMonth => "This Month", + DateFilter::LastMonth => "Last Month", + DateFilter::ThisYear => "This Year", + DateFilter::LastYear => "Last Year", + DateFilter::All => "All Time", + DateFilter::Custom => "Custom Range", + } + } + + pub fn from_value(value: &str) -> Option { + Self::ALL_FILTERS.into_iter().find(|f| f.value() == value) + } +} + +const MIN: &str = "0000-01-01"; +const MAX: &str = "9999-12-31"; + +fn iso(d: NaiveDate) -> String { + d.format("%Y-%m-%d").to_string() +} + +fn start_of_week(d: NaiveDate) -> NaiveDate { + d - Duration::days(i64::from(d.weekday().num_days_from_sunday())) +} + +/// Inclusive `(start, end)` for a preset. Weeks start on Sunday. `custom` +/// uses the given bounds, leaving either side open when absent. +pub fn date_range( + filter: DateFilter, + custom_start: Option, + custom_end: Option, + today: NaiveDate, +) -> (String, String) { + let first_of_month = today.with_day(1).unwrap(); + let last_month_end = first_of_month - Duration::days(1); + let week_start = start_of_week(today); + let last_week_end = week_start - Duration::days(1); + match filter { + DateFilter::Today => (iso(today), iso(today)), + DateFilter::Yesterday => { + let y = today - Duration::days(1); + (iso(y), iso(y)) + } + DateFilter::ThisWeek => (iso(week_start), iso(today)), + DateFilter::LastWeek => (iso(last_week_end - Duration::days(6)), iso(last_week_end)), + DateFilter::ThisMonth => (iso(first_of_month), iso(today)), + DateFilter::LastMonth => (iso(last_month_end.with_day(1).unwrap()), iso(last_month_end)), + DateFilter::ThisYear => (iso(NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap()), iso(today)), + DateFilter::LastYear => ( + iso(NaiveDate::from_ymd_opt(today.year() - 1, 1, 1).unwrap()), + iso(NaiveDate::from_ymd_opt(today.year() - 1, 12, 31).unwrap()), + ), + DateFilter::All => (MIN.into(), MAX.into()), + DateFilter::Custom => ( + custom_start.map(iso).unwrap_or_else(|| MIN.into()), + custom_end.map(iso).unwrap_or_else(|| MAX.into()), + ), + } +} + +pub fn is_date_in_range(date: &str, start: &str, end: &str) -> bool { + date >= start && date <= end +} + +#[cfg(test)] +mod tests { + use super::*; + + fn d(y: i32, m: u32, day: u32) -> NaiveDate { + NaiveDate::from_ymd_opt(y, m, day).unwrap() + } + + // Friday 2 October 2026. + fn today() -> NaiveDate { + d(2026, 10, 2) + } + + fn range(f: DateFilter) -> (String, String) { + date_range(f, None, None, today()) + } + + #[test] + fn single_day_presets() { + assert_eq!(range(DateFilter::Today), ("2026-10-02".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::Yesterday), ("2026-10-01".into(), "2026-10-01".into())); + } + + #[test] + fn weeks_start_on_sunday() { + assert_eq!(range(DateFilter::ThisWeek), ("2026-09-27".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::LastWeek), ("2026-09-20".into(), "2026-09-26".into())); + } + + #[test] + fn this_week_on_a_sunday_is_just_that_day() { + let sunday = d(2026, 10, 4); + assert_eq!(date_range(DateFilter::ThisWeek, None, None, sunday), ("2026-10-04".into(), "2026-10-04".into())); + } + + #[test] + fn month_presets() { + assert_eq!(range(DateFilter::ThisMonth), ("2026-10-01".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::LastMonth), ("2026-09-01".into(), "2026-09-30".into())); + } + + #[test] + fn last_month_across_a_year_boundary() { + assert_eq!( + date_range(DateFilter::LastMonth, None, None, d(2026, 1, 15)), + ("2025-12-01".into(), "2025-12-31".into()) + ); + } + + #[test] + fn year_presets() { + assert_eq!(range(DateFilter::ThisYear), ("2026-01-01".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::LastYear), ("2025-01-01".into(), "2025-12-31".into())); + } + + #[test] + fn all_time_covers_everything() { + let (s, e) = range(DateFilter::All); + assert!(is_date_in_range("1999-05-05", &s, &e)); + assert!(is_date_in_range("2999-05-05", &s, &e)); + } + + #[test] + fn custom_ranges_may_be_open_ended() { + let c = |a, b| date_range(DateFilter::Custom, a, b, today()); + assert_eq!(c(Some(d(2026, 3, 1)), Some(d(2026, 3, 31))), ("2026-03-01".into(), "2026-03-31".into())); + assert_eq!(c(Some(d(2026, 3, 1)), None).1, "9999-12-31"); + assert_eq!(c(None, Some(d(2026, 3, 31))).0, "0000-01-01"); + } + + #[test] + fn range_membership_is_inclusive() { + assert!(is_date_in_range("2026-03-01", "2026-03-01", "2026-03-31")); + assert!(is_date_in_range("2026-03-31", "2026-03-01", "2026-03-31")); + assert!(!is_date_in_range("2026-04-01", "2026-03-01", "2026-03-31")); + } + + #[test] + fn values_round_trip() { + for f in DateFilter::ALL_FILTERS { + assert_eq!(DateFilter::from_value(f.value()), Some(f)); + } + assert_eq!(DateFilter::from_value("fortnight"), None); + } +} diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..59ad436 --- /dev/null +++ b/src/export.rs @@ -0,0 +1,170 @@ +//! Data export formats. +use crate::commands::{Account, Category, ExportData, Transaction}; +use std::collections::HashMap; + +fn escape_csv_field(value: &str) -> String { + if value.contains(['"', ',', '\n', '\r']) { + format!("\"{}\"", value.replace('"', "\"\"")) + } else { + value.to_string() + } +} + +pub fn transactions_to_csv(transactions: &[Transaction], accounts: &[Account], categories: &[Category]) -> String { + let account_names: HashMap<&str, &str> = accounts.iter().map(|a| (a.id.as_str(), a.name.as_str())).collect(); + let category_names: HashMap<&str, &str> = categories.iter().map(|c| (c.id.as_str(), c.name.as_str())).collect(); + + let headers = [ + "id", "transaction_date", "description", "merchant", "transaction_type", "amount", "currency", + "account_name", "category_name", "notes", "created_at", "updated_at", + ]; + let mut lines = vec![headers.join(",")]; + for t in transactions { + let fields = [ + t.id.clone(), + t.transaction_date.clone(), + t.description.clone(), + t.merchant.clone().unwrap_or_default(), + t.transaction_type.clone(), + format!("{:.2}", t.amount as f64 / 100.0), + t.currency.clone(), + account_names.get(t.account_id.as_str()).map_or_else(|| t.account_id.clone(), |n| n.to_string()), + t.category_id + .as_deref() + .map(|id| category_names.get(id).map_or_else(|| id.to_string(), |n| n.to_string())) + .unwrap_or_default(), + t.notes.clone().unwrap_or_default(), + t.created_at.clone(), + t.updated_at.clone(), + ]; + lines.push(fields.iter().map(|f| escape_csv_field(f)).collect::>().join(",")); + } + lines.join("\n") +} + +pub fn export_to_json(data: &ExportData) -> Result { + serde_json::to_string_pretty(data).map_err(|e| e.to_string()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExportFormat { + Csv, + Json, +} + +impl ExportFormat { + pub fn default_file_name(self) -> &'static str { + match self { + ExportFormat::Csv => "chelete-transactions.csv", + ExportFormat::Json => "chelete-export.json", + } + } + + pub fn contents(self, data: &ExportData) -> Result { + match self { + ExportFormat::Csv => Ok(transactions_to_csv(&data.transactions, &data.accounts, &data.categories)), + ExportFormat::Json => export_to_json(data), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn txn(desc: &str, merchant: Option<&str>, category: Option<&str>) -> Transaction { + Transaction { + id: "t1".into(), + account_id: "a1".into(), + category_id: category.map(String::from), + transaction_type: "expense".into(), + amount: 1234, + currency: "USD".into(), + description: desc.into(), + merchant: merchant.map(String::from), + notes: None, + transaction_date: "2026-03-09".into(), + created_at: "c".into(), + updated_at: "u".into(), + } + } + + fn account() -> Account { + Account { + id: "a1".into(), + name: "Main".into(), + account_type: "bank".into(), + currency: "USD".into(), + balance: 0, + color: None, + icon: None, + is_active: true, + sort_order: 0, + created_at: String::new(), + updated_at: String::new(), + } + } + + fn category() -> Category { + Category { + id: "c1".into(), + name: "Food".into(), + parent_id: None, + category_type: "expense".into(), + icon: None, + color: None, + sort_order: 0, + created_at: String::new(), + updated_at: String::new(), + } + } + + #[test] + fn csv_has_header_and_resolved_names() { + let csv = transactions_to_csv(&[txn("Lunch", Some("Cafe"), Some("c1"))], &[account()], &[category()]); + let mut lines = csv.lines(); + assert_eq!( + lines.next().unwrap(), + "id,transaction_date,description,merchant,transaction_type,amount,currency,account_name,category_name,notes,created_at,updated_at" + ); + assert_eq!(lines.next().unwrap(), "t1,2026-03-09,Lunch,Cafe,expense,12.34,USD,Main,Food,,c,u"); + assert!(lines.next().is_none()); + } + + #[test] + fn csv_quotes_commas_quotes_and_newlines() { + let csv = transactions_to_csv(&[txn("Dinner, \"fancy\"\nlate", None, None)], &[account()], &[]); + assert!(csv.contains("\"Dinner, \"\"fancy\"\"\nlate\""), "{csv}"); + } + + #[test] + fn unknown_ids_fall_back_to_the_raw_id() { + let csv = transactions_to_csv(&[txn("x", None, Some("gone"))], &[], &[]); + assert!(csv.contains(",a1,gone,"), "{csv}"); + } + + #[test] + fn empty_export_is_just_the_header() { + assert_eq!(transactions_to_csv(&[], &[], &[]).lines().count(), 1); + } + + #[test] + fn json_round_trips() { + let data = ExportData { + accounts: vec![account()], + categories: vec![category()], + transactions: vec![txn("Lunch", None, None)], + subscriptions: vec![], + }; + let json = export_to_json(&data).unwrap(); + let back: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!(back["transactions"][0]["description"], "Lunch"); + assert_eq!(back["accounts"][0]["name"], "Main"); + } + + #[test] + fn file_names() { + assert_eq!(ExportFormat::Csv.default_file_name(), "chelete-transactions.csv"); + assert_eq!(ExportFormat::Json.default_file_name(), "chelete-export.json"); + } +} diff --git a/src/features/accounts/AccountsScreen.tsx b/src/features/accounts/AccountsScreen.tsx deleted file mode 100644 index 090ff4d..0000000 --- a/src/features/accounts/AccountsScreen.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { useEffect, useState } from "react"; -import { api } from "../../services/api"; -import { formatBalance, accountTypeLabel } from "../../services/format"; -import type { Account } from "../../types"; -import { ArrowRightLeft } from "lucide-react"; - -export function AccountsScreen() { - const [accounts, setAccounts] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [showTransfer, setShowTransfer] = useState(false); - const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); - - const showNotification = (message: string, type: "success" | "error" = "success") => { - setToast({ message, type }); - setTimeout(() => setToast(null), 3000); - }; - - const load = () => { - api.getAccounts().then(setAccounts); - }; - - useEffect(() => { - load(); - }, []); - - return ( -
-
-
Accounts
-
- - -
-
- - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {showTransfer && ( - { - setShowTransfer(false); - load(); - }} - onCancel={() => setShowTransfer(false)} - showNotification={showNotification} - /> - )} - - {toast && ( -
- {toast.message} -
- )} - - {accounts.length === 0 ? ( -
- No accounts yet. Add one to get started. -
- ) : ( -
- {accounts.map((a) => ( -
-
-
-
{accountTypeLabel(a.account_type)}
-
- {a.name} -
-
-
-
{formatBalance(a.balance)}
-
- -
-
- ))} -
- )} -
- ); -} - -function TransferDialog({ - accounts, - onDone, - onCancel, - showNotification, -}: { - accounts: Account[]; - onDone: () => void; - onCancel: () => void; - showNotification: (message: string, type?: "success" | "error") => void; -}) { - const [fromAccountId, setFromAccountId] = useState(accounts[0]?.id || ""); - const [toAccountId, setToAccountId] = useState( - accounts[1]?.id || accounts[0]?.id || "" - ); - const [amount, setAmount] = useState(""); - const [notes, setNotes] = useState(""); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const cents = Math.round(parseFloat(amount) * 100); - if (isNaN(cents) || cents <= 0 || fromAccountId === toAccountId) return; - - try { - await api.transfer({ - from_account_id: fromAccountId, - to_account_id: toAccountId, - amount: cents, - currency: "USD", - notes: notes || undefined, - }); - showNotification( - `Transferred $${amount} to ${accounts.find((a) => a.id === toAccountId)?.name}` - ); - onDone(); - } catch (err: any) { - showNotification( - `Transfer failed: ${err?.message || String(err)}`, - "error" - ); - } - }; - - return ( -
-
e.stopPropagation()}> -
Transfer Money
-
-
-
- - -
-
- - -
-
- -
- - setAmount(e.target.value)} - autoFocus - /> -
- -
- - setNotes(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} - -function AddAccountForm({ - onDone, - onCancel, -}: { - onDone: () => void; - onCancel: () => void; -}) { - const [name, setName] = useState(""); - const [type, setType] = useState("cash"); - const [balance, setBalance] = useState(""); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim()) return; - - const cents = Math.round(parseFloat(balance || "0") * 100); - await api.createAccount({ - name: name.trim(), - account_type: type, - currency: "USD", - balance: cents, - }); - onDone(); - }; - - return ( -
-
e.stopPropagation()}> -
Add Account
-
-
- - setName(e.target.value)} - autoFocus - /> -
- -
- - -
- -
- - setBalance(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} diff --git a/src/features/categories/CategoriesScreen.tsx b/src/features/categories/CategoriesScreen.tsx deleted file mode 100644 index 5f39d40..0000000 --- a/src/features/categories/CategoriesScreen.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import type { Category, Transaction } from "../../types"; -import { - CATEGORY_ICONS, - CategoryIcon, -} from "../../components/CategoryIcons"; -import { Pencil, Trash2 } from "lucide-react"; - -export function CategoriesScreen() { - const [categories, setCategories] = useState([]); - const [transactions, setTransactions] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [editing, setEditing] = useState(null); - - const load = () => { - api.getCategories().then(setCategories); - api.getTransactions().then(setTransactions); - }; - - useEffect(() => { - load(); - }, []); - - const expenseCategories = categories.filter( - (c) => c.category_type === "expense" - ); - const incomeCategories = categories.filter( - (c) => c.category_type === "income" - ); - - const transactionCounts = useMemo(() => { - const counts: Record = {}; - transactions.forEach((t) => { - if (t.category_id) { - counts[t.category_id] = (counts[t.category_id] || 0) + 1; - } - }); - return counts; - }, [transactions]); - - const CategoryRow = ({ c }: { c: Category }) => { - const count = transactionCounts[c.id] || 0; - return ( - - - {c.icon && ( - - )} - {c.name} - - - 0 ? "var(--chelete-fg-muted)" : "var(--chelete-fg-subtle)", - }} - > - {count} {count === 1 ? "record" : "records"} - - - - - - - - ); - }; - - return ( -
-
-
Categories
- -
- - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {editing && ( - { - setEditing(null); - load(); - }} - onCancel={() => setEditing(null)} - /> - )} - -
-
-
- Expenses -
- {expenseCategories.length === 0 ? ( -
- No expense categories -
- ) : ( -
- - - - - - - - - - {expenseCategories.map((c) => ( - - ))} - -
NameRecords
-
- )} -
- -
-
- Income -
- {incomeCategories.length === 0 ? ( -
- No income categories -
- ) : ( -
- - - - - - - - - - {incomeCategories.map((c) => ( - - ))} - -
NameRecords
-
- )} -
-
-
- ); -} - -function AddCategoryForm({ - onDone, - onCancel, -}: { - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.createCategory(values); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function EditCategoryForm({ - category, - onDone, - onCancel, -}: { - category: Category; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.updateCategory({ - id: category.id, - name: values.name, - category_type: values.category_type, - icon: values.icon, - }); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function CategoryForm({ - title, - initialName, - initialType, - initialIcon, - onSubmit, - onCancel, -}: { - title: string; - initialName: string; - initialType: string; - initialIcon: string; - onSubmit: (values: { name: string; category_type: string; icon: string }) => void | Promise; - onCancel: () => void; -}) { - const [name, setName] = useState(initialName); - const [type, setType] = useState(initialType); - const [selectedIcon, setSelectedIcon] = useState(initialIcon); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim()) return; - - await onSubmit({ - name: name.trim(), - category_type: type, - icon: selectedIcon, - }); - }; - - return ( -
-
e.stopPropagation()}> -
{title}
-
-
- - setName(e.target.value)} - autoFocus - /> -
- -
- - -
- -
- -
- {CATEGORY_ICONS.map((ic) => { - const Icon = ic.icon; - return ( - - ); - })} -
-
- -
- - -
-
-
-
- ); -} diff --git a/src/features/overview/OverviewScreen.tsx b/src/features/overview/OverviewScreen.tsx deleted file mode 100644 index e9de83d..0000000 --- a/src/features/overview/OverviewScreen.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import { - formatBalance, - formatMoney, - formatDate, - currentMonth, -} from "../../services/format"; -import type { Overview, Transaction, Account, Category } from "../../types"; -import { - Wallet, - TrendingUp, - TrendingDown, - Activity, - CreditCard, -} from "lucide-react"; -import { - DateFilter, - getDateRange, - isDateInRange, - type DateFilterValue, -} from "../../components/DateFilter"; -import { DonutChart } from "../../components/DonutChart"; -import { Pagination, paginate } from "../../components/Pagination"; -import { CategoryIcon } from "../../components/CategoryIcons"; - -export function OverviewScreen() { - const [overview, setOverview] = useState(null); - const [accounts, setAccounts] = useState([]); - const [categories, setCategories] = useState([]); - const [allTransactions, setAllTransactions] = useState([]); - const [filterType, setFilterType] = useState<"all" | "income" | "expense">("all"); - const [filterAccount, setFilterAccount] = useState("all"); - const [dateFilter, setDateFilter] = useState("all"); - const [customStart, setCustomStart] = useState(""); - const [customEnd, setCustomEnd] = useState(""); - const [transactionPage, setTransactionPage] = useState(1); - const TRANSACTIONS_PER_PAGE = 10; - - useEffect(() => { - api.getOverview().then(setOverview); - api.getAccounts().then(setAccounts); - api.getCategories().then(setCategories); - api.getTransactions().then(setAllTransactions); - }, []); - - const categoryMap = useMemo( - () => Object.fromEntries(categories.map((c) => [c.id, c])), - [categories] - ); - - const { start, end } = getDateRange(dateFilter, customStart, customEnd); - - const filteredTransactions = useMemo(() => { - let txns = allTransactions.filter((t) => isDateInRange(t.transaction_date, start, end)); - if (filterType !== "all") { - txns = txns.filter((t) => t.transaction_type === filterType); - } - if (filterAccount !== "all") { - txns = txns.filter((t) => t.account_id === filterAccount); - } - return txns; - }, [allTransactions, start, end, filterType, filterAccount]); - - useEffect(() => { - setTransactionPage(1); - }, [filterType, filterAccount, dateFilter, customStart, customEnd]); - - const filteredOverview = useMemo(() => { - if (!overview) return null; - const dateTxns = allTransactions.filter((t) => - isDateInRange(t.transaction_date, start, end) - ); - const total_income = dateTxns - .filter((t) => t.transaction_type === "income") - .reduce((sum, t) => sum + t.amount, 0); - const total_expenses = dateTxns - .filter((t) => t.transaction_type === "expense") - .reduce((sum, t) => sum + t.amount, 0); - - const spentByCategory: Record = {}; - const earnedByCategory: Record = {}; - for (const t of dateTxns) { - if (t.category_id) { - if (t.transaction_type === "expense") { - spentByCategory[t.category_id] = - (spentByCategory[t.category_id] || 0) + t.amount; - } else if (t.transaction_type === "income") { - earnedByCategory[t.category_id] = - (earnedByCategory[t.category_id] || 0) + t.amount; - } - } - } - - const category_spending = Object.entries(spentByCategory) - .map(([category_id, spent]) => ({ - category_id, - category_name: categoryMap[category_id]?.name || "Unknown", - spent, - budget_limit: null as number | null, - icon: categoryMap[category_id]?.icon || null, - })) - .sort((a, b) => b.spent - a.spent); - - const income_by_category = Object.entries(earnedByCategory) - .map(([category_id, value]) => ({ - label: categoryMap[category_id]?.name || "Unknown", - value, - color: categoryMap[category_id]?.color || null, - })) - .sort((a, b) => b.value - a.value); - - const expenses_by_category = Object.entries(spentByCategory) - .map(([category_id, value]) => ({ - label: categoryMap[category_id]?.name || "Unknown", - value, - color: categoryMap[category_id]?.color || null, - })) - .sort((a, b) => b.value - a.value); - - return { - ...overview, - total_income, - total_expenses, - category_spending, - income_by_category, - expenses_by_category, - }; - }, [overview, allTransactions, start, end, categoryMap]); - - if (!overview || !filteredOverview) { - return ( -
- Loading... -
- ); - } - - const net = filteredOverview.total_income - filteredOverview.total_expenses; - const savingsRate = - filteredOverview.total_income > 0 - ? Math.round((net / filteredOverview.total_income) * 100) - : 0; - - return ( -
-
-
-
Overview
-
{currentMonth()}
-
-
- - - -
-
-
-
Total Balance
- -
-
- {formatBalance(overview.total_balance)} -
-
-
-
-
Income
- -
-
- {formatMoney(filteredOverview.total_income)} -
-
-
-
-
Expenses
- -
-
- {formatMoney(-filteredOverview.total_expenses)} -
-
-
-
-
Net
- -
-
= 0 ? "positive" : "negative"}`}> - {formatMoney(net)} -
-
Savings rate: {savingsRate}%
-
-
- - {accounts.length > 0 && ( - <> -
- Accounts -
-
- {accounts.map((a) => ( -
-
-
{a.name}
- -
-
{formatBalance(a.balance)}
-
- ))} -
- - )} - -
- Breakdown -
-
- - -
- -
- Recent Transactions -
-
- - -
- - {filteredTransactions.length > 0 ? ( - <> -
- - - - - - - - - - - {paginate(filteredTransactions, transactionPage, TRANSACTIONS_PER_PAGE).map((t) => { - const cat = t.category_id ? categoryMap[t.category_id] : null; - return ( - - - - - - - ); - })} - -
DateDescriptionCategoryAmount
- {formatDate(t.transaction_date)} - {t.description} - {cat ? ( - - {cat.icon && ( - - )} - {cat.name} - - ) : ( - — - )} - - {formatMoney( - t.transaction_type === "expense" ? -t.amount : t.amount - )} -
-
- - - ) : ( -
- No transactions match the selected filters. -
- )} - - {filteredTransactions.length === 0 && overview.accounts.length === 0 && ( -
-
- Welcome to Chelete -
-
- Press{" "} - - Ctrl+K - {" "} - to open the command palette, or{" "} - - a - {" "} - to add your first transaction. -
-
- )} -
- ); -} - - diff --git a/src/features/projections/ProjectionsScreen.tsx b/src/features/projections/ProjectionsScreen.tsx deleted file mode 100644 index e35894a..0000000 --- a/src/features/projections/ProjectionsScreen.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { api } from "../../services/api"; -import { - computeProjection, - normalizeSubscriptionToPeriod, - PERIOD_PLURAL_LABELS, - PERIOD_LIMITS, - PERIOD_UNIT_LABELS, - type ProjectionPeriodType, -} from "../../services/projection"; -import { formatBalance, formatMoney, formatDateFull } from "../../services/format"; -import type { Subscription } from "../../types"; -import { LineChart, TrendingUp, Target, Calendar, Repeat } from "lucide-react"; -import { - ResponsiveContainer, - LineChart as RechartsLineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ReferenceLine, -} from "recharts"; - -const PERIOD_OPTIONS: { value: ProjectionPeriodType; label: string }[] = [ - { value: "weeks", label: "Weeks" }, - { value: "months", label: "Months" }, - { value: "years", label: "Years" }, -]; - -type ExpenseSource = "manual" | "subscriptions"; - -const EXPENSE_SOURCE_OPTIONS: { value: ExpenseSource; label: string }[] = [ - { value: "manual", label: "Manual" }, - { value: "subscriptions", label: "Subscriptions" }, -]; - -export function ProjectionsScreen() { - const [startingBalance, setStartingBalance] = useState("0"); - const [income, setIncome] = useState(""); - const [expense, setExpense] = useState(""); - const [expenseSource, setExpenseSource] = useState("manual"); - const [subscriptions, setSubscriptions] = useState([]); - const [subscriptionsLoaded, setSubscriptionsLoaded] = useState(false); - const [periodType, setPeriodType] = useState("months"); - const [periods, setPeriods] = useState("10"); - - useEffect(() => { - if (expenseSource === "subscriptions" && !subscriptionsLoaded) { - api - .getSubscriptions() - .then((subs) => setSubscriptions(subs.filter((s) => s.is_active))) - .finally(() => setSubscriptionsLoaded(true)); - } - }, [expenseSource, subscriptionsLoaded]); - - const subscriptionsExpense = useMemo(() => { - if (expenseSource !== "subscriptions") return 0; - return subscriptions.reduce( - (sum, s) => sum + normalizeSubscriptionToPeriod(s.amount, s.frequency, periodType), - 0 - ); - }, [subscriptions, expenseSource, periodType]); - - const parsed = useMemo(() => { - const startingBalanceCents = parseFloatDollarsToCents(startingBalance); - const incomeCents = parseFloatDollarsToCents(income); - const manualExpenseCents = parseFloatDollarsToCents(expense); - const expenseCents = - expenseSource === "subscriptions" ? subscriptionsExpense : manualExpenseCents; - const periodCount = parseInt(periods, 10); - const valid = - startingBalanceCents !== null && - incomeCents !== null && - expenseCents !== null && - Number.isFinite(periodCount) && - periodCount >= 1 && - periodCount <= PERIOD_LIMITS[periodType]; - return { startingBalanceCents, incomeCents, expenseCents, periodCount, valid }; - }, [startingBalance, income, expense, expenseSource, subscriptionsExpense, periodType, periods]); - - const result = useMemo(() => { - if (!parsed.valid) return null; - return computeProjection({ - startingBalance: parsed.startingBalanceCents!, - income: parsed.incomeCents!, - expense: parsed.expenseCents!, - periodType, - periods: parsed.periodCount, - }); - }, [parsed, periodType]); - - const summary = result?.summary ?? null; - const growing = (summary?.netPerPeriod ?? 0) > 0; - const declining = (summary?.netPerPeriod ?? 0) < 0; - - return ( -
-
-
-
Projections
-
- Project your balance growth over time -
-
-
- -
-
-
- - setStartingBalance(e.target.value)} - /> -
-
- - setIncome(e.target.value)} - /> -
-
- - -
- {expenseSource === "manual" ? ( -
- - setExpense(e.target.value)} - /> -
- ) : ( -
-
- - - {subscriptions.length} active subscription - {subscriptions.length === 1 ? "" : "s"} - -
- {subscriptions.length > 0 && ( -
- {subscriptions.map((s) => ( -
- {s.name} - - {formatMoney( - normalizeSubscriptionToPeriod(s.amount, s.frequency, periodType) - )} - /{PERIOD_UNIT_LABELS[periodType]} - -
- ))} -
- )} -
- Total: {formatMoney(subscriptionsExpense)}/{PERIOD_UNIT_LABELS[periodType]} -
- {subscriptionsLoaded && subscriptions.length === 0 && ( -
- No active subscriptions found -
- )} -
- )} -
- - -
-
- - setPeriods(e.target.value)} - /> -
- 1 – {PERIOD_LIMITS[periodType]} {PERIOD_PLURAL_LABELS[periodType]} -
-
-
- -
- {!result || !summary ? ( -
- -
Enter your income, expense and period to see a projection.
-
- ) : ( - <> -
-
-
-
- Projected Balance -
- -
-
- {formatBalance(summary.finalBalance)} -
-
- in {parsed.periodCount} {PERIOD_PLURAL_LABELS[periodType]} - {summary.projectedDate - ? ` · ${formatDateFull(summary.projectedDate)}` - : ""} -
-
-
-
-
Total Change
- -
-
= 0 ? "positive" : "negative"}`} - > - {formatMoney(summary.totalIncrease)} -
-
from starting balance
-
-
-
-
- Net per {capitalize(PERIOD_UNIT_LABELS[periodType])} -
- -
-
- {formatMoney(summary.netPerPeriod)} -
-
- {summary.retentionRate !== null - ? `Keeping ${summary.retentionRate.toFixed(1)}% of income` - : "No income entered"} -
-
-
- -
-
Balance Over Time
-
- - - - - formatMoneyShortAxis(v)} - tick={{ fill: "var(--chelete-fg-muted)", fontSize: 11 }} - tickLine={false} - axisLine={false} - width={72} - /> - [formatBalance(Number(value)), "Balance"]} - contentStyle={{ - background: "var(--chelete-surface)", - border: "1px solid var(--chelete-border)", - borderRadius: 4, - fontSize: 12, - color: "var(--chelete-fg)", - }} - labelStyle={{ color: "var(--chelete-fg-muted)" }} - /> - {summary.finalBalance < 0 && ( - - )} - - - -
-
- - )} -
-
-
- ); -} - -function capitalize(word: string): string { - return word.charAt(0).toUpperCase() + word.slice(1); -} - -function parseFloatDollarsToCents(value: string): number | null { - const trimmed = value.trim(); - if (trimmed === "") return null; - const parsed = Number(trimmed); - if (!Number.isFinite(parsed)) return null; - return Math.round(parsed * 100); -} - -function formatMoneyShortAxis(amount: number): string { - const abs = Math.abs(amount); - const dollars = abs / 100; - const sign = amount < 0 ? "-" : ""; - if (dollars >= 1_000_000) return `${sign}$${(dollars / 1_000_000).toFixed(1)}M`; - if (dollars >= 1000) return `${sign}$${(dollars / 1000).toFixed(1)}k`; - return `${sign}$${dollars.toFixed(0)}`; -} \ No newline at end of file diff --git a/src/features/settings/SettingsScreen.tsx b/src/features/settings/SettingsScreen.tsx deleted file mode 100644 index 116d964..0000000 --- a/src/features/settings/SettingsScreen.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useTheme } from "../../hooks/useTheme"; -import { useEffect, useState } from "react"; - -export function SettingsScreen() { - const theme = useTheme(); - const [rounded, setRounded] = useState(() => { - return localStorage.getItem("chelete-radius") !== "square"; - }); - - useEffect(() => { - const radius = rounded ? "4px" : "0px"; - document.documentElement.style.setProperty("--chelete-radius", radius); - localStorage.setItem("chelete-radius", rounded ? "rounded" : "square"); - }, [rounded]); - - return ( -
-
-
Settings
-
- -
-
-
- Appearance -
-
-
-
-
Theme
-
- Follows Omarchy system theme -
-
-
- {theme?.name || "Loading..."} -
-
- -
- -
-
-
Corners
-
- {rounded ? "Rounded" : "Square"} corners -
-
- -
-
-
- -
-
- Finance -
-
-
-
-
Base Currency
-
-
USD
-
-
-
-
-
Month Start
-
-
1st
-
-
-
- -
-
- ); -} diff --git a/src/features/subscriptions/SubscriptionsScreen.tsx b/src/features/subscriptions/SubscriptionsScreen.tsx deleted file mode 100644 index e2825c0..0000000 --- a/src/features/subscriptions/SubscriptionsScreen.tsx +++ /dev/null @@ -1,814 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import { formatMoney } from "../../services/format"; -import type { Subscription, Category, Account } from "../../types"; -import { Pagination, paginate } from "../../components/Pagination"; -import { Eye, Pencil, Trash2, CreditCard } from "lucide-react"; - -const FREQUENCIES = [ - { value: "weekly", label: "Weekly" }, - { value: "bi_weekly", label: "Bi-weekly" }, - { value: "monthly", label: "Monthly" }, - { value: "quarterly", label: "Quarterly" }, - { value: "bi_yearly", label: "Bi-yearly" }, - { value: "yearly", label: "Yearly" }, -]; - -const FREQUENCY_LABELS: Record = Object.fromEntries( - FREQUENCIES.map((f) => [f.value, f.label]) -); - -function getNextPaymentDate( - startDate: string | null, - frequency: string -): string | null { - if (!startDate) return null; - - const start = new Date(startDate + "T00:00:00"); - if (isNaN(start.getTime())) return null; - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const intervals: Record = { - weekly: { days: 7 }, - bi_weekly: { days: 14 }, - monthly: { months: 1 }, - quarterly: { months: 3 }, - bi_yearly: { months: 6 }, - yearly: { months: 12 }, - }; - - const interval = intervals[frequency]; - if (!interval) return startDate; - - const next = new Date(start); - const maxIterations = 120; - let iterations = 0; - - if (interval.days) { - while (next < today && iterations < maxIterations) { - next.setDate(next.getDate() + interval.days); - iterations++; - } - } else if (interval.months) { - while (next < today && iterations < maxIterations) { - const year = next.getFullYear(); - const month = next.getMonth() + interval.months; - next.setFullYear(year, month, next.getDate()); - iterations++; - } - } - - return next.toISOString().split("T")[0]; -} - -import { - CategoryIcon, -} from "../../components/CategoryIcons"; - -export function SubscriptionsScreen() { - const [subscriptions, setSubscriptions] = useState([]); - const [categories, setCategories] = useState([]); - const [accounts, setAccounts] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [filterFrequency, setFilterFrequency] = useState("all"); - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(25); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [confirmDelete, setConfirmDelete] = useState<{ - ids: string[]; - count: number; - } | null>(null); - const [viewing, setViewing] = useState(null); - const [editing, setEditing] = useState(null); - const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); - const PER_PAGE_OPTIONS = [10, 20, 25, 30, 40, 60, 80, 100]; - - const load = () => { - api.getSubscriptions().then(setSubscriptions); - api.getCategories().then(setCategories); - api.getAccounts().then(setAccounts); - }; - - useEffect(() => { - load(); - }, []); - - useEffect(() => { - setPage(1); - }, [filterFrequency, perPage]); - - const categoryMap = useMemo( - () => Object.fromEntries(categories.map((c) => [c.id, c])), - [categories] - ); - - const accountMap = useMemo( - () => Object.fromEntries(accounts.map((a) => [a.id, a])), - [accounts] - ); - - const filteredSubscriptions = useMemo(() => { - if (filterFrequency === "all") return subscriptions; - return subscriptions.filter((s) => s.frequency === filterFrequency); - }, [subscriptions, filterFrequency]); - - const paginatedSubscriptions = useMemo( - () => paginate(filteredSubscriptions, page, perPage), - [filteredSubscriptions, page, perPage] - ); - - const totalAmount = useMemo(() => { - return filteredSubscriptions.reduce((sum, s) => sum + s.amount, 0); - }, [filteredSubscriptions]); - - const selectedOnPage = useMemo(() => { - if (paginatedSubscriptions.length === 0) return false; - return paginatedSubscriptions.every((s) => selectedIds.has(s.id)); - }, [paginatedSubscriptions, selectedIds]); - - const someSelectedOnPage = useMemo(() => { - return paginatedSubscriptions.some((s) => selectedIds.has(s.id)); - }, [paginatedSubscriptions, selectedIds]); - - const toggleSelection = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }; - - const togglePageSelection = () => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (selectedOnPage) { - paginatedSubscriptions.forEach((s) => next.delete(s.id)); - } else { - paginatedSubscriptions.forEach((s) => next.add(s.id)); - } - return next; - }); - }; - - const handleDelete = async (ids: string[]) => { - await api.deleteSubscriptions(ids); - setSelectedIds((prev) => { - const next = new Set(prev); - ids.forEach((id) => next.delete(id)); - return next; - }); - load(); - }; - - const showToast = (message: string, type: "success" | "error" = "success") => { - setToast({ message, type }); - setTimeout(() => setToast(null), 3000); - }; - - const handlePay = async (s: Subscription) => { - try { - const today = new Date().toISOString().split("T")[0]; - await api.createTransaction({ - account_id: s.account_id || accounts[0]?.id, - category_id: s.category_id || undefined, - transaction_type: "expense", - amount: s.amount, - currency: s.currency, - description: s.name, - transaction_date: today, - }); - showToast(`Payment recorded for ${s.name}`); - load(); - } catch (err) { - showToast("Failed to record payment", "error"); - } - }; - - return ( -
-
-
Subscriptions
-
- {selectedIds.size > 0 ? ( - - ) : ( - <> - - - )} -
-
- - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {editing && ( - { - setEditing(null); - load(); - }} - onCancel={() => setEditing(null)} - /> - )} - - {viewing && ( - setViewing(null)} - /> - )} - -
- -
- -
-
-
-
- Total ({filterFrequency === "all" ? "All Periods" : FREQUENCY_LABELS[filterFrequency]}) -
-
-
{formatMoney(totalAmount)}
-
-
-
-
Subscriptions
-
-
{filteredSubscriptions.length}
-
-
- - {filteredSubscriptions.length === 0 ? ( -
- No subscriptions found. -
- ) : ( - <> -
- - - - - - - - - - - - - - - - {paginatedSubscriptions.map((s) => { - const category = s.category_id ? categoryMap[s.category_id] : null; - const account = s.account_id ? accountMap[s.account_id] : null; - const nextPayment = getNextPaymentDate(s.start_date, s.frequency); - return ( - - - - - - - - - - - - ); - })} - -
- { - if (el) el.indeterminate = someSelectedOnPage && !selectedOnPage; - }} - onChange={togglePageSelection} - /> - NameCategoryAccountPeriodStart DateNext PaymentAmount
- toggleSelection(s.id)} - /> - {s.name} - {category ? ( - - {category.icon && ( - - )} - {category.name} - - ) : ( - — - )} - {account?.name || —}{FREQUENCY_LABELS[s.frequency] || s.frequency}{s.start_date || —}{nextPayment || —}{formatMoney(s.amount)} - - - - -
-
- -
-
- Show - - per page -
- -
- - )} - - {confirmDelete && ( - { - await handleDelete(confirmDelete.ids); - setConfirmDelete(null); - }} - onCancel={() => setConfirmDelete(null)} - /> - )} - - {toast && ( -
- {toast.message} -
- )} -
- ); -} - -function DeleteConfirmDialog({ - count, - onConfirm, - onCancel, -}: { - count: number; - onConfirm: () => void; - onCancel: () => void; -}) { - return ( -
-
e.stopPropagation()}> -
- Delete {count} subscription{count === 1 ? "" : "s"}? -
-

- This will permanently remove the selected subscription{count === 1 ? "" : "s"}. This action - cannot be undone. -

-
- - -
-
-
- ); -} - -function ViewSubscriptionDialog({ - subscription, - category, - account, - onClose, -}: { - subscription: Subscription; - category?: Category | null; - account?: Account | null; - onClose: () => void; -}) { - const nextPayment = getNextPaymentDate(subscription.start_date, subscription.frequency); - return ( -
-
e.stopPropagation()}> -
Subscription Details
-
- Name - {subscription.name} - - Amount - {formatMoney(subscription.amount)} - - Period - {FREQUENCY_LABELS[subscription.frequency] || subscription.frequency} - - Start Date - {subscription.start_date || "—"} - - Next Payment - {nextPayment || "—"} - - Account - {account?.name || "—"} - - Category - - {category ? ( - - {category.icon && ( - - )} - {category.name} - - ) : ( - "—" - )} - -
-
- -
-
-
- ); -} - -function AddSubscriptionForm({ - categories, - accounts, - onDone, - onCancel, -}: { - categories: Category[]; - accounts: Account[]; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.createSubscription(values); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function EditSubscriptionForm({ - categories, - accounts, - subscription, - onDone, - onCancel, -}: { - categories: Category[]; - accounts: Account[]; - subscription: Subscription; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.updateSubscription({ - id: subscription.id, - name: values.name, - amount: values.amount, - currency: "USD", - frequency: values.frequency, - category_id: values.category_id || undefined, - account_id: values.account_id || undefined, - start_date: values.start_date || undefined, - }); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function SubscriptionForm({ - title, - categories, - accounts, - initialName = "", - initialAmount = "", - initialFrequency = "monthly", - initialCategoryId = "", - initialAccountId = "", - initialStartDate = "", - onSubmit, - onCancel, -}: { - title: string; - categories: Category[]; - accounts: Account[]; - initialName?: string; - initialAmount?: string; - initialFrequency?: string; - initialCategoryId?: string; - initialAccountId?: string; - initialStartDate?: string; - onSubmit: (values: { - name: string; - amount: number; - currency: string; - frequency: string; - category_id?: string; - account_id?: string; - start_date?: string; - }) => void | Promise; - onCancel: () => void; -}) { - const [name, setName] = useState(initialName); - const [amount, setAmount] = useState(initialAmount); - const [frequency, setFrequency] = useState(initialFrequency); - const [categoryId, setCategoryId] = useState(initialCategoryId); - const [accountId, setAccountId] = useState(initialAccountId); - const [startDate, setStartDate] = useState(initialStartDate); - - const expenseCategories = categories.filter((c) => c.category_type === "expense"); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim() || !amount) return; - - const cents = Math.round(parseFloat(amount) * 100); - await onSubmit({ - name: name.trim(), - amount: cents, - currency: "USD", - frequency, - category_id: categoryId || undefined, - account_id: accountId || undefined, - start_date: startDate || undefined, - }); - }; - - return ( -
-
e.stopPropagation()}> -
{title}
-
-
- - setName(e.target.value)} - autoFocus - /> -
- -
- - setAmount(e.target.value)} - /> -
- -
- - -
- -
- - -
- -
- - -
- -
- - setStartDate(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} diff --git a/src/features/transactions/CsvImportDialog.tsx b/src/features/transactions/CsvImportDialog.tsx deleted file mode 100644 index 4535154..0000000 --- a/src/features/transactions/CsvImportDialog.tsx +++ /dev/null @@ -1,825 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { api } from "../../services/api"; -import { formatMoney, formatDate } from "../../services/format"; -import type { Account, Category, CsvField, CsvMapping, CsvPreview, ImportResult, ImportOptions, ParsedRow } from "../../types"; - -const FIELD_OPTIONS: { value: CsvField; label: string }[] = [ - { value: "date", label: "Date" }, - { value: "description", label: "Description" }, - { value: "merchant", label: "Merchant" }, - { value: "amount", label: "Amount" }, - { value: "income_amount", label: "Income Amount" }, - { value: "expense_amount", label: "Expense Amount" }, - { value: "type", label: "Type (income/expense)" }, - { value: "category", label: "Category" }, - { value: "account", label: "Account" }, - { value: "currency", label: "Currency" }, - { value: "notes", label: "Notes" }, - { value: "ignore", label: "Ignore" }, -]; - -const DEFAULT_TYPE_ALIASES: Record = { - debit: "expense", - credit: "income", - dr: "expense", - cr: "income", - in: "income", - out: "expense", - expense: "expense", - income: "income", -}; - -const MAPPING_STORAGE_KEY = "chelete-csv-mapping"; - -function buildMappingKey(headers: string[]): string { - return headers.map((h) => h.trim().toLowerCase().replace(/[^a-z0-9]/g, "")).join(","); -} - -function loadSavedMapping(headers: string[]): CsvMapping | null { - try { - const raw = localStorage.getItem(MAPPING_STORAGE_KEY); - if (!raw) return null; - const data = JSON.parse(raw); - return data[buildMappingKey(headers)] ?? null; - } catch { - return null; - } -} - -function saveMapping(headers: string[], mapping: CsvMapping) { - try { - const raw = localStorage.getItem(MAPPING_STORAGE_KEY); - const data = raw ? JSON.parse(raw) : {}; - data[buildMappingKey(headers)] = mapping; - localStorage.setItem(MAPPING_STORAGE_KEY, JSON.stringify(data)); - } catch { - // ignore storage errors - } -} - -function suggestField(header: string): CsvField | undefined { - const h = header.trim().toLowerCase().replace(/[\s_-]/g, ""); - if (["date", "transactiondate", "transdate", "posted", "datetime"].includes(h)) return "date"; - if (["description", "payee", "memo", "details", "narrative"].includes(h)) return "description"; - if (h === "merchant") return "merchant"; - if (["amount", "value", "sum"].includes(h)) return "amount"; - if (["credit", "deposit", "income", "received", "inflow"].includes(h)) return "income_amount"; - if (["debit", "withdrawal", "payment", "expense", "outflow"].includes(h)) return "expense_amount"; - if (["type", "transactiontype", "kind"].includes(h)) return "type"; - if (["category", "categoryname"].includes(h)) return "category"; - if (["account", "accountname", "source"].includes(h)) return "account"; - if (["currency", "cur"].includes(h)) return "currency"; - if (["notes", "comment", "note"].includes(h)) return "notes"; - return undefined; -} - -function buildInitialMapping(headers: string[]): CsvMapping { - const columns: Record = {}; - headers.forEach((h) => { - const suggested = suggestField(h); - if (suggested) columns[h] = suggested; - }); - return { - columns, - default_currency: "USD", - type_aliases: { ...DEFAULT_TYPE_ALIASES }, - }; -} - -function fieldColumn(mapping: CsvMapping, field: CsvField): string | undefined { - return Object.entries(mapping.columns).find(([, f]) => f === field)?.[0]; -} - -function effectiveAccount(row: ParsedRow, mapping: CsvMapping): string | null { - if (row.account?.trim()) return row.account.trim().toLowerCase(); - if (mapping.default_account_id) return `__default:${mapping.default_account_id}`; - return null; -} - -function isDuplicatePreview(row: ParsedRow, existing: ParsedRow[], mapping: CsvMapping): boolean { - // Only flag duplicates when there is a meaningful account source. Otherwise rows with - // blank accounts look identical and all get marked as duplicates. - if (!row.date || row.amount_cents == null || !row.description) { - return false; - } - const accountA = effectiveAccount(row, mapping); - if (!accountA) return false; - return existing.some( - (r) => - r.row_index < row.row_index && - r.date === row.date && - r.amount_cents === row.amount_cents && - r.description?.trim().toLowerCase() === row.description?.trim().toLowerCase() && - effectiveAccount(r, mapping) === accountA - ); -} - -interface CsvImportDialogProps { - accounts: Account[]; - categories: Category[]; - onDone: () => void; - onCancel: () => void; -} - -type Stage = "mapping" | "preview" | "result"; - -export function CsvImportDialog({ accounts, categories, onDone, onCancel }: CsvImportDialogProps) { - const [stage, setStage] = useState("mapping"); - const [path, setPath] = useState(null); - const [mapping, setMapping] = useState({ - columns: {}, - default_currency: "USD", - type_aliases: { ...DEFAULT_TYPE_ALIASES }, - }); - const [preview, setPreview] = useState(null); - const [result, setResult] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const openFile = async () => { - setError(null); - try { - const selected = await api.openCsvFileDialog(); - if (!selected) return; - setPath(selected); - // Load preview with empty mapping to get headers. - const initialPreview = await api.previewCsvImport(selected, { - columns: {}, - type_aliases: {}, - }); - const saved = loadSavedMapping(initialPreview.headers); - const initial = saved ?? buildInitialMapping(initialPreview.headers); - setMapping(initial); - const preview = await api.previewCsvImport(selected, initial); - setPreview(preview); - } catch (e) { - setError(String(e)); - } - }; - - useEffect(() => { - openFile(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const updateColumn = (header: string, field: CsvField | "") => { - setMapping((prev) => { - const next: CsvMapping = { - ...prev, - columns: { ...prev.columns }, - }; - if (field === "") { - delete next.columns[header]; - } else { - // Remove any existing column mapped to the same field so each field is unique. - Object.keys(next.columns).forEach((h) => { - if (next.columns[h] === field) { - delete next.columns[h]; - } - }); - next.columns[header] = field; - } - return next; - }); - }; - - const updateDefaultAccount = (id: string) => { - setMapping((prev) => ({ ...prev, default_account_id: id || undefined })); - }; - - const updateDefaultCategory = (id: string) => { - setMapping((prev) => ({ ...prev, default_category_id: id || undefined })); - }; - - const updateCurrency = (currency: string) => { - setMapping((prev) => ({ ...prev, default_currency: currency.trim().toUpperCase() || "USD" })); - }; - - const updateDefaultType = (type: string) => { - setMapping((prev) => ({ - ...prev, - default_transaction_type: type === "" ? undefined : (type as "income" | "expense"), - })); - }; - - const updateTypeAlias = (from: string, to: string) => { - setMapping((prev) => ({ - ...prev, - type_aliases: { ...prev.type_aliases, [from.toLowerCase().trim()]: to }, - })); - }; - - const refreshPreview = async () => { - if (!path) return; - setLoading(true); - setError(null); - try { - const preview = await api.previewCsvImport(path, mapping); - setPreview(preview); - } catch (e) { - setError(String(e)); - } finally { - setLoading(false); - } - }; - - const handlePreview = async () => { - await refreshPreview(); - if (path) { - const currentPreview = await api.previewCsvImport(path, mapping); - setPreview(currentPreview); - saveMapping(currentPreview.headers, mapping); - } - setStage("preview"); - }; - - const handleImport = async (skipDuplicates: boolean) => { - if (!path) return; - setLoading(true); - setError(null); - try { - const options: ImportOptions = { - default_account_id: mapping.default_account_id, - default_category_id: mapping.default_category_id, - default_currency: mapping.default_currency, - skip_duplicates: skipDuplicates, - }; - const res = await api.importTransactions(path, mapping, options); - setResult(res); - setStage("result"); - } catch (e) { - setError(String(e)); - } finally { - setLoading(false); - } - }; - - const validForPreview = useMemo(() => { - const hasDate = fieldColumn(mapping, "date"); - const hasDescription = fieldColumn(mapping, "description"); - const hasAmount = - fieldColumn(mapping, "amount") || - fieldColumn(mapping, "income_amount") || - fieldColumn(mapping, "expense_amount"); - const hasType = - fieldColumn(mapping, "type") || - fieldColumn(mapping, "income_amount") || - fieldColumn(mapping, "expense_amount") || - mapping.default_transaction_type; - return Boolean(hasDate && hasDescription && hasAmount && hasType); - }, [mapping]); - - return ( -
-
e.stopPropagation()} - style={{ minWidth: 560, maxWidth: 760, width: "80vw" }} - > -
Import Transactions from CSV
- - {error && ( -
- {error} -
- )} - - {stage === "mapping" && ( - - )} - - {stage === "preview" && preview && ( - setStage("mapping")} - onImport={(skip) => handleImport(skip)} - onCancel={onCancel} - loading={loading} - /> - )} - - {stage === "result" && result && ( - setStage("preview")} /> - )} -
-
- ); -} - -function MappingStage({ - path, - preview, - mapping, - accounts, - categories, - validForPreview, - onOpenFile, - onColumnChange, - onDefaultAccountChange, - onDefaultCategoryChange, - onCurrencyChange, - onDefaultTypeChange, - onTypeAliasChange, - onPreview, - onCancel, - loading, -}: { - path: string | null; - preview: CsvPreview | null; - mapping: CsvMapping; - accounts: Account[]; - categories: Category[]; - validForPreview: boolean; - onOpenFile: () => void; - onColumnChange: (header: string, field: CsvField | "") => void; - onDefaultAccountChange: (id: string) => void; - onDefaultCategoryChange: (id: string) => void; - onCurrencyChange: (currency: string) => void; - onDefaultTypeChange: (type: string) => void; - onTypeAliasChange: (from: string, to: string) => void; - onPreview: () => void; - onCancel: () => void; - loading: boolean; -}) { - const hasTypeColumn = fieldColumn(mapping, "type") !== undefined; - const hasSeparateAmounts = - fieldColumn(mapping, "income_amount") !== undefined || - fieldColumn(mapping, "expense_amount") !== undefined; - - return ( - <> -
- {!path ? ( - - ) : ( -
- - {path} - - -
- )} -
- - {preview && ( - <> -
- - - - - - - - - {preview.headers.map((header) => ( - - - - - ))} - -
CSV ColumnChelete Field
{header} - -
-
- -
-
- - -
- -
- - -
- -
- - onCurrencyChange(e.target.value)} - maxLength={3} - /> -
- -
- - -
- - {hasTypeColumn && ( -
- -
- - -
-
- )} -
- - {!validForPreview && ( -
- Map at least Date, Description, and an Amount + Type field to continue. - {hasSeparateAmounts ? ( - <> - {" "} - Separate Income and Expense amount columns also satisfy the type requirement. - - ) : null} -
- )} - - )} - -
- - -
- - ); -} - -function AliasRow({ - label, - to, - mapping, - onChange, -}: { - label: string; - to: string; - mapping: CsvMapping; - onChange: (from: string, to: string) => void; -}) { - const from = label.toLowerCase(); - // `to` is the intended direction shown in the select default; read it as a fallback. - const value = mapping.type_aliases[from] || to; - return ( -
- {label} → - -
- ); -} - -function PreviewStage({ - preview, - mapping, - onBack, - onImport, - onCancel, - loading, -}: { - preview: CsvPreview; - mapping: CsvMapping; - onBack: () => void; - onImport: (skipDuplicates: boolean) => void; - onCancel: () => void; - loading: boolean; -}) { - const [skipDuplicates, setSkipDuplicates] = useState(true); - const previewWithDupes = useMemo(() => { - return preview.rows.map((row) => ({ - ...row, - duplicate: isDuplicatePreview(row, preview.rows, mapping), - })); - }, [preview.rows, mapping]); - - const validCount = previewWithDupes.filter((r) => r.errors.length === 0 && !r.duplicate).length; - - return ( - <> -
- - {preview.rows.length} preview rows (of {preview.total_rows} total) - - - {validCount} ready to import - -
- -
- - - - - - - - - - - - - - {previewWithDupes.map((row) => { - const typeField = fieldColumn(mapping, "type"); - const typeLabel = - row.transaction_type || - (row.amount_source === "income_column" ? "income" : undefined) || - (row.amount_source === "expense_column" ? "expense" : undefined) || - (typeField ? "—" : "—"); - const accountName = row.account || - (mapping.default_account_id ? "(default)" : "—"); - const categoryName = row.category || - (mapping.default_category_id ? "(default)" : "—"); - const hasError = row.errors.length > 0; - const isDup = row.duplicate; - - return ( - - - - - - - - - - ); - })} - -
DateDescriptionTypeAmountAccountCategoryStatus
{row.date ? formatDate(row.date) : "—"}{row.description || "—"}{typeLabel} - {row.amount_cents !== undefined ? formatMoney(row.amount_cents) : "—"} - {accountName}{categoryName} - {hasError ? ( - - {row.errors.join("; ")} - - ) : isDup ? ( - Duplicate - ) : ( - OK - )} -
-
- -
- setSkipDuplicates(e.target.checked)} - /> - -
- -
- - - -
- - ); -} - -function ResultStage({ - result, - onDone, - onBack, -}: { - result: ImportResult; - onDone: () => void; - onBack: () => void; -}) { - return ( - <> -
-
-
- {result.imported} -
-
- Imported -
-
-
-
- {result.skipped} -
-
- Skipped -
-
-
-
0 ? "var(--chelete-danger)" : "var(--chelete-fg-muted)", - }} - > - {result.errors.length} -
-
- Errors -
-
-
- - {result.errors.length > 0 && ( -
- {result.errors.map((err, i) => ( -
- {err} -
- ))} -
- )} - -
- - -
- - ); -} diff --git a/src/features/transactions/TransactionsScreen.tsx b/src/features/transactions/TransactionsScreen.tsx deleted file mode 100644 index c2d90ac..0000000 --- a/src/features/transactions/TransactionsScreen.tsx +++ /dev/null @@ -1,936 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import { formatMoney, formatDate } from "../../services/format"; -import type { Transaction, Account, Category, ExportData } from "../../types"; -import { CsvImportDialog } from "./CsvImportDialog"; -import { - DateFilter, - getDateRange, - isDateInRange, - type DateFilterValue, -} from "../../components/DateFilter"; -import { Pagination, paginate } from "../../components/Pagination"; -import { - ShoppingCart, - Car, - Briefcase, - Film, - ShoppingBag, - Zap, - Heart, - BookOpen, - Coffee, - Plane, - Home, - Utensils, - Wifi, - Smartphone, - Music, - Gamepad2, - Gift, - Stethoscope, - GraduationCap, - Building2, - Trash2, - Pencil, - Eye, - Upload, - Download, - type LucideIcon, -} from "lucide-react"; - -const CATEGORY_ICONS: { name: string; icon: LucideIcon }[] = [ - { name: "utensils", icon: Utensils }, - { name: "car", icon: Car }, - { name: "briefcase", icon: Briefcase }, - { name: "film", icon: Film }, - { name: "shopping-bag", icon: ShoppingBag }, - { name: "zap", icon: Zap }, - { name: "heart", icon: Heart }, - { name: "book-open", icon: BookOpen }, - { name: "coffee", icon: Coffee }, - { name: "plane", icon: Plane }, - { name: "home", icon: Home }, - { name: "shopping-cart", icon: ShoppingCart }, - { name: "wifi", icon: Wifi }, - { name: "smartphone", icon: Smartphone }, - { name: "music", icon: Music }, - { name: "gamepad-2", icon: Gamepad2 }, - { name: "gift", icon: Gift }, - { name: "stethoscope", icon: Stethoscope }, - { name: "graduation-cap", icon: GraduationCap }, - { name: "building-2", icon: Building2 }, -]; - -function CategoryIcon({ - name, - size = 14, - style, -}: { - name: string; - size?: number; - style?: React.CSSProperties; -}) { - const iconEntry = CATEGORY_ICONS.find((i) => i.name === name); - if (iconEntry) { - const Icon = iconEntry.icon; - return ; - } - return null; -} - -export function TransactionsScreen() { - const [transactions, setTransactions] = useState([]); - const [accounts, setAccounts] = useState([]); - const [categories, setCategories] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [showImport, setShowImport] = useState(false); - const [dateFilter, setDateFilter] = useState("all"); - const [customStart, setCustomStart] = useState(""); - const [customEnd, setCustomEnd] = useState(""); - const [transactionPage, setTransactionPage] = useState(1); - const [transactionsPerPage, setTransactionsPerPage] = useState(25); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [exportData, setExportData] = useState(null); - const [confirmDelete, setConfirmDelete] = useState<{ - ids: string[]; - count: number; - } | null>(null); - const [editingTransaction, setEditingTransaction] = useState(null); - const [viewingTransaction, setViewingTransaction] = useState(null); - const [showExport, setShowExport] = useState(false); - const PER_PAGE_OPTIONS = [10, 20, 25, 30, 40, 60, 80, 100]; - - const load = () => { - api.getTransactions().then(setTransactions); - api.getAccounts().then(setAccounts); - api.getCategories().then(setCategories); - api.exportData().then(setExportData); - }; - - useEffect(() => { - load(); - }, []); - - useEffect(() => { - const onOpenImport = () => setShowImport(true); - window.addEventListener("chelete-open-import", onOpenImport); - return () => window.removeEventListener("chelete-open-import", onOpenImport); - }, []); - - const accountMap = Object.fromEntries(accounts.map((a) => [a.id, a])); - const categoryMap = Object.fromEntries(categories.map((c) => [c.id, c])); - - const { start, end } = getDateRange(dateFilter, customStart, customEnd); - - const filteredTransactions = useMemo(() => { - return transactions.filter((t) => isDateInRange(t.transaction_date, start, end)); - }, [transactions, start, end]); - - useEffect(() => { - setTransactionPage(1); - }, [dateFilter, customStart, customEnd, transactionsPerPage]); - - const paginatedTransactions = useMemo( - () => paginate(filteredTransactions, transactionPage, transactionsPerPage), - [filteredTransactions, transactionPage, transactionsPerPage] - ); - - const selectedOnPage = useMemo(() => { - if (paginatedTransactions.length === 0) return false; - return paginatedTransactions.every((t) => selectedIds.has(t.id)); - }, [paginatedTransactions, selectedIds]); - - const someSelectedOnPage = useMemo(() => { - return paginatedTransactions.some((t) => selectedIds.has(t.id)); - }, [paginatedTransactions, selectedIds]); - - const toggleSelection = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }; - - const togglePageSelection = () => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (selectedOnPage) { - paginatedTransactions.forEach((t) => next.delete(t.id)); - } else { - paginatedTransactions.forEach((t) => next.add(t.id)); - } - return next; - }); - }; - - const handleDelete = async (ids: string[]) => { - await api.deleteTransactions(ids); - setSelectedIds((prev) => { - const next = new Set(prev); - ids.forEach((id) => next.delete(id)); - return next; - }); - load(); - }; - - return ( -
-
-
Transactions
-
- {selectedIds.size > 0 ? ( - - ) : ( - <> - - - - - )} -
-
- - - - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {editingTransaction && ( - { - setEditingTransaction(null); - load(); - }} - onCancel={() => setEditingTransaction(null)} - /> - )} - - {viewingTransaction && ( - setViewingTransaction(null)} - /> - )} - - {showExport && exportData && ( - setShowExport(false)} - onCancel={() => setShowExport(false)} - /> - )} - - {showImport && ( - { - setShowImport(false); - load(); - }} - onCancel={() => setShowImport(false)} - /> - )} - - {filteredTransactions.length === 0 ? ( -
- {transactions.length === 0 - ? "No transactions yet. Press " - : "No transactions match the selected filters."} - {transactions.length === 0 && ( - <> - - a - {" "} - to add one. - - )} -
- ) : ( - <> -
- - - - - - - - - - - - - - - {paginatedTransactions.map((t) => { - const cat = t.category_id ? categoryMap[t.category_id] : null; - return ( - - - - - - - - - - - ); - })} - -
- { - if (el) el.indeterminate = someSelectedOnPage && !selectedOnPage; - }} - onChange={togglePageSelection} - /> - DateDescriptionTypeCategoryAccountAmount
- toggleSelection(t.id)} - /> - - {formatDate(t.transaction_date)} - {t.description} - - {t.transaction_type} - - - {cat ? ( - - {cat.icon && ( - - )} - {cat.name} - - ) : ( - - — - - )} - {accountMap[t.account_id]?.name || "—"} - {formatMoney( - t.transaction_type === "expense" ? -t.amount : t.amount - )} - - - - -
-
- -
-
- Show - - per page -
- -
- - )} - - {confirmDelete && ( - { - await handleDelete(confirmDelete.ids); - setConfirmDelete(null); - }} - onCancel={() => setConfirmDelete(null)} - /> - )} -
- ); -} - -function ExportDialog({ - onDone, - onCancel, - data, -}: { - onDone: () => void; - onCancel: () => void; - data: ExportData; -}) { - const [format, setFormat] = useState<"json" | "csv">("csv"); - - const handleExport = async () => { - if (format === "json") { - const contents = JSON.stringify(data, null, 2); - const path = await api.saveFileDialog("chelete-export.json", "json"); - if (path) await api.writeExportFile(path, contents); - } else { - const contents = transactionsToCsv(data.transactions, data.accounts, data.categories); - const path = await api.saveFileDialog("chelete-transactions.csv", "csv"); - if (path) await api.writeExportFile(path, contents); - } - onDone(); - }; - - return ( -
-
e.stopPropagation()}> -
Export Data
-
{ - e.preventDefault(); - handleExport(); - }} - > -
- -
- - -
-
- -
- - -
-
-
-
- ); -} - -function escapeCsvField(value: unknown): string { - const str = value == null ? "" : String(value); - if (/[",\n\r]/.test(str)) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; -} - -function transactionsToCsv( - transactions: Transaction[], - accounts: Account[], - categories: Category[] -): string { - const accountMap = Object.fromEntries(accounts.map((a) => [a.id, a.name])); - const categoryMap = Object.fromEntries(categories.map((c) => [c.id, c.name])); - - const headers = [ - "id", - "transaction_date", - "description", - "merchant", - "transaction_type", - "amount", - "currency", - "account_name", - "category_name", - "notes", - "created_at", - "updated_at", - ]; - const rows = transactions.map((t) => [ - t.id, - t.transaction_date, - t.description, - t.merchant ?? "", - t.transaction_type, - (t.amount / 100).toFixed(2), - t.currency, - accountMap[t.account_id] ?? t.account_id, - t.category_id ? categoryMap[t.category_id] ?? t.category_id : "", - t.notes ?? "", - t.created_at, - t.updated_at, - ]); - return [headers, ...rows].map((row) => row.map(escapeCsvField).join(",")).join("\n"); -} - -function ViewTransactionDialog({ - transaction, - account, - category, - onClose, -}: { - transaction: Transaction; - account?: Account; - category?: Category | null; - onClose: () => void; -}) { - return ( -
-
e.stopPropagation()}> -
Transaction Details
-
- Date - {formatDate(transaction.transaction_date)} - - Description - {transaction.description} - - {transaction.merchant && ( - <> - Merchant - {transaction.merchant} - - )} - - Type - - - {transaction.transaction_type} - - - - Amount - - {formatMoney( - transaction.transaction_type === "expense" ? -transaction.amount : transaction.amount - )} - - - Account - {account?.name || "—"} - - Category - - {category ? ( - - {category.icon && ( - - )} - {category.name} - - ) : ( - "—" - )} - - - {transaction.notes && ( - <> - Notes - {transaction.notes} - - )} -
-
- -
-
-
- ); -} - -function DeleteConfirmDialog({ - count, - onConfirm, - onCancel, -}: { - count: number; - onConfirm: () => void; - onCancel: () => void; -}) { - return ( -
-
e.stopPropagation()}> -
Delete {count} transaction{count === 1 ? "" : "s"}?
-

- This will permanently remove the selected transaction{count === 1 ? "" : "s"} and reverse - any account balance changes. This action cannot be undone. -

-
- - -
-
-
- ); -} - -function AddTransactionForm({ - accounts, - categories, - onDone, - onCancel, -}: { - accounts: Account[]; - categories: Category[]; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.createTransaction(values); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function EditTransactionForm({ - accounts, - categories, - transaction, - onDone, - onCancel, -}: { - accounts: Account[]; - categories: Category[]; - transaction: Transaction; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.updateTransaction({ - id: transaction.id, - account_id: values.account_id, - category_id: values.category_id || undefined, - transaction_type: values.transaction_type, - amount: values.amount, - currency: "USD", - description: values.description, - transaction_date: values.transaction_date, - }); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function TransactionForm({ - title, - accounts, - categories, - initialType = "expense", - initialAmount = "", - initialDescription = "", - initialAccountId, - initialCategoryId = "", - initialDate = new Date().toISOString().split("T")[0], - onSubmit, - onCancel, -}: { - title: string; - accounts: Account[]; - categories: Category[]; - initialType?: string; - initialAmount?: string; - initialDescription?: string; - initialAccountId?: string; - initialCategoryId?: string; - initialDate?: string; - onSubmit: (values: { - account_id: string; - category_id?: string; - transaction_type: string; - amount: number; - currency: string; - description: string; - transaction_date: string; - }) => void | Promise; - onCancel: () => void; -}) { - const [type, setType] = useState(initialType); - const [amount, setAmount] = useState(initialAmount); - const [description, setDescription] = useState(initialDescription); - const [accountId, setAccountId] = useState(initialAccountId || accounts[0]?.id || ""); - const [categoryId, setCategoryId] = useState(initialCategoryId); - const [date, setDate] = useState(initialDate); - - const expenseCategories = categories.filter( - (c) => c.category_type === "expense" - ); - const incomeCategories = categories.filter( - (c) => c.category_type === "income" - ); - const filteredCategories = - type === "expense" ? expenseCategories : incomeCategories; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const cents = Math.round(parseFloat(amount) * 100); - if (isNaN(cents) || cents <= 0) return; - - await onSubmit({ - account_id: accountId, - category_id: categoryId || undefined, - transaction_type: type, - amount: cents, - currency: "USD", - description, - transaction_date: date, - }); - }; - - return ( -
-
e.stopPropagation()}> -
{title}
-
-
- - -
- -
- - setAmount(e.target.value)} - autoFocus - /> -
- -
- - setDescription(e.target.value)} - /> -
- -
-
- - -
- -
- - -
-
- -
- - setDate(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..0eaa332 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,156 @@ +//! Money and label formatting. Amounts are stored as integer cents. + +fn group_thousands(n: u64) -> String { + let digits = n.to_string(); + let mut out = String::new(); + for (i, c) in digits.chars().enumerate() { + if i > 0 && (digits.len() - i) % 3 == 0 { + out.push(','); + } + out.push(c); + } + out +} + +fn dollars(abs_cents: u64) -> String { + format!("{}.{:02}", group_thousands(abs_cents / 100), abs_cents % 100) +} + +/// Signed amount, e.g. `+$5,250.00` / `-$10.00`. +pub fn format_money(cents: i64) -> String { + let sign = if cents >= 0 { '+' } else { '-' }; + format!("{sign}${}", dollars(cents.unsigned_abs())) +} + +/// Unsigned compact amount, e.g. `$123.45` / `$1.5k`. +pub fn format_money_short(cents: i64) -> String { + let abs = cents.unsigned_abs(); + if abs >= 100_000 { + format!("${:.1}k", abs as f64 / 100_000.0) + } else { + format!("${}", dollars(abs)) + } +} + +/// Balance without a plus sign; negatives keep the old `$-1,200.00` shape. +pub fn format_balance(cents: i64) -> String { + let minus = if cents < 0 { "-" } else { "" }; + format!("${minus}{}", dollars(cents.unsigned_abs())) +} + +/// Parse a user-typed dollar amount ("12.5", "-3", "1,250.00") into cents. +/// Returns `None` for empty or malformed input, or more than two decimals. +pub fn parse_cents(input: &str) -> Option { + let cleaned: String = input.trim().chars().filter(|c| *c != ',' && *c != '$').collect(); + if cleaned.is_empty() { + return None; + } + let (negative, digits) = match cleaned.strip_prefix('-') { + Some(rest) => (true, rest), + None => (false, cleaned.strip_prefix('+').unwrap_or(&cleaned)), + }; + let (whole, frac) = match digits.split_once('.') { + Some((w, f)) => (w, f), + None => (digits, ""), + }; + if (whole.is_empty() && frac.is_empty()) + || frac.len() > 2 + || !whole.chars().all(|c| c.is_ascii_digit()) + || !frac.chars().all(|c| c.is_ascii_digit()) + { + return None; + } + let whole: i64 = if whole.is_empty() { 0 } else { whole.parse().ok()? }; + let frac: i64 = match frac.len() { + 0 => 0, + 1 => frac.parse::().ok()? * 10, + _ => frac.parse().ok()?, + }; + let cents = whole.checked_mul(100)?.checked_add(frac)?; + Some(if negative { -cents } else { cents }) +} + +/// Lenient dollars-to-cents for free-form numeric fields: any finite number, +/// rounded to the nearest cent ("1e3", "12.345" and "-4" all parse). +pub fn parse_dollars_lenient(input: &str) -> Option { + let value: f64 = input.trim().parse().ok()?; + value.is_finite().then(|| (value * 100.0).round() as i64) +} + +pub fn account_type_label(kind: &str) -> String { + match kind { + "cash" => "Cash", + "bank" => "Bank", + "savings" => "Savings", + "credit_card" => "Credit Card", + "mobile_money" => "Mobile Money", + "investment" => "Investment", + "other" => "Other", + other => other, + } + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn money_is_signed() { + assert_eq!(format_money(1000), "+$10.00"); + assert_eq!(format_money(525000), "+$5,250.00"); + assert_eq!(format_money(-120000), "-$1,200.00"); + assert_eq!(format_money(0), "+$0.00"); + assert_eq!(format_money(50), "+$0.50"); + assert_eq!(format_money(-25), "-$0.25"); + } + + #[test] + fn money_short_uses_k_suffix() { + assert_eq!(format_money_short(500), "$5.00"); + assert_eq!(format_money_short(12345), "$123.45"); + assert_eq!(format_money_short(100000), "$1.0k"); + assert_eq!(format_money_short(5250000), "$52.5k"); + } + + #[test] + fn balance_has_no_plus() { + assert_eq!(format_balance(525000), "$5,250.00"); + assert_eq!(format_balance(-120000), "$-1,200.00"); + assert_eq!(format_balance(0), "$0.00"); + } + + #[test] + fn parses_typed_amounts() { + assert_eq!(parse_cents("12.50"), Some(1250)); + assert_eq!(parse_cents("12.5"), Some(1250)); + assert_eq!(parse_cents("12"), Some(1200)); + assert_eq!(parse_cents(".75"), Some(75)); + assert_eq!(parse_cents("-3"), Some(-300)); + assert_eq!(parse_cents("$1,250.00"), Some(125000)); + assert_eq!(parse_cents(" 0 "), Some(0)); + assert_eq!(parse_cents(""), None); + assert_eq!(parse_cents("abc"), None); + assert_eq!(parse_cents("1.234"), None); + assert_eq!(parse_cents("1.2.3"), None); + assert_eq!(parse_cents("-"), None); + } + + #[test] + fn lenient_parse_rounds_and_rejects_junk() { + assert_eq!(parse_dollars_lenient("12.345"), Some(1235)); + assert_eq!(parse_dollars_lenient(" 1200 "), Some(120000)); + assert_eq!(parse_dollars_lenient("-4"), Some(-400)); + assert_eq!(parse_dollars_lenient("1e3"), Some(100000)); + assert_eq!(parse_dollars_lenient(""), None); + assert_eq!(parse_dollars_lenient("abc"), None); + assert_eq!(parse_dollars_lenient("inf"), None); + assert_eq!(parse_dollars_lenient("NaN"), None); + } + + #[test] + fn account_labels() { + assert_eq!(account_type_label("credit_card"), "Credit Card"); + assert_eq!(account_type_label("weird"), "weird"); + } +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts deleted file mode 100644 index d152fd8..0000000 --- a/src/hooks/useTheme.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useState } from "react"; -import { api } from "../services/api"; -import type { OmarchyTheme } from "../types"; - -export function useTheme() { - const [theme, setTheme] = useState(null); - - useEffect(() => { - let lastThemeJson = ""; - - const poll = () => { - api.getOmarchyTheme().then((t) => { - const json = JSON.stringify(t); - if (json !== lastThemeJson) { - lastThemeJson = json; - setTheme(t); - applyTheme(t); - } - }); - }; - - poll(); - const interval = setInterval(poll, 2000); - return () => clearInterval(interval); - }, []); - - return theme; -} - -function applyTheme(theme: OmarchyTheme) { - const root = document.documentElement; - const c = theme.colors; - - root.style.setProperty("--chelete-bg", c.background || "#1a1b26"); - root.style.setProperty("--chelete-bg-secondary", darken(c.background, 0.1)); - root.style.setProperty("--chelete-surface", c.surface || darken(c.background, -0.08)); - root.style.setProperty("--chelete-surface-hover", c.surface_hover || darken(c.background, -0.15)); - root.style.setProperty("--chelete-fg", theme.foreground || "#c0caf5"); - root.style.setProperty("--chelete-fg-muted", c.foreground_muted || "#737aa2"); - root.style.setProperty("--chelete-fg-subtle", c.foreground_subtle || "#565f89"); - root.style.setProperty("--chelete-accent", theme.accent || "#7aa2f7"); - root.style.setProperty("--chelete-success", c.success || c.green || "#9ece6a"); - root.style.setProperty("--chelete-danger", c.danger || c.red || "#f7768e"); - root.style.setProperty("--chelete-warning", c.warning || c.yellow || "#e0af68"); - root.style.setProperty("--chelete-info", c.info || c.cyan || "#7dcfff"); - - const border = c.border || (isLight(c.background) ? darken(c.background, -0.15) : darken(c.background, 0.2)); - root.style.setProperty("--chelete-border", border); -} - -function darken(hex: string, amount: number): string { - const h = hex.replace("#", ""); - const r = parseInt(h.substring(0, 2), 16); - const g = parseInt(h.substring(2, 4), 16); - const b = parseInt(h.substring(4, 6), 16); - - const factor = amount > 0 ? 1 + amount : 1 + amount; - const nr = Math.min(255, Math.max(0, Math.round(r * factor))); - const ng = Math.min(255, Math.max(0, Math.round(g * factor))); - const nb = Math.min(255, Math.max(0, Math.round(b * factor))); - - return `#${nr.toString(16).padStart(2, "0")}${ng.toString(16).padStart(2, "0")}${nb.toString(16).padStart(2, "0")}`; -} - -function isLight(hex: string): boolean { - const h = hex.replace("#", ""); - const r = parseInt(h.substring(0, 2), 16); - const g = parseInt(h.substring(2, 4), 16); - const b = parseInt(h.substring(4, 6), 16); - return (r * 299 + g * 587 + b * 114) / 1000 > 128; -} diff --git a/src-tauri/src/import/mapping.rs b/src/import/mapping.rs similarity index 100% rename from src-tauri/src/import/mapping.rs rename to src/import/mapping.rs diff --git a/src-tauri/src/import/mod.rs b/src/import/mod.rs similarity index 70% rename from src-tauri/src/import/mod.rs rename to src/import/mod.rs index bb3826d..6e69147 100644 --- a/src-tauri/src/import/mod.rs +++ b/src/import/mod.rs @@ -1,5 +1,6 @@ pub mod mapping; pub mod parser; +pub mod session; -pub use mapping::CsvMapping; +pub use mapping::{CsvField, CsvMapping}; pub use parser::{parse_csv_preview, parse_csv_rows, AmountSource, CsvPreview, ImportResult, ParsedRow}; diff --git a/src-tauri/src/import/parser.rs b/src/import/parser.rs similarity index 100% rename from src-tauri/src/import/parser.rs rename to src/import/parser.rs diff --git a/src/import/session.rs b/src/import/session.rs new file mode 100644 index 0000000..e6ab9b2 --- /dev/null +++ b/src/import/session.rs @@ -0,0 +1,291 @@ +//! Rules the CSV import dialog applies on top of the parser: column +//! assignment, readiness, duplicate flagging and remembered mappings. +use super::{AmountSource, CsvField, CsvMapping, ParsedRow}; +use std::collections::HashMap; +use std::path::Path; + +/// Map `header` to `field`, or unmap it. A field can only come from one +/// column, so any other column using it is released. +pub fn set_column(mapping: &mut CsvMapping, header: &str, field: Option) { + match field { + None => { + mapping.columns.remove(header); + } + Some(field) => { + mapping.columns.retain(|_, f| *f != field); + mapping.columns.insert(header.to_string(), field); + } + } +} + +/// Date, description, an amount, and some way to tell income from expense. +pub fn is_valid_for_preview(mapping: &CsvMapping) -> bool { + let has = |f| mapping.field_column(f).is_some(); + let has_amount = has(CsvField::Amount) || has(CsvField::IncomeAmount) || has(CsvField::ExpenseAmount); + let has_type = has(CsvField::Type) + || has(CsvField::IncomeAmount) + || has(CsvField::ExpenseAmount) + || mapping.default_transaction_type.is_some(); + has(CsvField::Date) && has(CsvField::Description) && has_amount && has_type +} + +fn effective_account(row: &ParsedRow, mapping: &CsvMapping) -> Option { + if let Some(account) = row.account.as_deref().map(str::trim).filter(|a| !a.is_empty()) { + return Some(account.to_lowercase()); + } + mapping.default_account_id.as_ref().map(|id| format!("__default:{id}")) +} + +/// A row repeats an earlier row: same date, amount, description and account. +/// Rows with no account source are never flagged, or every blank-account row +/// would look identical. +pub fn is_duplicate_preview(row: &ParsedRow, all: &[ParsedRow], mapping: &CsvMapping) -> bool { + let (Some(date), Some(amount), Some(description)) = (&row.date, row.amount_cents, &row.description) else { + return false; + }; + let Some(account) = effective_account(row, mapping) else { + return false; + }; + let description = description.trim().to_lowercase(); + all.iter().any(|other| { + other.row_index < row.row_index + && other.date.as_ref() == Some(date) + && other.amount_cents == Some(amount) + && other.description.as_deref().map(|d| d.trim().to_lowercase()).as_deref() == Some(description.as_str()) + && effective_account(other, mapping).as_deref() == Some(account.as_str()) + }) +} + +pub fn flag_duplicates(rows: &[ParsedRow], mapping: &CsvMapping) -> Vec { + rows.iter().map(|r| is_duplicate_preview(r, rows, mapping)).collect() +} + +/// Rows that would import: no errors and not a duplicate. +pub fn valid_count(rows: &[ParsedRow], duplicates: &[bool]) -> usize { + rows.iter() + .zip(duplicates) + .filter(|(r, dup)| r.errors.is_empty() && !**dup) + .count() +} + +/// What the preview's Type column shows. Only separate income/expense columns +/// tell us the type before import. +pub fn type_label(row: &ParsedRow) -> &'static str { + match row.amount_source { + Some(AmountSource::IncomeColumn) => "income", + Some(AmountSource::ExpenseColumn) => "expense", + _ => "—", + } +} + +/// Identifies a CSV layout by its headers, ignoring case and punctuation. +pub fn mapping_key(headers: &[String]) -> String { + headers + .iter() + .map(|h| h.trim().to_lowercase().chars().filter(|c| c.is_ascii_alphanumeric()).collect::()) + .collect::>() + .join(",") +} + +/// Column mappings remembered per CSV layout. +#[derive(Debug, Default)] +pub struct SavedMappings(HashMap); + +impl SavedMappings { + pub fn load_from(path: &Path) -> Self { + let map = std::fs::read_to_string(path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + Self(map) + } + + pub fn save_to(&self, path: &Path) -> Result<(), String> { + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + let json = serde_json::to_string_pretty(&self.0).map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) + } + + pub fn get(&self, headers: &[String]) -> Option<&CsvMapping> { + self.0.get(&mapping_key(headers)) + } + + pub fn insert(&mut self, headers: &[String], mapping: CsvMapping) { + self.0.insert(mapping_key(headers), mapping); + } + + pub fn default_path() -> std::path::PathBuf { + crate::database::data_dir().join("csv-mappings.json") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::import::{parse_csv_preview, parse_csv_rows}; + + fn headers(list: &[&str]) -> Vec { + list.iter().map(|s| s.to_string()).collect() + } + + fn row(index: usize, date: &str, desc: &str, cents: i64, account: Option<&str>) -> ParsedRow { + ParsedRow { + row_index: index, + date: Some(date.into()), + description: Some(desc.into()), + merchant: None, + transaction_type: None, + amount_cents: Some(cents), + amount_source: None, + currency: None, + category: None, + account: account.map(String::from), + notes: None, + errors: vec![], + } + } + + #[test] + fn a_field_comes_from_one_column_only() { + let mut m = CsvMapping::from_headers(&headers(&["Date", "Memo", "Amount"])); + set_column(&mut m, "Memo", Some(CsvField::Amount)); + assert_eq!(m.columns.get("Memo"), Some(&CsvField::Amount)); + assert_eq!(m.columns.get("Amount"), None, "previous amount column released"); + assert_eq!(m.field_column(CsvField::Amount), Some("Memo")); + set_column(&mut m, "Memo", None); + assert_eq!(m.field_column(CsvField::Amount), None); + } + + #[test] + fn readiness_needs_date_description_amount_and_a_type_source() { + let mut m = CsvMapping::default(); + assert!(!is_valid_for_preview(&m)); + set_column(&mut m, "d", Some(CsvField::Date)); + set_column(&mut m, "p", Some(CsvField::Description)); + set_column(&mut m, "a", Some(CsvField::Amount)); + assert!(!is_valid_for_preview(&m), "amount alone does not say income or expense"); + m.default_transaction_type = Some("expense".into()); + assert!(is_valid_for_preview(&m)); + m.default_transaction_type = None; + set_column(&mut m, "t", Some(CsvField::Type)); + assert!(is_valid_for_preview(&m)); + } + + #[test] + fn separate_income_and_expense_columns_are_enough() { + let mut m = CsvMapping::default(); + set_column(&mut m, "d", Some(CsvField::Date)); + set_column(&mut m, "p", Some(CsvField::Description)); + set_column(&mut m, "in", Some(CsvField::IncomeAmount)); + assert!(is_valid_for_preview(&m)); + } + + #[test] + fn duplicates_match_on_date_amount_description_and_account() { + let m = CsvMapping::default(); + let rows = vec![ + row(1, "2026-03-01", "Coffee", 450, Some("Main")), + row(2, "2026-03-01", " coffee ", 450, Some("main")), + row(3, "2026-03-01", "Coffee", 451, Some("Main")), + row(4, "2026-03-02", "Coffee", 450, Some("Main")), + row(5, "2026-03-01", "Coffee", 450, Some("Other")), + ]; + assert_eq!(flag_duplicates(&rows, &m), vec![false, true, false, false, false]); + assert_eq!(valid_count(&rows, &flag_duplicates(&rows, &m)), 4); + } + + #[test] + fn rows_without_an_account_source_are_never_duplicates() { + let m = CsvMapping::default(); + let rows = vec![row(1, "2026-03-01", "A", 1, None), row(2, "2026-03-01", "A", 1, None)]; + assert_eq!(flag_duplicates(&rows, &m), vec![false, false]); + } + + #[test] + fn a_default_account_makes_blank_accounts_comparable() { + let mut m = CsvMapping::default(); + m.default_account_id = Some("acc-1".into()); + let rows = vec![row(1, "2026-03-01", "A", 1, None), row(2, "2026-03-01", "A", 1, Some(""))]; + assert_eq!(flag_duplicates(&rows, &m), vec![false, true]); + } + + #[test] + fn rows_with_errors_do_not_count_as_valid() { + let m = CsvMapping::default(); + let mut bad = row(1, "2026-03-01", "A", 1, Some("x")); + bad.errors.push("bad date".into()); + let rows = vec![bad, row(2, "2026-03-02", "B", 2, Some("x"))]; + assert_eq!(valid_count(&rows, &flag_duplicates(&rows, &m)), 1); + } + + #[test] + fn type_label_reflects_split_amount_columns() { + let mut r = row(1, "d", "x", 1, None); + assert_eq!(type_label(&r), "—"); + r.amount_source = Some(AmountSource::IncomeColumn); + assert_eq!(type_label(&r), "income"); + r.amount_source = Some(AmountSource::ExpenseColumn); + assert_eq!(type_label(&r), "expense"); + } + + #[test] + fn keys_ignore_case_and_punctuation() { + assert_eq!(mapping_key(&headers(&["Transaction Date", "Amount ($)"])), "transactiondate,amount"); + assert_eq!(mapping_key(&headers(&["transaction_date", "AMOUNT"])), "transactiondate,amount"); + } + + #[test] + fn saved_mappings_round_trip_on_disk() { + let dir = std::env::temp_dir().join(format!("chelete-mappings-{}", std::process::id())); + let path = dir.join("m.json"); + let hs = headers(&["Date", "Memo", "Amount"]); + let mut saved = SavedMappings::load_from(&path); + assert!(saved.get(&hs).is_none(), "missing file starts empty"); + let mut mapping = CsvMapping::from_headers(&hs); + mapping.default_account_id = Some("acc".into()); + saved.insert(&hs, mapping); + saved.save_to(&path).unwrap(); + + let loaded = SavedMappings::load_from(&path); + let got = loaded.get(&headers(&["date", "MEMO", "amount"])).expect("same layout, different case"); + assert_eq!(got.default_account_id.as_deref(), Some("acc")); + assert_eq!(got.columns.get("Date"), Some(&CsvField::Date)); + assert!(loaded.get(&headers(&["Other"])).is_none()); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn corrupt_saved_file_is_ignored() { + let dir = std::env::temp_dir().join(format!("chelete-mappings-bad-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("m.json"); + std::fs::write(&path, "{ not json").unwrap(); + assert!(SavedMappings::load_from(&path).get(&headers(&["a"])).is_none()); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn works_end_to_end_on_a_real_csv() { + let dir = std::env::temp_dir().join(format!("chelete-csv-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("bank.csv"); + std::fs::write( + &path, + "Date,Payee,Debit,Credit,Account\n2026-03-01,Coffee,4.50,,Main\n2026-03-01,Coffee,4.50,,Main\n2026-03-02,Salary,,2000.00,Main\n", + ) + .unwrap(); + let p = path.to_string_lossy().to_string(); + + let first = parse_csv_preview(&p, &CsvMapping::default()).unwrap(); + let mapping = CsvMapping::from_headers(&first.headers); + assert!(is_valid_for_preview(&mapping), "headers are recognised automatically"); + + let rows = parse_csv_rows(&p, &mapping).unwrap(); + assert_eq!(rows.len(), 3); + assert_eq!(flag_duplicates(&rows, &mapping), vec![false, true, false]); + assert_eq!(type_label(&rows[2]), "income"); + std::fs::remove_dir_all(&dir).ok(); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..710329d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +pub mod commands; +pub mod database; +pub mod date_filter; +pub mod export; +pub mod format; +pub mod import; +pub mod overview; +pub mod paging; +pub mod prefs; +pub mod projection; +pub mod seed; +pub mod subscriptions; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a241c7c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,48 @@ +mod ui; + +use chelete_lib::database::{self, DbState}; +use chelete_lib::prefs::Prefs; +use gpui_kit::{px, size, AppContext as _, Bounds, SharedString, TitlebarOptions, WindowBounds, WindowOptions}; +use std::sync::Arc; + +fn main() { + let db = Arc::new(DbState( + database::init_database().expect("failed to open database"), + )); + + gpui_kit::application() + .with_assets(gpui_kit::assets::AllAssets) + .run(move |cx| { + gpui_kit::init(cx); + gpui_omarchy::init(cx); + ui::install(cx); + cx.on_action(|_: &ui::Quit, cx| cx.quit()); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(1100.), px(700.)), + cx, + ))), + titlebar: Some(TitlebarOptions { + title: Some(SharedString::from("Chelete")), + ..Default::default() + }), + // Lets window rules and the desktop entry find the app (class "chelete"). + app_id: Some("chelete".into()), + window_min_size: Some(size(px(760.), px(520.))), + ..Default::default() + }; + gpui_kit::open_window(options, cx, |window, cx| { + cx.new(|cx| ui::Shell::new(db, Prefs::load(), true, window, cx)) + }) + .expect("open window"); + cx.activate(true); + }); +} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 9609ffe..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import "./styles/global.css"; - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - -); diff --git a/src/overview.rs b/src/overview.rs new file mode 100644 index 0000000..27f7c2c --- /dev/null +++ b/src/overview.rs @@ -0,0 +1,226 @@ +//! Dashboard figures for a chosen date range. +use crate::commands::{Category, Transaction}; +use crate::date_filter::is_date_in_range; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub struct CategoryTotal { + pub category_id: String, + pub name: String, + pub color: Option, + pub icon: Option, + pub value: i64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Summary { + pub total_income: i64, + pub total_expenses: i64, + pub net: i64, + /// Whole percent of income kept; zero without income. + pub savings_rate: i64, + pub income_by_category: Vec, + pub expenses_by_category: Vec, +} + +fn totals(map: HashMap, categories: &[Category]) -> Vec { + let mut list: Vec = map + .into_iter() + .map(|(category_id, value)| { + let category = categories.iter().find(|c| c.id == category_id); + CategoryTotal { + name: category.map_or_else(|| "Unknown".to_string(), |c| c.name.clone()), + color: category.and_then(|c| c.color.clone()), + icon: category.and_then(|c| c.icon.clone()), + category_id, + value, + } + }) + .collect(); + // Largest first; ties by name so the order is stable. + list.sort_by(|a, b| b.value.cmp(&a.value).then_with(|| a.name.cmp(&b.name))); + list +} + +/// Totals for transactions dated within `start..=end`. +pub fn summarize(transactions: &[Transaction], categories: &[Category], start: &str, end: &str) -> Summary { + let (mut income, mut expenses) = (0i64, 0i64); + let mut income_by: HashMap = HashMap::new(); + let mut expense_by: HashMap = HashMap::new(); + for t in transactions.iter().filter(|t| is_date_in_range(&t.transaction_date, start, end)) { + match t.transaction_type.as_str() { + "income" => { + income += t.amount; + if let Some(id) = &t.category_id { + *income_by.entry(id.clone()).or_insert(0) += t.amount; + } + } + "expense" => { + expenses += t.amount; + if let Some(id) = &t.category_id { + *expense_by.entry(id.clone()).or_insert(0) += t.amount; + } + } + _ => {} + } + } + let net = income - expenses; + Summary { + total_income: income, + total_expenses: expenses, + net, + // Round half up like the web UI (Math.round). + savings_rate: if income > 0 { (net as f64 / income as f64 * 100.0 + 0.5).floor() as i64 } else { 0 }, + income_by_category: totals(income_by, categories), + expenses_by_category: totals(expense_by, categories), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeFilter { + All, + Income, + Expense, +} + +impl TypeFilter { + pub fn from_value(value: &str) -> Self { + match value { + "income" => TypeFilter::Income, + "expense" => TypeFilter::Expense, + _ => TypeFilter::All, + } + } +} + +/// Transactions in the date range, optionally narrowed by type and account. +pub fn filter_transactions<'a>( + transactions: &'a [Transaction], + start: &str, + end: &str, + kind: TypeFilter, + account_id: Option<&str>, +) -> Vec<&'a Transaction> { + transactions + .iter() + .filter(|t| is_date_in_range(&t.transaction_date, start, end)) + .filter(|t| match kind { + TypeFilter::All => true, + TypeFilter::Income => t.transaction_type == "income", + TypeFilter::Expense => t.transaction_type == "expense", + }) + .filter(|t| account_id.is_none_or(|id| t.account_id == id)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tx(id: &str, kind: &str, amount: i64, date: &str, cat: Option<&str>, account: &str) -> Transaction { + Transaction { + id: id.into(), + account_id: account.into(), + category_id: cat.map(String::from), + transaction_type: kind.into(), + amount, + currency: "USD".into(), + description: id.into(), + merchant: None, + notes: None, + transaction_date: date.into(), + created_at: String::new(), + updated_at: String::new(), + } + } + + fn cat(id: &str, name: &str, color: Option<&str>) -> Category { + Category { + id: id.into(), + name: name.into(), + parent_id: None, + category_type: "expense".into(), + icon: Some("tag".into()), + color: color.map(String::from), + sort_order: 0, + created_at: String::new(), + updated_at: String::new(), + } + } + + fn sample() -> Vec { + vec![ + tx("pay", "income", 200000, "2026-03-01", Some("job"), "a"), + tx("rent", "expense", 80000, "2026-03-02", Some("home"), "a"), + tx("food1", "expense", 10000, "2026-03-03", Some("food"), "b"), + tx("food2", "expense", 5000, "2026-03-04", Some("food"), "a"), + tx("old", "expense", 99999, "2025-01-01", Some("food"), "a"), + tx("loose", "expense", 700, "2026-03-05", None, "a"), + ] + } + + fn cats() -> Vec { + vec![cat("job", "Salary", Some("#9ece6a")), cat("home", "Rent", None), cat("food", "Food", None)] + } + + #[test] + fn totals_only_include_the_date_range() { + let s = summarize(&sample(), &cats(), "2026-03-01", "2026-03-31"); + assert_eq!(s.total_income, 200000); + assert_eq!(s.total_expenses, 80000 + 10000 + 5000 + 700); + assert_eq!(s.net, 200000 - 95700); + } + + #[test] + fn categories_are_grouped_and_sorted_largest_first() { + let s = summarize(&sample(), &cats(), "2026-03-01", "2026-03-31"); + let names: Vec<_> = s.expenses_by_category.iter().map(|c| (c.name.as_str(), c.value)).collect(); + assert_eq!(names, vec![("Rent", 80000), ("Food", 15000)], "uncategorised spending is not charted"); + assert_eq!(s.income_by_category.len(), 1); + assert_eq!(s.income_by_category[0].color.as_deref(), Some("#9ece6a")); + } + + #[test] + fn unknown_categories_get_a_placeholder_name() { + let t = vec![tx("x", "expense", 100, "2026-03-01", Some("deleted"), "a")]; + let s = summarize(&t, &[], "2026-03-01", "2026-03-31"); + assert_eq!(s.expenses_by_category[0].name, "Unknown"); + } + + #[test] + fn savings_rate_rounds_half_up_and_handles_no_income() { + let s = summarize(&sample(), &cats(), "2026-03-01", "2026-03-31"); + assert_eq!(s.savings_rate, 52, "(200000-95700)/200000 = 52.15%"); + let none = summarize(&sample(), &cats(), "2026-04-01", "2026-04-30"); + assert_eq!((none.total_income, none.savings_rate), (0, 0)); + let half = vec![tx("i", "income", 200, "2026-03-01", None, "a"), tx("e", "expense", 101, "2026-03-01", None, "a")]; + assert_eq!(summarize(&half, &[], "2026-03-01", "2026-03-31").savings_rate, 50, "49.5 rounds up"); + } + + #[test] + fn overspending_gives_a_negative_rate() { + let t = vec![tx("i", "income", 100, "2026-03-01", None, "a"), tx("e", "expense", 250, "2026-03-01", None, "a")]; + let s = summarize(&t, &[], "2026-03-01", "2026-03-31"); + assert_eq!((s.net, s.savings_rate), (-150, -150)); + } + + #[test] + fn filters_combine_range_type_and_account() { + let all = sample(); + let ids = |v: Vec<&Transaction>| v.into_iter().map(|t| t.id.clone()).collect::>(); + let (s, e) = ("2026-03-01", "2026-03-31"); + assert_eq!(ids(filter_transactions(&all, s, e, TypeFilter::All, None)).len(), 5); + assert_eq!(ids(filter_transactions(&all, s, e, TypeFilter::Income, None)), vec!["pay"]); + assert_eq!(ids(filter_transactions(&all, s, e, TypeFilter::Expense, Some("b"))), vec!["food1"]); + assert!(filter_transactions(&all, s, e, TypeFilter::Income, Some("b")).is_empty()); + assert_eq!(ids(filter_transactions(&all, "0000-01-01", "9999-12-31", TypeFilter::All, None)).len(), 6); + } + + #[test] + fn type_filter_parses_values() { + assert_eq!(TypeFilter::from_value("income"), TypeFilter::Income); + assert_eq!(TypeFilter::from_value("expense"), TypeFilter::Expense); + assert_eq!(TypeFilter::from_value("all"), TypeFilter::All); + assert_eq!(TypeFilter::from_value("???"), TypeFilter::All); + } +} diff --git a/src/paging.rs b/src/paging.rs new file mode 100644 index 0000000..abaed91 --- /dev/null +++ b/src/paging.rs @@ -0,0 +1,93 @@ +//! Page arithmetic shared by the list screens. + +pub const PER_PAGE_OPTIONS: [usize; 8] = [10, 20, 25, 30, 40, 60, 80, 100]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pager { + pub page: usize, + pub per_page: usize, + pub total: usize, +} + +impl Pager { + pub fn new(page: usize, per_page: usize, total: usize) -> Self { + let per_page = per_page.max(1); + let pages = total.div_ceil(per_page).max(1); + Self { page: page.clamp(1, pages), per_page, total } + } + + pub fn total_pages(&self) -> usize { + self.total.div_ceil(self.per_page).max(1) + } + + pub fn has_prev(&self) -> bool { + self.page > 1 + } + + pub fn has_next(&self) -> bool { + self.page < self.total_pages() + } + + /// Index range of the current page within the full list. + pub fn range(&self) -> std::ops::Range { + let start = ((self.page - 1) * self.per_page).min(self.total); + start..(start + self.per_page).min(self.total) + } + + /// e.g. `1–10 of 25`. + pub fn label(&self) -> String { + if self.total == 0 { + return "0–0 of 0".into(); + } + let r = self.range(); + format!("{}–{} of {}", r.start + 1, r.end, self.total) + } +} + +pub fn page_slice<'a, T>(items: &'a [T], pager: &Pager) -> &'a [T] { + &items[pager.range()] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn labels_and_ranges() { + let p = Pager::new(1, 10, 25); + assert_eq!(p.label(), "1–10 of 25"); + assert_eq!(p.range(), 0..10); + assert!(!p.has_prev() && p.has_next()); + let last = Pager::new(3, 10, 25); + assert_eq!(last.label(), "21–25 of 25"); + assert_eq!(last.range(), 20..25); + assert!(last.has_prev() && !last.has_next()); + } + + #[test] + fn page_is_clamped_when_the_list_shrinks() { + let p = Pager::new(9, 10, 12); + assert_eq!(p.page, 2); + assert_eq!(Pager::new(0, 10, 12).page, 1); + } + + #[test] + fn empty_list() { + let p = Pager::new(1, 10, 0); + assert_eq!(p.label(), "0–0 of 0"); + assert_eq!(p.total_pages(), 1); + assert!(!p.has_next() && !p.has_prev()); + assert!(page_slice::(&[], &p).is_empty()); + } + + #[test] + fn slices_the_current_page() { + let items: Vec = (0..25).collect(); + assert_eq!(page_slice(&items, &Pager::new(3, 10, 25)), &[20, 21, 22, 23, 24]); + } + + #[test] + fn zero_per_page_does_not_panic() { + assert_eq!(Pager::new(1, 0, 5).per_page, 1); + } +} diff --git a/src/prefs.rs b/src/prefs.rs new file mode 100644 index 0000000..5930a61 --- /dev/null +++ b/src/prefs.rs @@ -0,0 +1,121 @@ +//! Small UI preferences persisted next to the database. +use serde::{Deserialize, Serialize}; + +/// Text size as a percentage of the toolkit's 16px base. +pub const TEXT_SCALES: [u16; 9] = [70, 80, 90, 100, 110, 125, 150, 175, 200]; +pub const DEFAULT_TEXT_SCALE: u16 = 110; + +/// The next larger (`direction > 0`) or smaller (`< 0`) step, staying put at +/// either end. A value between steps moves to the nearest step in that direction. +pub fn step_text_scale(current: u16, direction: i8) -> u16 { + let steps = TEXT_SCALES; + if direction > 0 { + steps.into_iter().find(|s| *s > current).unwrap_or(steps[steps.len() - 1]) + } else if direction < 0 { + steps.into_iter().rev().find(|s| *s < current).unwrap_or(steps[0]) + } else { + current + } +} + +pub fn clamp_text_scale(percent: u16) -> u16 { + percent.clamp(TEXT_SCALES[0], TEXT_SCALES[TEXT_SCALES.len() - 1]) +} + +/// Pixels for one rem at the given text size. +pub fn rem_size_px(percent: u16) -> f32 { + 16.0 * f32::from(clamp_text_scale(percent)) / 100.0 +} + +fn default_text_scale() -> u16 { + DEFAULT_TEXT_SCALE +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Prefs { + #[serde(default)] + pub sidebar_collapsed: bool, + #[serde(default = "default_text_scale")] + pub text_scale: u16, +} + +impl Default for Prefs { + fn default() -> Self { + Self { sidebar_collapsed: false, text_scale: DEFAULT_TEXT_SCALE } + } +} + +fn path() -> std::path::PathBuf { + crate::database::data_dir().join("ui.json") +} + +impl Prefs { + pub fn load() -> Self { + std::fs::read_to_string(path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() + } + + pub fn save(&self) { + if let Ok(json) = serde_json::to_string_pretty(self) { + let _ = std::fs::create_dir_all(crate::database::data_dir()); + let _ = std::fs::write(path(), json); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_fields_fall_back_to_defaults() { + let p: Prefs = serde_json::from_str("{}").unwrap(); + assert!(!p.sidebar_collapsed); + assert_eq!(p.text_scale, DEFAULT_TEXT_SCALE); + assert_eq!(p, Prefs::default()); + } + + #[test] + fn files_written_before_text_size_existed_still_load() { + let p: Prefs = serde_json::from_str(r#"{"sidebar_collapsed": true}"#).unwrap(); + assert!(p.sidebar_collapsed); + assert_eq!(p.text_scale, DEFAULT_TEXT_SCALE); + } + + #[test] + fn stepping_moves_one_notch_and_stops_at_the_ends() { + assert_eq!(step_text_scale(100, 1), 110); + assert_eq!(step_text_scale(110, 1), 125); + assert_eq!(step_text_scale(110, -1), 100); + assert_eq!(step_text_scale(200, 1), 200); + assert_eq!(step_text_scale(70, -1), 70); + assert_eq!(step_text_scale(110, 0), 110); + } + + #[test] + fn stepping_from_between_notches_goes_to_the_nearest_one_in_that_direction() { + assert_eq!(step_text_scale(105, 1), 110); + assert_eq!(step_text_scale(105, -1), 100); + assert_eq!(step_text_scale(500, -1), 200, "an out-of-range value comes back to the top step"); + assert_eq!(step_text_scale(1, 1), 70); + } + + #[test] + fn every_step_maps_to_a_sensible_rem_size() { + assert_eq!(rem_size_px(100), 16.0); + assert!((rem_size_px(110) - 17.6).abs() < 1e-4); + assert_eq!(rem_size_px(200), 32.0); + assert_eq!(rem_size_px(10), 11.2, "clamped to the smallest step"); + assert_eq!(rem_size_px(900), 32.0, "clamped to the largest step"); + for s in TEXT_SCALES { + assert_eq!(clamp_text_scale(s), s); + } + } + + #[test] + fn the_default_is_one_of_the_offered_steps() { + assert!(TEXT_SCALES.contains(&DEFAULT_TEXT_SCALE)); + } +} diff --git a/src/projection.rs b/src/projection.rs new file mode 100644 index 0000000..8af86f4 --- /dev/null +++ b/src/projection.rs @@ -0,0 +1,294 @@ +//! Balance projections: a straight line from a starting balance, income and +//! expense per period. +use chrono::{Duration, NaiveDate}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PeriodType { + Weeks, + Months, + Years, +} + +impl PeriodType { + pub const ALL: [PeriodType; 3] = [PeriodType::Weeks, PeriodType::Months, PeriodType::Years]; + + pub fn value(self) -> &'static str { + match self { + PeriodType::Weeks => "weeks", + PeriodType::Months => "months", + PeriodType::Years => "years", + } + } + + pub fn from_value(value: &str) -> Option { + Self::ALL.into_iter().find(|p| p.value() == value) + } + + /// Singular: "week", "month", "year". + pub fn unit(self) -> &'static str { + match self { + PeriodType::Weeks => "week", + PeriodType::Months => "month", + PeriodType::Years => "year", + } + } + + /// Plural: "weeks", "months", "years". + pub fn plural(self) -> &'static str { + self.value() + } + + pub fn limit(self) -> u32 { + match self { + PeriodType::Weeks => 260, + PeriodType::Months => 600, + PeriodType::Years => 60, + } + } + + fn days(self) -> i64 { + match self { + PeriodType::Weeks => 7, + PeriodType::Months => 30, + PeriodType::Years => 365, + } + } + + fn per_year(self) -> f64 { + match self { + PeriodType::Weeks => 52.0, + PeriodType::Months => 12.0, + PeriodType::Years => 1.0, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ProjectionInput { + pub starting_balance: i64, + pub income: i64, + pub expense: i64, + pub period_type: PeriodType, + pub periods: u32, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectionPoint { + pub step: u32, + pub label: String, + pub balance: i64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectionSummary { + pub net_per_period: i64, + pub final_balance: i64, + pub total_increase: i64, + /// Share of income kept, as a percentage. `None` without income. + pub retention_rate: Option, + pub projected_date: NaiveDate, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectionResult { + pub points: Vec, + pub summary: ProjectionSummary, + pub max_periods: u32, +} + +pub fn period_label(period_type: PeriodType, step: u32) -> String { + match step { + 0 => "Now".into(), + 1 => format!("1 {}", period_type.unit()), + n => format!("{n} {}s", period_type.unit()), + } +} + +/// A subscription's amount expressed per projection period, in cents. +/// Unknown frequencies contribute nothing. +pub fn normalize_subscription_to_period(amount: i64, frequency: &str, period_type: PeriodType) -> i64 { + let per_year = match frequency { + "weekly" => 52.0, + "bi_weekly" => 26.0, + "monthly" => 12.0, + "quarterly" => 4.0, + "bi_yearly" => 2.0, + "yearly" => 1.0, + _ => return 0, + }; + (amount as f64 * per_year / period_type.per_year()).round() as i64 +} + +pub fn compute_projection(input: ProjectionInput, today: NaiveDate) -> ProjectionResult { + let ProjectionInput { starting_balance, income, expense, period_type, periods } = input; + let net = income - expense; + let points = (0..=periods) + .map(|step| ProjectionPoint { + step, + label: period_label(period_type, step), + balance: starting_balance + net * i64::from(step), + }) + .collect(); + let final_balance = starting_balance + net * i64::from(periods); + ProjectionResult { + points, + summary: ProjectionSummary { + net_per_period: net, + final_balance, + total_increase: final_balance - starting_balance, + retention_rate: (income > 0).then(|| net as f64 / income as f64 * 100.0), + projected_date: today + Duration::days(period_type.days() * i64::from(periods)), + }, + max_periods: period_type.limit(), + } +} + +/// Compact axis label: `$950`, `-$1.5k`, `$2.0M`. +pub fn format_axis(cents: i64) -> String { + let dollars = cents.unsigned_abs() as f64 / 100.0; + let sign = if cents < 0 { "-" } else { "" }; + if dollars >= 1_000_000.0 { + format!("{sign}${:.1}M", dollars / 1_000_000.0) + } else if dollars >= 1000.0 { + format!("{sign}${:.1}k", dollars / 1000.0) + } else { + format!("{sign}${dollars:.0}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn today() -> NaiveDate { + NaiveDate::from_ymd_opt(2026, 10, 2).unwrap() + } + + fn input(income: i64, expense: i64, period_type: PeriodType, periods: u32) -> ProjectionInput { + ProjectionInput { starting_balance: 0, income, expense, period_type, periods } + } + + fn balances(r: &ProjectionResult) -> Vec { + r.points.iter().map(|p| p.balance).collect() + } + + #[test] + fn canonical_example() { + let r = compute_projection(input(120000, 10000, PeriodType::Months, 10), today()); + assert_eq!(r.summary.net_per_period, 110000); + assert_eq!(r.summary.final_balance, 1100000); + assert_eq!(r.summary.total_increase, 1100000); + assert_eq!(r.points.len(), 11); + assert_eq!(r.points[0].label, "Now"); + assert_eq!(r.points[0].balance, 0); + assert_eq!(r.points[10].label, "10 months"); + assert_eq!(r.points[10].balance, 1100000); + } + + #[test] + fn balances_increase_linearly() { + let r = compute_projection(input(300000, 120000, PeriodType::Weeks, 4), today()); + assert_eq!(balances(&r), vec![0, 180000, 360000, 540000, 720000]); + let labels: Vec<_> = r.points.iter().map(|p| p.label.as_str()).collect(); + assert_eq!(labels, ["Now", "1 week", "2 weeks", "3 weeks", "4 weeks"]); + } + + #[test] + fn offsets_by_starting_balance() { + let r = compute_projection( + ProjectionInput { starting_balance: 500000, income: 100000, expense: 0, period_type: PeriodType::Months, periods: 3 }, + today(), + ); + assert_eq!(r.summary.final_balance, 800000); + assert_eq!(r.summary.total_increase, 300000); + assert_eq!(r.points[0].balance, 500000); + assert_eq!(r.points[3].balance, 800000); + } + + #[test] + fn flat_when_income_equals_expense() { + let r = compute_projection(input(50000, 50000, PeriodType::Months, 5), today()); + assert_eq!(r.summary.net_per_period, 0); + assert!(r.points.iter().all(|p| p.balance == 0)); + } + + #[test] + fn declines_when_expenses_exceed_income() { + let r = compute_projection(input(20000, 50000, PeriodType::Months, 4), today()); + assert_eq!(r.summary.net_per_period, -30000); + assert_eq!(r.summary.final_balance, -120000); + assert_eq!(balances(&r), vec![0, -30000, -60000, -90000, -120000]); + } + + #[test] + fn retention_rate() { + let r = compute_projection(input(120000, 10000, PeriodType::Months, 10), today()); + assert!((r.summary.retention_rate.unwrap() - 91.6667).abs() < 0.001); + let none = compute_projection(input(0, 10000, PeriodType::Months, 10), today()); + assert_eq!(none.summary.retention_rate, None); + } + + #[test] + fn years_period_type() { + let r = compute_projection(input(1200000, 0, PeriodType::Years, 2), today()); + let labels: Vec<_> = r.points.iter().map(|p| p.label.as_str()).collect(); + assert_eq!(labels, ["Now", "1 year", "2 years"]); + assert_eq!(r.summary.final_balance, 2400000); + } + + #[test] + fn respects_period_limits() { + assert_eq!(PeriodType::Months.limit(), 600); + let r = compute_projection(input(0, 0, PeriodType::Months, 600), today()); + assert_eq!(r.max_periods, 600); + assert_eq!(r.points.len(), 601); + } + + #[test] + fn projected_date_is_periods_ahead() { + let r = compute_projection(input(0, 0, PeriodType::Months, 10), today()); + assert_eq!(r.summary.projected_date, today() + Duration::days(300)); + } + + #[test] + fn labels() { + for p in PeriodType::ALL { + assert_eq!(period_label(p, 0), "Now"); + assert_eq!(period_label(p, 1), format!("1 {}", p.unit())); + assert_eq!(period_label(p, 3), format!("3 {}s", p.unit())); + } + } + + #[test] + fn normalizes_subscriptions() { + use PeriodType::*; + let n = normalize_subscription_to_period; + assert_eq!(n(10000, "monthly", Months), 10000); + assert_eq!(n(12000, "monthly", Weeks), 2769); + assert_eq!(n(10000, "monthly", Years), 120000); + assert_eq!(n(1200000, "yearly", Months), 100000); + assert_eq!(n(1000, "weekly", Months), 4333); + assert_eq!(n(2000, "bi_weekly", Months), 4333); + assert_eq!(n(30000, "quarterly", Months), 10000); + assert_eq!(n(60000, "bi_yearly", Months), 10000); + assert_eq!(n(500, "weekly", Weeks), 500); + assert_eq!(n(1, "monthly", Weeks), 0); + assert_eq!(n(10000, "daily", Months), 0); + } + + #[test] + fn axis_labels() { + assert_eq!(format_axis(95000), "$950"); + assert_eq!(format_axis(-150000), "-$1.5k"); + assert_eq!(format_axis(200000000), "$2.0M"); + assert_eq!(format_axis(0), "$0"); + } + + #[test] + fn period_type_round_trips() { + for p in PeriodType::ALL { + assert_eq!(PeriodType::from_value(p.value()), Some(p)); + } + assert_eq!(PeriodType::from_value("decades"), None); + } +} diff --git a/src-tauri/src/seed/mod.rs b/src/seed/mod.rs similarity index 98% rename from src-tauri/src/seed/mod.rs rename to src/seed/mod.rs index e7d23ed..cdf3153 100644 --- a/src-tauri/src/seed/mod.rs +++ b/src/seed/mod.rs @@ -1,6 +1,5 @@ use crate::database::DbState; use rusqlite::params; -use tauri::State; use uuid::Uuid; fn uid() -> String { @@ -127,8 +126,7 @@ pub fn seed_database(conn: &rusqlite::Connection) -> Result { Ok(true) } -#[tauri::command] -pub fn seed_database_command(state: State<'_, DbState>) -> Result { +pub fn seed_database_command(state: &DbState) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; seed_database(&conn) } diff --git a/src/services/__tests__/api.test.ts b/src/services/__tests__/api.test.ts deleted file mode 100644 index 2497f35..0000000 --- a/src/services/__tests__/api.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { api } from "../api"; - -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), -})); - -import { invoke } from "@tauri-apps/api/core"; -const mockInvoke = vi.mocked(invoke); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("api.getAccounts", () => { - it("calls invoke with correct command", async () => { - const mockAccounts = [ - { id: "1", name: "Checking", balance: 500000 }, - { id: "2", name: "Savings", balance: 1500000 }, - ]; - mockInvoke.mockResolvedValue(mockAccounts); - - const result = await api.getAccounts(); - - expect(mockInvoke).toHaveBeenCalledWith("get_accounts"); - expect(result).toEqual(mockAccounts); - }); -}); - -describe("api.createAccount", () => { - it("calls invoke with correct command and request", async () => { - const request = { - name: "New Account", - account_type: "bank", - currency: "USD", - balance: 100000, - }; - const mockAccount = { id: "new-id", ...request, is_active: true }; - mockInvoke.mockResolvedValue(mockAccount); - - const result = await api.createAccount(request); - - expect(mockInvoke).toHaveBeenCalledWith("create_account", { request }); - expect(result).toEqual(mockAccount); - }); -}); - -describe("api.deleteAccount", () => { - it("calls invoke with correct command and id", async () => { - mockInvoke.mockResolvedValue(undefined); - - await api.deleteAccount("account-123"); - - expect(mockInvoke).toHaveBeenCalledWith("delete_account", { - id: "account-123", - }); - }); -}); - -describe("api.getTransactions", () => { - it("calls invoke with correct command", async () => { - mockInvoke.mockResolvedValue([]); - - const result = await api.getTransactions(); - - expect(mockInvoke).toHaveBeenCalledWith("get_transactions"); - expect(result).toEqual([]); - }); -}); - -describe("api.createTransaction", () => { - it("calls invoke with correct command and request", async () => { - const request = { - account_id: "acc-1", - transaction_type: "expense", - amount: 5000, - currency: "USD", - description: "Groceries", - transaction_date: "2026-03-15", - }; - const mockTxn = { id: "txn-1", ...request }; - mockInvoke.mockResolvedValue(mockTxn); - - const result = await api.createTransaction(request); - - expect(mockInvoke).toHaveBeenCalledWith("create_transaction", { request }); - expect(result).toEqual(mockTxn); - }); -}); - -describe("api.getCategories", () => { - it("calls invoke with correct command", async () => { - const mockCategories = [{ id: "cat-1", name: "Food" }]; - mockInvoke.mockResolvedValue(mockCategories); - - const result = await api.getCategories(); - - expect(mockInvoke).toHaveBeenCalledWith("get_categories"); - expect(result).toEqual(mockCategories); - }); -}); - -describe("api.createCategory", () => { - it("calls invoke with correct command and request", async () => { - const request = { - name: "New Category", - category_type: "expense", - icon: "star", - color: "#ff0000", - }; - const mockCategory = { id: "cat-new", ...request }; - mockInvoke.mockResolvedValue(mockCategory); - - const result = await api.createCategory(request); - - expect(mockInvoke).toHaveBeenCalledWith("create_category", { request }); - expect(result).toEqual(mockCategory); - }); -}); - -describe("api.getOverview", () => { - it("calls invoke with correct command", async () => { - const mockOverview = { - total_balance: 2000000, - total_income: 450000, - total_expenses: 120000, - accounts: [], - recent_transactions: [], - category_spending: [], - }; - mockInvoke.mockResolvedValue(mockOverview); - - const result = await api.getOverview(); - - expect(mockInvoke).toHaveBeenCalledWith("get_overview"); - expect(result).toEqual(mockOverview); - }); -}); - -describe("api.getOmarchyTheme", () => { - it("calls invoke with correct command", async () => { - const mockTheme = { - name: "default", - background: "#1a1b26", - foreground: "#c0caf5", - accent: "#7aa2f7", - colors: {}, - }; - mockInvoke.mockResolvedValue(mockTheme); - - const result = await api.getOmarchyTheme(); - - expect(mockInvoke).toHaveBeenCalledWith("get_omarchy_theme"); - expect(result).toEqual(mockTheme); - }); -}); diff --git a/src/services/__tests__/format.test.ts b/src/services/__tests__/format.test.ts deleted file mode 100644 index c0385e6..0000000 --- a/src/services/__tests__/format.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - formatMoney, - formatMoneyShort, - formatBalance, - formatDate, - formatDateFull, - todayISO, - currentMonth, - accountTypeLabel, -} from "../format"; - -describe("formatMoney", () => { - it("formats positive amount with + sign", () => { - expect(formatMoney(1000)).toBe("+$10.00"); - expect(formatMoney(525000)).toBe("+$5,250.00"); - }); - - it("formats negative amount with - sign", () => { - expect(formatMoney(-1000)).toBe("-$10.00"); - expect(formatMoney(-120000)).toBe("-$1,200.00"); - }); - - it("formats zero as positive", () => { - expect(formatMoney(0)).toBe("+$0.00"); - }); - - it("handles small amounts", () => { - expect(formatMoney(50)).toBe("+$0.50"); - expect(formatMoney(-25)).toBe("-$0.25"); - }); -}); - -describe("formatMoneyShort", () => { - it("formats amounts under 1000 normally", () => { - expect(formatMoneyShort(500)).toBe("$5.00"); - expect(formatMoneyShort(12345)).toBe("$123.45"); - }); - - it("formats amounts >= 1000 with k suffix", () => { - expect(formatMoneyShort(100000)).toBe("$1.0k"); - expect(formatMoneyShort(1500000)).toBe("$15.0k"); - expect(formatMoneyShort(5250000)).toBe("$52.5k"); - }); -}); - -describe("formatBalance", () => { - it("formats positive balance without sign", () => { - expect(formatBalance(525000)).toBe("$5,250.00"); - }); - - it("formats negative balance with minus", () => { - expect(formatBalance(-120000)).toBe("$-1,200.00"); - }); - - it("formats zero balance", () => { - expect(formatBalance(0)).toBe("$0.00"); - }); -}); - -describe("formatDate", () => { - it("formats date string to short format", () => { - const result = formatDate("2026-03-15"); - expect(result).toMatch(/Mar/); - expect(result).toContain("15"); - }); -}); - -describe("formatDateFull", () => { - it("formats date string to full format", () => { - const result = formatDateFull("2026-03-15"); - expect(result).toMatch(/Mar/); - expect(result).toContain("15"); - expect(result).toContain("2026"); - }); -}); - -describe("todayISO", () => { - it("returns today in YYYY-MM-DD format", () => { - const result = todayISO(); - expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/); - const today = new Date().toISOString().split("T")[0]; - expect(result).toBe(today); - }); -}); - -describe("currentMonth", () => { - it("returns current month and year", () => { - const result = currentMonth(); - const now = new Date(); - const monthName = now.toLocaleDateString("en-US", { month: "long" }); - const year = now.getFullYear().toString(); - expect(result).toContain(monthName); - expect(result).toContain(year); - }); -}); - -describe("accountTypeLabel", () => { - it("returns correct labels for known types", () => { - expect(accountTypeLabel("cash")).toBe("Cash"); - expect(accountTypeLabel("bank")).toBe("Bank"); - expect(accountTypeLabel("savings")).toBe("Savings"); - expect(accountTypeLabel("credit_card")).toBe("Credit Card"); - expect(accountTypeLabel("mobile_money")).toBe("Mobile Money"); - expect(accountTypeLabel("investment")).toBe("Investment"); - expect(accountTypeLabel("other")).toBe("Other"); - }); - - it("returns the type string for unknown types", () => { - expect(accountTypeLabel("crypto")).toBe("crypto"); - expect(accountTypeLabel("custom")).toBe("custom"); - }); -}); diff --git a/src/services/__tests__/projection.test.ts b/src/services/__tests__/projection.test.ts deleted file mode 100644 index d95a482..0000000 --- a/src/services/__tests__/projection.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - computeProjection, - periodLabel, - normalizeSubscriptionToPeriod, - PERIOD_LIMITS, - type ProjectionInput, -} from "../projection"; - -const baseInput = (overrides: Partial = {}): ProjectionInput => ({ - startingBalance: 0, - income: 0, - expense: 0, - periodType: "months", - periods: 10, - ...overrides, -}); - -describe("computeProjection", () => { - it("matches the canonical example: 1200 income, 100 expense, 10 months -> 11000", () => { - const result = computeProjection( - baseInput({ income: 120000, expense: 10000, periodType: "months", periods: 10 }) - ); - - expect(result.summary.netPerPeriod).toBe(110000); - expect(result.summary.finalBalance).toBe(1100000); - expect(result.summary.totalIncrease).toBe(1100000); - expect(result.points).toHaveLength(11); - expect(result.points[0].label).toBe("Now"); - expect(result.points[0].balance).toBe(0); - expect(result.points[10].label).toBe("10 months"); - expect(result.points[10].balance).toBe(1100000); - }); - - it("balances increase linearly by net per period", () => { - const result = computeProjection( - baseInput({ income: 300000, expense: 120000, periodType: "weeks", periods: 4 }) - ); - - expect(result.points.map((p) => p.balance)).toEqual([0, 180000, 360000, 540000, 720000]); - expect(result.points.map((p) => p.label)).toEqual([ - "Now", - "1 week", - "2 weeks", - "3 weeks", - "4 weeks", - ]); - }); - - it("offsets by starting balance", () => { - const result = computeProjection( - baseInput({ - startingBalance: 500000, - income: 100000, - expense: 0, - periodType: "months", - periods: 3, - }) - ); - - expect(result.summary.finalBalance).toBe(800000); - expect(result.summary.totalIncrease).toBe(300000); - expect(result.points[0].balance).toBe(500000); - expect(result.points[3].balance).toBe(800000); - }); - - it("produces a flat line when income equals expense", () => { - const result = computeProjection( - baseInput({ income: 50000, expense: 50000, periods: 5 }) - ); - - expect(result.summary.netPerPeriod).toBe(0); - expect(result.points.every((p) => p.balance === 0)).toBe(true); - }); - - it("declines when expenses exceed income", () => { - const result = computeProjection( - baseInput({ income: 20000, expense: 50000, periods: 4 }) - ); - - expect(result.summary.netPerPeriod).toBe(-30000); - expect(result.summary.finalBalance).toBe(-120000); - expect(result.points.map((p) => p.balance)).toEqual([0, -30000, -60000, -90000, -120000]); - }); - - it("computes retention rate as net over income", () => { - const result = computeProjection(baseInput({ income: 120000, expense: 10000 })); - - expect(result.summary.retentionRate).toBeCloseTo(91.6667, 3); - }); - - it("returns null retention rate when income is zero", () => { - const result = computeProjection(baseInput({ income: 0, expense: 10000 })); - - expect(result.summary.retentionRate).toBeNull(); - }); - - it("supports years period type", () => { - const result = computeProjection( - baseInput({ income: 1200000, expense: 0, periodType: "years", periods: 2 }) - ); - - expect(result.points.map((p) => p.label)).toEqual(["Now", "1 year", "2 years"]); - expect(result.summary.finalBalance).toBe(2400000); - }); - - it("respects period limits", () => { - expect(PERIOD_LIMITS.months).toBe(600); - - const result = computeProjection(baseInput({ periodType: "months", periods: 600 })); - expect(result.maxPeriods).toBe(600); - expect(result.points).toHaveLength(601); - }); - - it("sets projected date roughly periods ahead", () => { - const result = computeProjection(baseInput({ periodType: "months", periods: 10 })); - - const expected = new Date(); - expected.setDate(expected.getDate() + 300); - const expectedStr = expected.toISOString().split("T")[0]; - expect(result.summary.projectedDate).toBe(expectedStr); - }); -}); - -describe("periodLabel", () => { - it("labels step 0 as Now", () => { - expect(periodLabel("months", 0)).toBe("Now"); - expect(periodLabel("weeks", 0)).toBe("Now"); - expect(periodLabel("years", 0)).toBe("Now"); - }); - - it("uses singular for step 1", () => { - expect(periodLabel("months", 1)).toBe("1 month"); - expect(periodLabel("weeks", 1)).toBe("1 week"); - expect(periodLabel("years", 1)).toBe("1 year"); - }); - - it("uses plural for steps > 1", () => { - expect(periodLabel("months", 10)).toBe("10 months"); - expect(periodLabel("weeks", 3)).toBe("3 weeks"); - expect(periodLabel("years", 2)).toBe("2 years"); - }); -}); - -describe("normalizeSubscriptionToPeriod", () => { - it("normalizes monthly amounts to months unchanged", () => { - expect(normalizeSubscriptionToPeriod(10000, "monthly", "months")).toBe(10000); - }); - - it("converts monthly to weeks", () => { - expect(normalizeSubscriptionToPeriod(12000, "monthly", "weeks")).toBe(2769); - }); - - it("converts monthly to years", () => { - expect(normalizeSubscriptionToPeriod(10000, "monthly", "years")).toBe(120000); - }); - - it("converts yearly to months", () => { - expect(normalizeSubscriptionToPeriod(1200000, "yearly", "months")).toBe(100000); - }); - - it("converts weekly to months", () => { - expect(normalizeSubscriptionToPeriod(1000, "weekly", "months")).toBe(4333); - }); - - it("converts bi_weekly to months", () => { - expect(normalizeSubscriptionToPeriod(2000, "bi_weekly", "months")).toBe(4333); - }); - - it("converts quarterly to months", () => { - expect(normalizeSubscriptionToPeriod(30000, "quarterly", "months")).toBe(10000); - }); - - it("converts bi_yearly to months", () => { - expect(normalizeSubscriptionToPeriod(60000, "bi_yearly", "months")).toBe(10000); - }); - - it("rounds fractional cents", () => { - expect(normalizeSubscriptionToPeriod(500, "weekly", "weeks")).toBe(500); - expect(normalizeSubscriptionToPeriod(1, "monthly", "weeks")).toBe(0); - }); - - it("returns 0 for unknown frequencies", () => { - expect(normalizeSubscriptionToPeriod(10000, "daily", "months")).toBe(0); - }); -}); \ No newline at end of file diff --git a/src/services/api.ts b/src/services/api.ts deleted file mode 100644 index 79778a3..0000000 --- a/src/services/api.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import type { - Account, - Category, - Transaction, - Subscription, - Overview, - OmarchyTheme, - CreateAccountRequest, - CreateTransactionRequest, - CreateCategoryRequest, - UpdateCategoryRequest, - CreateSubscriptionRequest, - CsvMapping, - CsvPreview, - ImportOptions, - ImportResult, - ExportData, -} from "../types"; - -export const api = { - // Accounts - getAccounts: () => invoke("get_accounts"), - createAccount: (request: CreateAccountRequest) => - invoke("create_account", { request }), - updateAccount: (request: { id: string } & Partial) => - invoke("update_account", { request }), - deleteAccount: (id: string) => - invoke("delete_account", { id }), - - // Transactions - getTransactions: () => invoke("get_transactions"), - createTransaction: (request: CreateTransactionRequest) => - invoke("create_transaction", { request }), - updateTransaction: (request: { id: string } & Partial) => - invoke("update_transaction", { request }), - deleteTransaction: (id: string) => - invoke("delete_transaction", { id }), - deleteTransactions: (ids: string[]) => - invoke("delete_transactions", { ids }), - - // Categories - getCategories: () => invoke("get_categories"), - createCategory: (request: CreateCategoryRequest) => - invoke("create_category", { request }), - updateCategory: (request: UpdateCategoryRequest) => - invoke("update_category", { request }), - deleteCategory: (id: string) => - invoke("delete_category", { id }), - - // Subscriptions - getSubscriptions: () => invoke("get_subscriptions"), - createSubscription: (request: CreateSubscriptionRequest) => - invoke("create_subscription", { request }), - updateSubscription: (request: { id: string } & Partial) => - invoke("update_subscription", { request }), - deleteSubscription: (id: string) => - invoke("delete_subscription", { id }), - deleteSubscriptions: (ids: string[]) => - invoke("delete_subscriptions", { ids }), - - // Transfers - transfer: (request: { - from_account_id: string; - to_account_id: string; - amount: number; - currency: string; - notes?: string; - }) => - invoke("transfer", { - fromAccountId: request.from_account_id, - toAccountId: request.to_account_id, - amount: request.amount, - currency: request.currency, - notes: request.notes, - }), - - // Overview - getOverview: () => invoke("get_overview"), - - // Theme - getOmarchyTheme: () => invoke("get_omarchy_theme"), - - // Import - openCsvFileDialog: () => invoke("open_csv_file_dialog"), - previewCsvImport: (path: string, mapping: CsvMapping) => - invoke("preview_csv_import", { path, mapping }), - importTransactions: ( - path: string, - mapping: CsvMapping, - options: ImportOptions - ) => invoke("import_transactions", { path, mapping, options }), - - // Export - exportData: () => invoke("export_data"), - saveFileDialog: (defaultName: string, extension: string) => - invoke("save_file_dialog", { defaultName, extension }), - writeExportFile: (path: string, contents: string) => - invoke("write_export_file", { path, contents }), -}; diff --git a/src/services/format.ts b/src/services/format.ts deleted file mode 100644 index 1fd3ea0..0000000 --- a/src/services/format.ts +++ /dev/null @@ -1,66 +0,0 @@ -export function formatMoney(amount: number, _currency = "USD"): string { - const abs = Math.abs(amount); - const dollars = abs / 100; - const formatted = dollars.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); - const sign = amount >= 0 ? "+" : "-"; - return `${sign}$${formatted}`; -} - -export function formatMoneyShort(amount: number): string { - const abs = Math.abs(amount); - const dollars = abs / 100; - if (dollars >= 1000) { - return `$${(dollars / 1000).toFixed(1)}k`; - } - return `$${dollars.toFixed(2)}`; -} - -export function formatBalance(amount: number): string { - const dollars = amount / 100; - return `$${dollars.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`; -} - -export function formatDate(dateStr: string): string { - const d = new Date(dateStr); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); -} - -export function formatDateFull(dateStr: string): string { - const d = new Date(dateStr); - return d.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -export function todayISO(): string { - return new Date().toISOString().split("T")[0]; -} - -export function currentMonth(): string { - const d = new Date(); - return d.toLocaleDateString("en-US", { year: "numeric", month: "long" }); -} - -export function accountTypeLabel(type: string): string { - const labels: Record = { - cash: "Cash", - bank: "Bank", - savings: "Savings", - credit_card: "Credit Card", - mobile_money: "Mobile Money", - investment: "Investment", - other: "Other", - }; - return labels[type] || type; -} diff --git a/src/services/projection.ts b/src/services/projection.ts deleted file mode 100644 index 18e4a79..0000000 --- a/src/services/projection.ts +++ /dev/null @@ -1,138 +0,0 @@ -export type ProjectionPeriodType = "weeks" | "months" | "years"; - -export interface ProjectionInput { - startingBalance: number; - income: number; - expense: number; - periodType: ProjectionPeriodType; - periods: number; -} - -export interface ProjectionPoint { - step: number; - label: string; - balance: number; -} - -export interface ProjectionSummary { - netPerPeriod: number; - finalBalance: number; - totalIncrease: number; - retentionRate: number | null; - projectedDate: string | null; -} - -export interface ProjectionResult { - points: ProjectionPoint[]; - summary: ProjectionSummary; - maxPeriods: number; -} - -export const PERIOD_UNIT_LABELS: Record = { - weeks: "week", - months: "month", - years: "year", -}; - -export const PERIOD_PLURAL_LABELS: Record = { - weeks: "weeks", - months: "months", - years: "years", -}; - -export const PERIOD_LIMITS: Record = { - weeks: 260, - months: 600, - years: 60, -}; - -const PERIOD_DAYS: Record = { - weeks: 7, - months: 30, - years: 365, -}; - -export function periodLabel( - periodType: ProjectionPeriodType, - step: number -): string { - if (step === 0) return "Now"; - const unit = PERIOD_UNIT_LABELS[periodType]; - return step === 1 ? `1 ${unit}` : `${step} ${unit}s`; -} - -export type SubscriptionFrequency = - | "weekly" - | "bi_weekly" - | "monthly" - | "quarterly" - | "bi_yearly" - | "yearly"; - -const SUBSCRIPTION_PERIODS_PER_YEAR: Record = { - weekly: 52, - bi_weekly: 26, - monthly: 12, - quarterly: 4, - bi_yearly: 2, - yearly: 1, -}; - -const PROJECTION_PERIODS_PER_YEAR: Record = { - weeks: 52, - months: 12, - years: 1, -}; - -export function normalizeSubscriptionToPeriod( - amount: number, - frequency: string, - periodType: ProjectionPeriodType -): number { - const perYear = - SUBSCRIPTION_PERIODS_PER_YEAR[frequency as SubscriptionFrequency]; - if (!perYear) return 0; - return Math.round((amount * perYear) / PROJECTION_PERIODS_PER_YEAR[periodType]); -} - -export function computeProjection(input: ProjectionInput): ProjectionResult { - const { startingBalance, income, expense, periodType, periods } = input; - const netPerPeriod = income - expense; - const maxPeriods = PERIOD_LIMITS[periodType]; - - const points: ProjectionPoint[] = []; - const balance0 = startingBalance; - points.push({ - step: 0, - label: periodLabel(periodType, 0), - balance: balance0, - }); - - for (let step = 1; step <= periods; step++) { - points.push({ - step, - label: periodLabel(periodType, step), - balance: startingBalance + netPerPeriod * step, - }); - } - - const finalBalance = startingBalance + netPerPeriod * periods; - const totalIncrease = finalBalance - startingBalance; - - const today = new Date(); - today.setHours(0, 0, 0, 0); - today.setDate(today.getDate() + PERIOD_DAYS[periodType] * periods); - const projectedDate = today.toISOString().split("T")[0]; - - return { - points, - summary: { - netPerPeriod, - finalBalance, - totalIncrease, - retentionRate: income > 0 ? (netPerPeriod / income) * 100 : null, - projectedDate, - }, - maxPeriods, - }; -} \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css deleted file mode 100644 index 1d55c1b..0000000 --- a/src/styles/global.css +++ /dev/null @@ -1,1470 +0,0 @@ -:root { - /* Default theme (Tokyo Night) — overridden at runtime by Omarchy theme */ - --chelete-bg: #1a1b26; - --chelete-bg-secondary: #16161e; - --chelete-surface: #24283b; - --chelete-surface-hover: #2f3349; - --chelete-fg: #c0caf5; - --chelete-fg-muted: #737aa2; - --chelete-fg-subtle: #565f89; - --chelete-accent: #7aa2f7; - --chelete-success: #9ece6a; - --chelete-danger: #f7768e; - --chelete-warning: #e0af68; - --chelete-info: #7dcfff; - --chelete-border: #3b4261; - --chelete-radius: 4px; - --chelete-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --chelete-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, body, #root { - height: 100%; - width: 100%; - overflow: hidden; -} - -body { - font-family: var(--chelete-font); - background: var(--chelete-bg); - color: var(--chelete-fg); - font-size: 13px; - line-height: 1.5; - -webkit-font-smoothing: antialiased; -} - -/* ── Layout ────────────────────────────────────────────────── */ - -.app { - display: flex; - height: 100vh; -} - -.sidebar { - width: 200px; - min-width: 200px; - background: var(--chelete-bg-secondary); - border-right: 1px solid var(--chelete-border); - display: flex; - flex-direction: column; - padding: 16px 0; - overflow-y: auto; - overflow-x: hidden; - transition: width 0.2s ease, min-width 0.2s ease; -} - -.sidebar.collapsed { - width: 52px; - min-width: 52px; - align-items: center; -} - -.sidebar.collapsed .sidebar-logo .element-box { - width: 36px; - height: 36px; -} - -.sidebar.collapsed .element-number { - font-size: 7px; - top: 2px; - left: 3px; -} - -.sidebar.collapsed .element-symbol { - font-size: 16px; - margin-top: 3px; -} - -.sidebar.collapsed .sidebar-link { - justify-content: center; - padding: 10px; -} - -.sidebar.collapsed .sidebar-divider { - width: 24px; - margin: 8px auto; -} - -.sidebar.collapsed nav { - width: 100%; -} - -.sidebar-logo { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - gap: 12px; - padding: 0 16px 16px; - border-bottom: 1px solid var(--chelete-border); - margin-bottom: 8px; -} - -.sidebar.collapsed .sidebar-logo { - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0; - padding: 0 8px 16px; -} - -.element-box { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - border: 2px solid var(--chelete-accent); - border-radius: 4px; - position: relative; -} - -.element-number { - position: absolute; - top: 3px; - left: 5px; - font-size: 9px; - color: var(--chelete-fg-muted); - font-weight: 500; -} - -.element-symbol { - font-size: 22px; - font-weight: 700; - color: var(--chelete-accent); - line-height: 1; - margin-top: 4px; -} - -.element-name { - font-size: 9px; - color: var(--chelete-fg-muted); - letter-spacing: 0.12em; - text-transform: uppercase; - margin-top: 6px; -} - -.sidebar nav { - display: flex; - flex-direction: column; - gap: 1px; -} - -.sidebar-link { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 16px; - color: var(--chelete-fg-muted); - text-decoration: none; - font-size: 13px; - cursor: pointer; - transition: background 0.1s, color 0.1s; - border: none; - background: none; - width: 100%; - text-align: left; -} - -.sidebar-link:hover { - background: var(--chelete-surface); - color: var(--chelete-fg); -} - -.sidebar-link.active { - background: var(--chelete-surface); - color: var(--chelete-accent); -} - -.sidebar-link .shortcut { - margin-left: auto; - font-size: 11px; - color: var(--chelete-fg-subtle); - font-family: var(--chelete-mono); -} - -.sidebar-divider { - height: 1px; - background: var(--chelete-border); - margin: 8px 16px; -} - -.sidebar-divider-thin { - height: 1px; - background: var(--chelete-border); - margin: 8px 16px; - opacity: 0.6; -} - -.main-content { - flex: 1; - overflow-y: auto; - padding: 24px 32px; -} - -/* ── Header ────────────────────────────────────────────────── */ - -.page-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 24px; -} - -.page-title { - font-size: 18px; - font-weight: 600; - color: var(--chelete-fg); -} - -.page-subtitle { - font-size: 12px; - color: var(--chelete-fg-muted); - margin-top: 2px; -} - -/* ── Cards ─────────────────────────────────────────────────── */ - -.card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 12px; - margin-bottom: 24px; -} - -.card { - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 16px; -} - -.card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 6px; -} - -.card-icon { - color: var(--chelete-fg-muted); -} - -.card-icon.positive { - color: var(--chelete-success); -} - -.card-icon.negative { - color: var(--chelete-danger); -} - -.card-label { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg-muted); - margin-bottom: 6px; -} - -.card-value { - font-size: 22px; - font-weight: 600; - font-variant-numeric: tabular-nums; - color: var(--chelete-fg); -} - -.card-value.positive { - color: var(--chelete-success); -} - -.card-value.negative { - color: var(--chelete-danger); -} - -.card-change { - font-size: 11px; - margin-top: 4px; - color: var(--chelete-fg-muted); -} - -.overview-filters { - display: flex; - gap: 8px; - margin-bottom: 16px; -} - -/* ── Date filter ───────────────────────────────────────────── */ - -.df-row { - display: flex; - align-items: center; - gap: 12px; -} - -.df { - position: relative; -} - -.df-btn { - display: inline-flex; - align-items: center; - gap: 6px; - height: 28px; - padding: 0 10px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg-muted); - background: none; - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - cursor: pointer; - transition: all 0.12s ease; -} - -.df-btn:hover { - color: var(--chelete-fg); - border-color: var(--chelete-fg-subtle); - background: var(--chelete-surface); -} - -.df-chevron-open { - transform: rotate(180deg); - transition: transform 0.15s ease; -} - -.df-menu { - position: absolute; - top: calc(100% + 4px); - left: 0; - min-width: 170px; - background: var(--chelete-bg); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35); - z-index: 200; - padding: 4px; - opacity: 0; - transform: translateY(-4px); - animation: df-show 0.12s ease forwards; -} - -@keyframes df-show { - to { opacity: 1; transform: translateY(0); } -} - -.df-opt { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 30px; - padding: 0 8px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: none; - border: none; - border-radius: var(--chelete-radius); - cursor: pointer; - transition: background 0.08s; -} - -.df-opt:hover { - background: var(--chelete-surface); -} - -.df-opt-active { - color: var(--chelete-accent); -} - -.df-range { - display: flex; - align-items: center; - gap: 6px; -} - -.df-date { - height: 28px; - width: 135px; - padding: 0 8px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - outline: none; - transition: border-color 0.12s; -} - -.df-date:focus { - border-color: var(--chelete-accent); -} - -.df-arrow { - color: var(--chelete-fg-subtle); -} - -/* ── Date picker modal ─────────────────────────────────────── */ - -.df-modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.df-modal { - background: var(--chelete-bg); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); - min-width: 520px; -} - -.df-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px; - border-bottom: 1px solid var(--chelete-border); -} - -.df-modal-title { - font-size: 13px; - font-weight: 600; - color: var(--chelete-fg); -} - -.df-modal-close { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - background: none; - border: none; - border-radius: var(--chelete-radius); - color: var(--chelete-fg-muted); - cursor: pointer; - transition: all 0.1s; -} - -.df-modal-close:hover { - background: var(--chelete-surface); - color: var(--chelete-fg); -} - -.df-modal-body { - display: flex; - gap: 16px; - padding: 16px; - justify-content: center; -} - -.df-modal-body .react-datepicker { - border: none !important; - background: transparent !important; -} - -.df-modal-body .react-datepicker-wrapper { - display: block !important; -} - -.df-modal-footer { - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 12px 16px; - border-top: 1px solid var(--chelete-border); -} - -.df-modal-btn { - padding: 6px 14px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - cursor: pointer; - transition: all 0.1s; -} - -.df-modal-btn:hover { - background: var(--chelete-surface-hover); -} - -.df-modal-btn-primary { - background: var(--chelete-accent); - color: var(--chelete-bg); - border-color: var(--chelete-accent); -} - -.df-modal-btn-primary:hover { - opacity: 0.9; -} - -/* ── React Datepicker overrides ────────────────────────────── */ - -.df-calendar { - font-family: var(--chelete-font) !important; - background: transparent !important; - border: none !important; - color: var(--chelete-fg) !important; - padding: 0 !important; -} - -.df-calendar .react-datepicker__triangle { - display: none !important; -} - -.df-calendar .react-datepicker__header { - background: transparent !important; - border: none !important; - padding: 4px 8px 8px !important; -} - -.df-calendar .react-datepicker__current-month { - font-size: 13px !important; - font-weight: 600 !important; - color: var(--chelete-fg) !important; - font-family: var(--chelete-font) !important; -} - -.df-calendar .react-datepicker__day-names { - border-bottom: 1px solid var(--chelete-border) !important; - margin-bottom: 4px !important; -} - -.df-calendar .react-datepicker__day-name { - font-size: 10px !important; - font-weight: 500 !important; - color: var(--chelete-fg-subtle) !important; - width: 30px !important; - height: 28px !important; - line-height: 28px !important; -} - -.df-calendar .react-datepicker__week { - display: flex !important; -} - -.df-day { - font-size: 12px !important; - font-family: var(--chelete-font) !important; - color: var(--chelete-fg) !important; - width: 30px !important; - height: 30px !important; - line-height: 30px !important; - border-radius: var(--chelete-radius) !important; - transition: background 0.08s !important; - margin: 0 !important; - border: none !important; - background: none !important; -} - -.df-day:hover { - background: var(--chelete-surface-hover) !important; -} - -.df-day-inrange { - background: color-mix(in srgb, var(--chelete-accent) 12%, transparent) !important; - color: var(--chelete-accent) !important; - border-radius: 0 !important; -} - -.react-datepicker__day--selected, -.react-datepicker__day--keyboard-selected { - background: var(--chelete-accent) !important; - color: var(--chelete-bg) !important; - font-weight: 600 !important; - border-radius: var(--chelete-radius) !important; -} - -.react-datepicker__day--outside-month { - color: var(--chelete-fg-subtle) !important; - opacity: 0.4 !important; -} - -.react-datepicker__navigation { - top: 6px !important; - width: 24px !important; - height: 24px !important; -} - -.react-datepicker__navigation-icon::before { - display: none !important; -} - -.react-datepicker__navigation-icon::after { - display: none !important; -} - -.react-datepicker__navigation--previous { - background: none !important; - border: none !important; -} - -.react-datepicker__navigation--next { - background: none !important; - border: none !important; -} - -.react-datepicker__navigation--previous::after { - content: "" !important; - display: block !important; - width: 8px !important; - height: 8px !important; - border-left: 2px solid var(--chelete-fg-muted) !important; - border-bottom: 2px solid var(--chelete-fg-muted) !important; - transform: rotate(45deg) !important; - margin-left: 4px !important; - transition: border-color 0.1s !important; -} - -.react-datepicker__navigation--next::after { - content: "" !important; - display: block !important; - width: 8px !important; - height: 8px !important; - border-right: 2px solid var(--chelete-fg-muted) !important; - border-top: 2px solid var(--chelete-fg-muted) !important; - transform: rotate(45deg) !important; - margin-right: 4px !important; - transition: border-color 0.1s !important; -} - -.react-datepicker__navigation--previous:hover::after { - border-color: var(--chelete-fg) !important; -} - -.react-datepicker__navigation--next:hover::after { - border-color: var(--chelete-fg) !important; -} - -/* Hide month/year dropdowns entirely */ - -.react-datepicker__month-dropdown-container, -.react-datepicker__year-dropdown-container { - display: none !important; -} - -.react-datepicker__month-read-view, -.react-datepicker__year-read-view { - display: none !important; -} - -.react-datepicker__month-select, -.react-datepicker__year-select { - display: none !important; -} - -/* ── Toggle switch ─────────────────────────────────────────── */ - -.toggle-switch { - position: relative; - width: 36px; - height: 20px; - background: var(--chelete-border); - border: none; - border-radius: 10px; - cursor: pointer; - transition: background 0.2s ease; - padding: 0; - flex-shrink: 0; -} - -.toggle-switch.on { - background: var(--chelete-accent); -} - -.toggle-knob { - position: absolute; - top: 2px; - left: 2px; - width: 16px; - height: 16px; - background: white; - border-radius: 50%; - transition: transform 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); -} - -.toggle-switch.on .toggle-knob { - transform: translateX(16px); -} - -/* ── Settings layout ───────────────────────────────────────── */ - -.settings-row { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 0; -} - -.settings-row:not(:first-child) { - padding-top: 0; -} - -.settings-label { - font-size: 13px; - color: var(--chelete-fg); -} - -.settings-desc { - font-size: 11px; - color: var(--chelete-fg-muted); - margin-top: 2px; -} - -.settings-value { - font-size: 12px; - color: var(--chelete-fg-muted); -} - -.settings-divider { - height: 1px; - background: var(--chelete-border); - margin: 0; -} - -/* ── Icon picker ───────────────────────────────────────────── */ - -.icon-picker { - display: flex; - flex-wrap: wrap; - gap: 4px; -} - -.icon-picker-item { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - background: var(--chelete-surface); - color: var(--chelete-fg-muted); - cursor: pointer; - transition: background 0.1s, color 0.1s, border-color 0.1s; -} - -.icon-picker-item:hover { - background: var(--chelete-surface-hover); - color: var(--chelete-fg); - border-color: var(--chelete-fg-subtle); -} - -.icon-picker-item.selected { - background: var(--chelete-accent); - color: var(--chelete-bg); - border-color: var(--chelete-accent); -} - -.filter-select { - width: auto; - min-width: 140px; -} - -/* ── Tables ────────────────────────────────────────────────── */ - -.table-container { - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - overflow: hidden; - margin-bottom: 24px; -} - -table { - width: 100%; - border-collapse: collapse; - font-size: 13px; -} - -thead { - background: var(--chelete-surface); -} - -th { - text-align: left; - padding: 10px 12px; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg-muted); - font-weight: 500; - border-bottom: 1px solid var(--chelete-border); -} - -td { - padding: 10px 12px; - border-bottom: 1px solid var(--chelete-border); - color: var(--chelete-fg); -} - -tr:last-child td { - border-bottom: none; -} - -tr:hover td { - background: var(--chelete-surface-hover); -} - -.amount { - font-variant-numeric: tabular-nums; - font-weight: 500; - text-align: right; -} - -.amount.income { - color: var(--chelete-success); -} - -.amount.expense { - color: var(--chelete-danger); -} - -/* ── Buttons ───────────────────────────────────────────────── */ - -.btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - font-size: 13px; - font-family: var(--chelete-font); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - background: var(--chelete-surface); - color: var(--chelete-fg); - cursor: pointer; - transition: background 0.1s, border-color 0.1s; -} - -.btn:hover { - background: var(--chelete-surface-hover); - border-color: var(--chelete-fg-subtle); -} - -.btn-primary { - background: var(--chelete-accent); - color: var(--chelete-bg); - border-color: var(--chelete-accent); -} - -.btn-primary:hover { - opacity: 0.9; -} - -.btn-danger { - color: var(--chelete-danger); - border-color: var(--chelete-danger); - background: color-mix(in srgb, var(--chelete-danger) 10%, transparent); -} - -.btn-danger:hover { - background: color-mix(in srgb, var(--chelete-danger) 20%, transparent); - border-color: var(--chelete-danger); -} - -.type-tag { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: var(--chelete-radius); - font-size: 11px; - font-weight: 500; - text-transform: capitalize; - border: 1px solid var(--chelete-border); - color: var(--chelete-fg-muted); -} - -.type-tag.income { - color: var(--chelete-success); - border-color: var(--chelete-success); - background: color-mix(in srgb, var(--chelete-success) 10%, transparent); -} - -.type-tag.expense { - color: var(--chelete-danger); - border-color: var(--chelete-danger); - background: color-mix(in srgb, var(--chelete-danger) 10%, transparent); -} - -/* ── Forms ─────────────────────────────────────────────────── */ - -.form-group { - margin-bottom: 14px; -} - -.form-label { - display: block; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg-muted); - margin-bottom: 4px; -} - -.form-input, -.form-select { - width: 100%; - padding: 8px 10px; - font-size: 13px; - font-family: var(--chelete-font); - background: var(--chelete-bg); - color: var(--chelete-fg); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - outline: none; - transition: border-color 0.1s; - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; -} - -.form-select { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23737aa2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 30px; - cursor: pointer; -} - -.form-select option { - background: var(--chelete-bg); - color: var(--chelete-fg); -} - -.form-input:focus, -.form-select:focus { - border-color: var(--chelete-accent); -} - -.form-input::placeholder { - color: var(--chelete-fg-subtle); -} - -.form-checkbox { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - width: 16px; - height: 16px; - margin: 0; - background: var(--chelete-bg); - border: 1px solid var(--chelete-border); - border-radius: calc(var(--chelete-radius) * 1.25); - cursor: pointer; - display: inline-grid; - place-items: center; - transition: border-color 0.15s, background 0.15s; -} - -.form-checkbox:focus { - outline: none; - border-color: var(--chelete-accent); - box-shadow: 0 0 0 2px rgba(122, 162, 247, 0.2); -} - -.form-checkbox:checked { - background: var(--chelete-accent); - border-color: var(--chelete-accent); -} - -.form-checkbox:checked::after { - content: ""; - width: 8px; - height: 8px; - background: var(--chelete-bg); - clip-path: polygon(14% 50%, 38% 74%, 86% 26%, 78% 18%, 38% 58%, 22% 42%); -} - -.form-checkbox:indeterminate { - background: var(--chelete-accent); - border-color: var(--chelete-accent); -} - -.form-checkbox:indeterminate::after { - content: ""; - width: 8px; - height: 2px; - background: var(--chelete-bg); -} - -/* ── Modal / Dialog ────────────────────────────────────────── */ - -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal { - background: var(--chelete-bg-secondary); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 24px; - min-width: 380px; - max-width: 500px; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); -} - -.modal-title { - font-size: 15px; - font-weight: 600; - margin-bottom: 16px; - color: var(--chelete-fg); -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 20px; -} - -/* ── Progress bar ──────────────────────────────────────────── */ - -.progress-bar { - height: 6px; - background: var(--chelete-bg); - border-radius: 3px; - overflow: hidden; - margin-top: 8px; -} - -.progress-fill { - height: 100%; - border-radius: 3px; - transition: width 0.3s ease; -} - -.progress-fill.ok { - background: var(--chelete-accent); -} - -.progress-fill.warning { - background: var(--chelete-warning); -} - -.progress-fill.danger { - background: var(--chelete-danger); -} - -/* ── Category tag ──────────────────────────────────────────── */ - -.category-tag { - display: inline-block; - padding: 2px 8px; - font-size: 11px; - border-radius: var(--chelete-radius); - background: var(--chelete-surface); - color: var(--chelete-fg-muted); - border: 1px solid var(--chelete-border); -} - -/* ── Command palette ───────────────────────────────────────── */ - -.command-palette-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 20vh; - z-index: 2000; -} - -.command-palette { - background: var(--chelete-bg-secondary); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - width: 460px; - max-height: 360px; - overflow: hidden; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5); -} - -.command-palette input { - width: 100%; - padding: 14px 16px; - font-size: 14px; - font-family: var(--chelete-font); - background: transparent; - color: var(--chelete-fg); - border: none; - border-bottom: 1px solid var(--chelete-border); - outline: none; -} - -.command-palette-results { - max-height: 300px; - overflow-y: auto; -} - -.command-palette-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 16px; - cursor: pointer; - font-size: 13px; - color: var(--chelete-fg); - transition: background 0.05s; -} - -.command-palette-item:hover, -.command-palette-item.selected { - background: var(--chelete-surface); -} - -.command-palette-item .shortcut { - font-size: 11px; - color: var(--chelete-fg-subtle); - font-family: var(--chelete-mono); -} - -/* ── Overview budget bars ──────────────────────────────────── */ - -.budget-row { - margin-bottom: 12px; -} - -.budget-header { - display: flex; - justify-content: space-between; - font-size: 12px; - margin-bottom: 4px; -} - -.budget-name { - color: var(--chelete-fg); -} - -.budget-amounts { - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; -} - -/* ── Scrollbar ─────────────────────────────────────────────── */ - -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--chelete-border); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--chelete-fg-subtle); -} - -/* ── Donut charts ──────────────────────────────────────────── */ - -.donut-chart-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 16px; - margin-bottom: 24px; -} - -.donut-chart { - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 16px; -} - -.donut-chart-title { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg); - margin-bottom: 12px; -} - -.donut-chart-body { - display: flex; - align-items: center; - gap: 16px; -} - -.donut-chart-svg { - width: 140px; - height: 140px; - flex-shrink: 0; -} - -.donut-chart-label { - font-size: 10px; - fill: var(--chelete-fg-muted); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.donut-chart-value { - font-size: 20px; - font-weight: 600; - fill: var(--chelete-fg); - font-variant-numeric: tabular-nums; -} - -.donut-chart-empty { - color: var(--chelete-fg-muted); - font-size: 13px; - padding: 32px 0; - text-align: center; - flex: 1; -} - -.donut-chart-legend { - display: flex; - flex-direction: column; - gap: 8px; - flex: 1; - min-width: 0; -} - -.donut-chart-legend-item { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; -} - -.donut-chart-legend-color { - width: 10px; - height: 10px; - border-radius: 2px; - flex-shrink: 0; -} - -.donut-chart-legend-label { - color: var(--chelete-fg); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; -} - -.donut-chart-legend-value { - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; - flex-shrink: 0; -} - -/* ── Pagination ────────────────────────────────────────────── */ - -.pagination { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 12px; - margin-top: 12px; - margin-bottom: 24px; -} - -.pagination-btn { - padding: 6px 12px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - cursor: pointer; - transition: background 0.1s, border-color 0.1s; -} - -.pagination-btn:hover:not(:disabled) { - background: var(--chelete-surface-hover); - border-color: var(--chelete-fg-subtle); -} - -.pagination-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.pagination-info { - font-size: 12px; - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; -} - -.pagination-per-page { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - color: var(--chelete-fg-muted); -} - -.pagination-per-page .form-select { - min-width: 64px; - width: auto; - padding: 4px 24px 4px 8px; - font-size: 12px; - height: 26px; -} - -/* ── Projections ───────────────────────────────────────────── */ - -.projection-layout { - display: grid; - grid-template-columns: 260px 1fr; - gap: 16px; - align-items: start; -} - -.projection-form { - display: flex; - flex-direction: column; - gap: 12px; -} - -.projection-form .form-group { - margin-bottom: 0; -} - -.projection-hint { - font-size: 11px; - color: var(--chelete-fg-muted); - margin-top: 4px; -} - -.projection-subs-box { - background: var(--chelete-surface-hover); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 10px; - font-size: 12px; -} - -.projection-subs-header { - display: flex; - align-items: center; - gap: 6px; - color: var(--chelete-fg-muted); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 8px; -} - -.projection-subs-list { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 8px; - max-height: 180px; - overflow-y: auto; -} - -.projection-subs-item { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; -} - -.projection-subs-name { - color: var(--chelete-fg); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.projection-subs-amount { - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -.projection-subs-total { - border-top: 1px solid var(--chelete-border); - padding-top: 8px; - color: var(--chelete-fg); - font-weight: 600; - font-variant-numeric: tabular-nums; -} - -.projection-subs-empty { - margin-top: 8px; - color: var(--chelete-fg-muted); - font-size: 11px; -} - -.projection-content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 0; -} - -.projection-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - padding: 64px 24px; - color: var(--chelete-fg-muted); - font-size: 13px; - text-align: center; - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); -} - -.projection-chart-card { - padding: 16px; -} - -.projection-chart-title { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg); - margin-bottom: 12px; -} - -.projection-chart-container { - width: 100%; - height: 320px; -} - -@media (max-width: 900px) { - .projection-layout { - grid-template-columns: 1fr; - } -} diff --git a/src/subscriptions.rs b/src/subscriptions.rs new file mode 100644 index 0000000..6cc5b54 --- /dev/null +++ b/src/subscriptions.rs @@ -0,0 +1,171 @@ +//! Subscription scheduling and labels. +use crate::commands::Subscription; +use chrono::{Datelike, Duration, NaiveDate}; + +pub const FREQUENCIES: [(&str, &str); 6] = [ + ("weekly", "Weekly"), + ("bi_weekly", "Bi-weekly"), + ("monthly", "Monthly"), + ("quarterly", "Quarterly"), + ("bi_yearly", "Bi-yearly"), + ("yearly", "Yearly"), +]; + +pub fn frequency_label(value: &str) -> String { + FREQUENCIES + .iter() + .find(|(v, _)| *v == value) + .map(|(_, l)| l.to_string()) + .unwrap_or_else(|| value.to_string()) +} + +/// Subscriptions with the given frequency, or all of them for `"all"`. +pub fn filter_by_frequency<'a>(subs: &'a [Subscription], frequency: &str) -> Vec<&'a Subscription> { + subs.iter() + .filter(|s| frequency == "all" || s.frequency == frequency) + .collect() +} + +pub fn total_amount(subs: &[&Subscription]) -> i64 { + subs.iter().map(|s| s.amount).sum() +} + +enum Interval { + Days(i64), + Months(u32), +} + +fn interval(frequency: &str) -> Option { + Some(match frequency { + "weekly" => Interval::Days(7), + "bi_weekly" => Interval::Days(14), + "monthly" => Interval::Months(1), + "quarterly" => Interval::Months(3), + "bi_yearly" => Interval::Months(6), + "yearly" => Interval::Months(12), + _ => return None, + }) +} + +/// Add months the way JavaScript's `setFullYear(y, m + n, d)` does: a day past +/// the end of the target month spills into the following month. +fn add_months_overflowing(date: NaiveDate, months: u32) -> Option { + let total = date.month0() + months; + let year = date.year() + (total / 12) as i32; + let first = NaiveDate::from_ymd_opt(year, total % 12 + 1, 1)?; + first.checked_add_signed(Duration::days(i64::from(date.day()) - 1)) +} + +/// The first payment on or after `today`, stepping forward from `start`. +/// Mirrors the web UI: unknown frequencies return the start date unchanged, +/// and stepping stops after 120 iterations. +pub fn next_payment_date(start: Option<&str>, frequency: &str, today: NaiveDate) -> Option { + let start = NaiveDate::parse_from_str(start?, "%Y-%m-%d").ok()?; + let Some(step) = interval(frequency) else { + return Some(start); + }; + let mut next = start; + let mut iterations = 0; + while next < today && iterations < 120 { + next = match step { + Interval::Days(d) => next.checked_add_signed(Duration::days(d))?, + Interval::Months(m) => add_months_overflowing(next, m)?, + }; + iterations += 1; + } + Some(next) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn d(y: i32, m: u32, day: u32) -> NaiveDate { + NaiveDate::from_ymd_opt(y, m, day).unwrap() + } + + #[test] + fn future_start_is_returned_as_is() { + assert_eq!(next_payment_date(Some("2026-12-01"), "monthly", d(2026, 6, 1)), Some(d(2026, 12, 1))); + } + + #[test] + fn today_counts_as_due() { + assert_eq!(next_payment_date(Some("2026-06-01"), "monthly", d(2026, 6, 1)), Some(d(2026, 6, 1))); + } + + #[test] + fn monthly_rolls_forward() { + assert_eq!(next_payment_date(Some("2026-01-15"), "monthly", d(2026, 6, 20)), Some(d(2026, 7, 15))); + } + + #[test] + fn weekly_and_bi_weekly() { + assert_eq!(next_payment_date(Some("2026-06-01"), "weekly", d(2026, 6, 10)), Some(d(2026, 6, 15))); + assert_eq!(next_payment_date(Some("2026-06-01"), "bi_weekly", d(2026, 6, 10)), Some(d(2026, 6, 15))); + } + + #[test] + fn longer_periods() { + assert_eq!(next_payment_date(Some("2025-01-10"), "quarterly", d(2026, 6, 1)), Some(d(2026, 7, 10))); + assert_eq!(next_payment_date(Some("2025-01-10"), "bi_yearly", d(2026, 6, 1)), Some(d(2026, 7, 10))); + assert_eq!(next_payment_date(Some("2024-03-05"), "yearly", d(2026, 6, 1)), Some(d(2027, 3, 5))); + } + + #[test] + fn month_end_spills_like_javascript() { + // Jan 31 + 1 month is "Feb 31", which JavaScript normalises to Mar 3. + assert_eq!(next_payment_date(Some("2026-01-31"), "monthly", d(2026, 2, 1)), Some(d(2026, 3, 3))); + } + + #[test] + fn missing_or_bad_start_has_no_next_payment() { + assert_eq!(next_payment_date(None, "monthly", d(2026, 1, 1)), None); + assert_eq!(next_payment_date(Some("not a date"), "monthly", d(2026, 1, 1)), None); + } + + #[test] + fn unknown_frequency_returns_the_start() { + assert_eq!(next_payment_date(Some("2020-01-01"), "fortnightly-ish", d(2026, 1, 1)), Some(d(2020, 1, 1))); + } + + #[test] + fn stepping_stops_after_120_iterations() { + // Weekly from 1990 would need ~1,900 steps; the cap leaves it in the past. + let next = next_payment_date(Some("1990-01-01"), "weekly", d(2026, 1, 1)).unwrap(); + assert_eq!(next, d(1990, 1, 1) + Duration::days(7 * 120)); + } + + fn sub(freq: &str, amount: i64) -> Subscription { + Subscription { + id: freq.into(), + name: freq.into(), + amount, + currency: "USD".into(), + frequency: freq.into(), + category_id: None, + account_id: None, + start_date: None, + is_active: true, + created_at: String::new(), + updated_at: String::new(), + } + } + + #[test] + fn filters_and_totals() { + let subs = vec![sub("monthly", 1500), sub("yearly", 12000), sub("monthly", 500)]; + let monthly = filter_by_frequency(&subs, "monthly"); + assert_eq!(monthly.len(), 2); + assert_eq!(total_amount(&monthly), 2000); + assert_eq!(filter_by_frequency(&subs, "all").len(), 3); + assert_eq!(total_amount(&filter_by_frequency(&subs, "all")), 14000); + assert!(filter_by_frequency(&subs, "weekly").is_empty()); + } + + #[test] + fn labels() { + assert_eq!(frequency_label("bi_weekly"), "Bi-weekly"); + assert_eq!(frequency_label("other"), "other"); + } +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 6ffa79b..0000000 --- a/src/types.ts +++ /dev/null @@ -1,198 +0,0 @@ -export interface Account { - id: string; - name: string; - account_type: string; - currency: string; - balance: number; - color: string | null; - icon: string | null; - is_active: boolean; - sort_order: number; - created_at: string; - updated_at: string; -} - -export interface Category { - id: string; - name: string; - parent_id: string | null; - category_type: string; - icon: string | null; - color: string | null; - sort_order: number; - created_at: string; - updated_at: string; -} - -export interface Transaction { - id: string; - account_id: string; - category_id: string | null; - transaction_type: string; - amount: number; - currency: string; - description: string; - merchant: string | null; - notes: string | null; - transaction_date: string; - created_at: string; - updated_at: string; -} - -export interface Overview { - total_balance: number; - total_income: number; - total_expenses: number; - accounts: Account[]; - recent_transactions: Transaction[]; - category_spending: CategorySpending[]; -} - -export interface CategorySpending { - category_id: string; - category_name: string; - spent: number; - budget_limit: number | null; -} - -export interface OmarchyTheme { - name: string; - background: string; - foreground: string; - accent: string; - colors: Record; -} - -export type View = - | "overview" - | "transactions" - | "accounts" - | "categories" - | "subscriptions" - | "projections" - | "settings"; - -export interface CreateAccountRequest { - name: string; - account_type: string; - currency: string; - balance: number; - color?: string; - icon?: string; -} - -export interface CreateTransactionRequest { - account_id: string; - category_id?: string; - transaction_type: string; - amount: number; - currency: string; - description: string; - merchant?: string; - notes?: string; - transaction_date: string; -} - -export interface CreateCategoryRequest { - name: string; - parent_id?: string; - category_type: string; - icon?: string; - color?: string; -} - -export interface UpdateCategoryRequest { - id: string; - name?: string; - category_type?: string; - icon?: string; - color?: string; -} - -export interface Subscription { - id: string; - name: string; - amount: number; - currency: string; - frequency: string; - category_id: string | null; - account_id: string | null; - start_date: string | null; - is_active: boolean; - created_at: string; - updated_at: string; -} - -export interface CreateSubscriptionRequest { - name: string; - amount: number; - currency: string; - frequency: string; - category_id?: string; - account_id?: string; - start_date?: string; -} - -export type CsvField = - | "date" - | "description" - | "merchant" - | "amount" - | "income_amount" - | "expense_amount" - | "type" - | "category" - | "account" - | "currency" - | "notes" - | "ignore"; - -export interface CsvMapping { - columns: Record; - default_account_id?: string; - default_category_id?: string; - default_currency?: string; - default_transaction_type?: "income" | "expense"; - type_aliases: Record; -} - -export interface ParsedRow { - row_index: number; - date?: string; - description?: string; - merchant?: string; - transaction_type?: string; - amount_cents?: number; - amount_source?: "income_column" | "expense_column" | "amount_column"; - currency?: string; - category?: string; - account?: string; - notes?: string; - errors: string[]; -} - -export interface CsvPreview { - headers: string[]; - rows: ParsedRow[]; - total_rows: number; -} - -export interface ImportOptions { - default_account_id?: string; - default_category_id?: string; - default_currency?: string; - skip_duplicates: boolean; -} - -export interface ImportResult { - imported: number; - skipped: number; - errors: string[]; -} - -export interface ExportData { - accounts: Account[]; - categories: Category[]; - transactions: Transaction[]; - subscriptions: Subscription[]; -} diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs new file mode 100644 index 0000000..23e4253 --- /dev/null +++ b/src/ui/accounts.rs @@ -0,0 +1,488 @@ +use super::widgets::*; +use chelete_lib::commands::{self, Account, CreateAccountRequest}; +use chelete_lib::database::DbState; +use chelete_lib::format::{account_type_label, format_balance, parse_cents}; +use gpui_kit::base::input::InputState; +use gpui_kit::{ + div, prelude::*, rems, Context, Entity, EventEmitter, IntoElement, Render, Subscription, + SharedString, Window, +}; +use gpui_omarchy::{ + button, empty_state, icon, input, panel, select, ActiveTheme as _, ButtonVariant, + ChoiceState, IconName, +}; +use std::sync::Arc; + +enum Dialog { + Add(Entity), + Transfer(Entity), +} + +pub struct AccountsScreen { + db: Arc, + accounts: Result, String>, + dialog: Option, + _dialog_sub: Option, +} + +impl EventEmitter for AccountsScreen {} + +impl AccountsScreen { + pub fn new(db: Arc) -> Self { + let accounts = commands::get_accounts(&db); + Self { db, accounts, dialog: None, _dialog_sub: None } + } + + pub fn reload(&mut self, cx: &mut Context) { + self.accounts = commands::get_accounts(&self.db); + cx.notify(); + } + + pub fn accounts(&self) -> &[Account] { + self.accounts.as_deref().unwrap_or(&[]) + } + + #[cfg(test)] + pub fn has_dialog(&self) -> bool { + self.dialog.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| AddAccountForm::new(self.db.clone(), window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, Self::on_form_event)); + self.dialog = Some(Dialog::Add(form)); + cx.notify(); + } + + fn open_transfer(&mut self, window: &mut Window, cx: &mut Context) { + let accounts = self.accounts().to_vec(); + let form = cx.new(|cx| TransferForm::new(self.db.clone(), accounts, window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, Self::on_form_event)); + self.dialog = Some(Dialog::Transfer(form)); + cx.notify(); + } + + fn on_form_event(&mut self, _: Entity, event: &FormEvent, cx: &mut Context) { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + self.accounts = commands::get_accounts(&self.db); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + self.dialog = None; + self._dialog_sub = None; + cx.notify(); + } + + fn delete(&mut self, id: String, cx: &mut Context) { + match commands::delete_account(&self.db, id) { + Ok(()) => { + self.accounts = commands::get_accounts(&self.db); + } + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete account: {e}"))), + } + cx.notify(); + } +} + +impl Render for AccountsScreen { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let actions = div() + .flex() + .gap(rems(0.5)) + .child( + button("transfer", "Transfer", ButtonVariant::Outline, cx) + .child(icon(IconName::ArrowLeftRight).size(rems(0.875))) + .on_click(cx.listener(|this, _, window, cx| this.open_transfer(window, cx))), + ) + .child( + button("add-account", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))), + ); + + let body = match &self.accounts { + Err(e) => div().text_color(t.danger).child(format!("Could not load accounts: {e}")).into_any_element(), + Ok(list) if list.is_empty() => { + empty_state("No accounts yet", "Add one to get started.", cx).into_any_element() + } + Ok(list) => { + let cards: Vec<_> = list + .iter() + .map(|a| { + let id = a.id.clone(); + panel(account_type_label(&a.account_type), cx) + .w(rems(16.)) + .child(div().text_size(rems(0.875)).child(a.name.clone())) + .child( + div() + .text_size(rems(1.25)) + .text_color(if a.balance < 0 { t.danger } else { t.foreground }) + .child(format_balance(a.balance)), + ) + .child( + button(SharedString::from(format!("delete-account-{}", a.id)), "Delete", ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.delete(id.clone(), cx))), + ) + }) + .collect(); + div().flex().flex_wrap().gap(rems(1.)).children(cards).into_any_element() + } + }; + + let dialog = self.dialog.as_ref().map(|d| match d { + Dialog::Add(form) => form.clone().into_any_element(), + Dialog::Transfer(form) => form.clone().into_any_element(), + }); + + div() + .id("accounts") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Accounts", actions, cx)) + .child(body) + .children(dialog) + } +} + +#[derive(Clone, Debug)] +pub enum FormEvent { + Cancel, + Saved(String), + Failed(String), +} + +pub struct AddAccountForm { + db: Arc, + name: Entity, + kind: Entity, + balance: Entity, + _subs: Vec, +} + +impl EventEmitter for AddAccountForm {} + +impl AddAccountForm { + fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let name = text_input("e.g. Main Bank", window, cx); + name.update(cx, |s, cx| s.focus(window, cx)); + let balance = text_input("0.00", window, cx); + let _subs = submit_on_enter(&[&name, &balance], Self::submit, cx); + Self { + db, + name, + kind: choice(account_type_options(), Some("cash"), window, cx), + balance, + _subs, + } + } + + fn submit(&mut self, cx: &mut Context) { + let name = text_of(&self.name, cx); + if name.is_empty() { + return; + } + let balance_text = text_of(&self.balance, cx); + let balance = if balance_text.is_empty() { + 0 + } else { + match parse_cents(&balance_text) { + Some(c) => c, + None => return cx.emit(FormEvent::Failed("Starting balance is not a valid amount".into())), + } + }; + let request = CreateAccountRequest { + name, + account_type: choice_value(&self.kind, cx).unwrap_or_else(|| "cash".into()), + currency: "USD".into(), + balance, + color: None, + icon: None, + }; + match commands::create_account(&self.db, request) { + Ok(a) => cx.emit(FormEvent::Saved(format!("Added account {}", a.name))), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not add account: {e}"))), + } + } +} + +impl Render for AddAccountForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Name", input("account-name", &self.name, window, cx), cx)) + .child(field("Type", select("account-type", &self.kind, window, cx), cx)) + .child(field("Starting Balance", input("account-balance", &self.balance, window, cx), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("add-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel))), + ) + .child( + button("add-submit", "Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), + ), + ); + modal("Add Account", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) + } +} + +pub struct TransferForm { + db: Arc, + accounts: Vec, + from: Entity, + to: Entity, + amount: Entity, + notes: Entity, + _subs: Vec, +} + +impl EventEmitter for TransferForm {} + +impl TransferForm { + fn new(db: Arc, accounts: Vec, window: &mut Window, cx: &mut Context) -> Self { + let items: Vec<(String, String)> = + accounts.iter().map(|a| (a.id.clone(), a.name.clone())).collect(); + let first = accounts.first().map(|a| a.id.clone()); + let second = accounts.get(1).or(accounts.first()).map(|a| a.id.clone()); + let amount = text_input("0.00", window, cx); + amount.update(cx, |s, cx| s.focus(window, cx)); + let notes = text_input("e.g. Monthly savings", window, cx); + let _subs = submit_on_enter(&[&amount, ¬es], Self::submit, cx); + Self { + db, + from: choice(items.clone(), first.as_deref(), window, cx), + to: choice(items, second.as_deref(), window, cx), + amount, + notes, + accounts, + _subs, + } + } + + fn submit(&mut self, cx: &mut Context) { + let (Some(from), Some(to)) = (choice_value(&self.from, cx), choice_value(&self.to, cx)) else { + return; + }; + if from == to { + return cx.emit(FormEvent::Failed("Pick two different accounts".into())); + } + let amount_text = text_of(&self.amount, cx); + let cents = match parse_cents(&amount_text) { + Some(c) if c > 0 => c, + _ => return cx.emit(FormEvent::Failed("Enter an amount greater than zero".into())), + }; + let notes = text_of(&self.notes, cx); + let to_name = self.accounts.iter().find(|a| a.id == to).map(|a| a.name.clone()).unwrap_or_default(); + match commands::transfer( + &self.db, + from, + to, + cents, + "USD".into(), + (!notes.is_empty()).then_some(notes), + ) { + Ok(_) => cx.emit(FormEvent::Saved(format!("Transferred ${amount_text} to {to_name}"))), + Err(e) => cx.emit(FormEvent::Failed(format!("Transfer failed: {e}"))), + } + } +} + +impl Render for TransferForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(field("From", select("transfer-from", &self.from, window, cx), cx)) + .child(field("To", select("transfer-to", &self.to, window, cx), cx)), + ) + .child(field("Amount", input("transfer-amount", &self.amount, window, cx), cx)) + .child(field("Notes", input("transfer-notes", &self.notes, window, cx), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("transfer-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel))), + ) + .child( + button("transfer-submit", "Transfer", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), + ), + ); + modal("Transfer Money", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::{TestAppContext, VisualTestContext}; + use std::cell::RefCell; + use std::rc::Rc; + + fn setup(cx: &mut TestAppContext) -> (Entity, &mut VisualTestContext, Rc>>) { + init(cx); + let db = seeded_db(); + let (screen, vcx) = cx.add_window_view(|_, _| AccountsScreen::new(db)); + let events = Rc::new(RefCell::new(Vec::new())); + let sink = events.clone(); + vcx.update(|_, cx| { + cx.subscribe(&screen, move |_, e: &ScreenEvent, _| sink.borrow_mut().push(e.clone())).detach(); + }); + (screen, vcx, events) + } + + fn open_add(screen: &Entity, cx: &mut VisualTestContext) -> Entity { + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Add(f)) => f.clone(), + _ => panic!("add form should be open"), + }) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + #[gpui_kit::test] + fn lists_seeded_accounts(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + cx.update(|_, cx| assert_eq!(screen.read(cx).accounts().len(), 4)); + } + + #[gpui_kit::test] + fn add_account_saves_and_closes(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + let form = open_add(&screen, cx); + let (name, balance) = cx.update(|_, cx| (form.read(cx).name.clone(), form.read(cx).balance.clone())); + type_into(&name, "Rainy Day", cx); + type_into(&balance, "1,250.50", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let screen = screen.read(cx); + assert!(!screen.has_dialog(), "dialog closes after saving"); + let added = screen.accounts().iter().find(|a| a.name == "Rainy Day").expect("account saved"); + assert_eq!(added.balance, 125050); + assert_eq!(added.account_type, "cash"); + }); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: true, .. }))); + } + + #[gpui_kit::test] + fn blank_name_is_ignored_and_bad_balance_reports(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + let form = open_add(&screen, cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog(), "blank name keeps the form open")); + + let (name, balance) = cx.update(|_, cx| (form.read(cx).name.clone(), form.read(cx).balance.clone())); + type_into(&name, "Oops", cx); + type_into(&balance, "twelve", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_dialog(), "invalid balance keeps the form open"); + assert_eq!(screen.read(cx).accounts().len(), 4); + }); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: false, .. }))); + } + + #[gpui_kit::test] + fn transfer_moves_money_between_accounts(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + let before: Vec = cx.update(|_, cx| screen.read(cx).accounts().iter().map(|a| a.balance).collect()); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_transfer(window, cx))); + let form = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Transfer(f)) => f.clone(), + _ => panic!("transfer form should be open"), + }); + let amount = cx.update(|_, cx| form.read(cx).amount.clone()); + type_into(&amount, "10.00", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let screen = screen.read(cx); + assert!(!screen.has_dialog()); + let after: Vec = screen.accounts().iter().map(|a| a.balance).collect(); + assert_eq!(after[0], before[0] - 1000, "source debited"); + assert_eq!(after[1], before[1] + 1000, "destination credited"); + assert_eq!(after.iter().sum::(), before.iter().sum::(), "total unchanged"); + }); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: true, .. }))); + } + + #[gpui_kit::test] + fn transfer_rejects_zero_amount(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_transfer(window, cx))); + let form = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Transfer(f)) => f.clone(), + _ => panic!("transfer form should be open"), + }); + let amount = cx.update(|_, cx| form.read(cx).amount.clone()); + type_into(&amount, "0", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: false, .. }))); + } + + #[gpui_kit::test] + fn delete_removes_the_account(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + let id = cx.update(|_, cx| screen.read(cx).accounts()[0].id.clone()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.delete(id.clone(), cx))); + cx.update(|_, cx| { + let accounts = screen.read(cx).accounts(); + assert_eq!(accounts.len(), 3); + assert!(accounts.iter().all(|a| a.id != id)); + }); + } + + #[gpui_kit::test] + fn escape_closes_the_open_form(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + open_add(&screen, cx); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("escape"); + cx.update(|_, cx| assert!(!screen.read(cx).has_dialog(), "escape dismissed the form")); + cx.update(|_, cx| assert_eq!(screen.read(cx).accounts().len(), 4, "nothing was saved")); + } + + #[gpui_kit::test] + fn enter_in_a_field_submits_the_form(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + let form = open_add(&screen, cx); + let name = cx.update(|_, cx| form.read(cx).name.clone()); + type_into(&name, "Keyboard Only", cx); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("enter"); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + assert!(s.accounts().iter().any(|a| a.name == "Keyboard Only")); + }); + } +} diff --git a/src/ui/categories.rs b/src/ui/categories.rs new file mode 100644 index 0000000..1435568 --- /dev/null +++ b/src/ui/categories.rs @@ -0,0 +1,463 @@ +use super::accounts::FormEvent; +use super::icons::{category_icon, CATEGORY_ICONS, DEFAULT_ICON}; +use super::widgets::*; +use chelete_lib::commands::{self, Category, CreateCategoryRequest, Transaction, UpdateCategoryRequest}; +use chelete_lib::database::DbState; +use gpui_kit::base::input::InputState; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Entity, EventEmitter, Hsla, IntoElement, Render, + SharedString, Subscription, Window, +}; +use gpui_omarchy::{ + button, icon, icon_button, input, select, ActiveTheme as _, ButtonVariant, ChoiceState, + IconName, +}; +use std::collections::HashMap; +use std::sync::Arc; + +/// How many transactions reference each category. +pub fn record_counts(transactions: &[Transaction]) -> HashMap { + let mut counts = HashMap::new(); + for t in transactions { + if let Some(id) = &t.category_id { + *counts.entry(id.clone()).or_insert(0) += 1; + } + } + counts +} + +fn records_label(count: usize) -> String { + format!("{count} {}", if count == 1 { "record" } else { "records" }) +} + +pub struct CategoriesScreen { + db: Arc, + categories: Result, String>, + counts: HashMap, + form: Option>, + _form_sub: Option, +} + +impl EventEmitter for CategoriesScreen {} + +impl CategoriesScreen { + pub fn new(db: Arc) -> Self { + let mut screen = Self { + db, + categories: Ok(Vec::new()), + counts: HashMap::new(), + form: None, + _form_sub: None, + }; + screen.load(); + screen + } + + fn load(&mut self) { + self.categories = commands::get_categories(&self.db); + self.counts = commands::get_transactions(&self.db) + .map(|t| record_counts(&t)) + .unwrap_or_default(); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + pub fn categories(&self) -> &[Category] { + self.categories.as_deref().unwrap_or(&[]) + } + + #[cfg(test)] + pub fn has_form(&self) -> bool { + self.form.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + self.open(None, window, cx); + } + + fn open_edit(&mut self, category: Category, window: &mut Window, cx: &mut Context) { + self.open(Some(category), window, cx); + } + + fn open(&mut self, editing: Option, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| CategoryForm::new(self.db.clone(), editing, window, cx)); + self._form_sub = Some(cx.subscribe(&form, |this, _, event: &FormEvent, cx| { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + this.load(); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.form = None; + this._form_sub = None; + cx.notify(); + })); + self.form = Some(form); + cx.notify(); + } + + fn delete(&mut self, id: String, cx: &mut Context) { + match commands::delete_category(&self.db, id) { + Ok(()) => self.load(), + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete category: {e}"))), + } + cx.notify(); + } + + fn section(&self, title: &str, kind: &str, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let rows: Vec = self + .categories() + .iter() + .filter(|c| c.category_type == kind) + .cloned() + .collect(); + let body = if rows.is_empty() { + div() + .py(rems(1.)) + .text_color(t.secondary) + .child(format!("No {kind} categories")) + .into_any_element() + } else { + let mut list = div().flex().flex_col().border_1().border_color(t.border); + for (i, c) in rows.into_iter().enumerate() { + let count = self.counts.get(&c.id).copied().unwrap_or(0); + let color = c.color.as_deref().and_then(parse_hex).unwrap_or(t.secondary); + let edit = c.clone(); + let id = c.id.clone(); + list = list.child( + div() + .flex() + .items_center() + .gap(rems(0.5)) + .px(rems(0.5)) + .py(rems(0.25)) + .when(i > 0, |r| r.border_t_1().border_color(t.border)) + .child( + div() + .flex() + .flex_1() + .items_center() + .gap(rems(0.5)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(14.), color))) + .child(c.name.clone()), + ) + .child( + div() + .text_color(if count > 0 { t.secondary } else { t.foreground.opacity(0.4) }) + .child(records_label(count)), + ) + .child( + icon_button( + SharedString::from(format!("edit-cat-{}", c.id)), + IconName::Pencil, + "Edit category", + ButtonVariant::Secondary, + cx, + ) + .on_click(cx.listener(move |this, _, window, cx| { + this.open_edit(edit.clone(), window, cx) + })), + ) + .child( + icon_button( + SharedString::from(format!("del-cat-{}", c.id)), + IconName::Trash, + "Delete category", + ButtonVariant::Danger, + cx, + ) + .on_click(cx.listener(move |this, _, _, cx| this.delete(id.clone(), cx))), + ), + ); + } + list.into_any_element() + }; + div() + .flex_1() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(div().text_size(rems(0.875)).font_weight(gpui_kit::FontWeight::BOLD).child(title.to_string())) + .child(body) + } +} + +pub(super) fn parse_hex(value: &str) -> Option { + let hex = value.trim().trim_start_matches('#'); + if hex.len() != 6 { + return None; + } + u32::from_str_radix(hex, 16).ok().map(|v| gpui_kit::rgb(v).into()) +} + +impl Render for CategoriesScreen { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let add = button("add-category", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))); + let expenses = self.section("Expenses", "expense", cx); + let income = self.section("Income", "income", cx); + div() + .id("categories") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Categories", add, cx)) + .child(div().flex().gap(rems(1.5)).child(expenses).child(income)) + .children(self.form.clone()) + } +} + +pub struct CategoryForm { + db: Arc, + editing: Option, + name: Entity, + kind: Entity, + selected_icon: String, + _subs: Vec, +} + +impl EventEmitter for CategoryForm {} + +impl CategoryForm { + fn new(db: Arc, editing: Option, window: &mut Window, cx: &mut Context) -> Self { + let (name, kind, icon) = match &editing { + Some(c) => (c.name.clone(), c.category_type.clone(), c.icon.clone().unwrap_or_else(|| DEFAULT_ICON.into())), + None => (String::new(), "expense".into(), DEFAULT_ICON.into()), + }; + let name_input = text_input_with("e.g. Groceries", &name, window, cx); + name_input.update(cx, |s, cx| s.focus(window, cx)); + let _subs = submit_on_enter(&[&name_input], Self::submit, cx); + Self { + db, + editing: editing.map(|c| c.id), + name: name_input, + kind: choice( + vec![("expense".into(), "Expense".into()), ("income".into(), "Income".into())], + Some(&kind), + window, + cx, + ), + selected_icon: icon, + _subs, + } + } + + fn submit(&mut self, cx: &mut Context) { + let name = text_of(&self.name, cx); + if name.is_empty() { + return; + } + let category_type = choice_value(&self.kind, cx).unwrap_or_else(|| "expense".into()); + let result = match &self.editing { + None => commands::create_category( + &self.db, + CreateCategoryRequest { + name: name.clone(), + parent_id: None, + category_type, + icon: Some(self.selected_icon.clone()), + color: None, + }, + ) + .map(|_| format!("Added category {name}")), + Some(id) => commands::update_category( + &self.db, + UpdateCategoryRequest { + id: id.clone(), + name: Some(name.clone()), + parent_id: None, + category_type: Some(category_type), + icon: Some(Some(self.selected_icon.clone())), + color: None, + }, + ) + .map(|_| format!("Saved category {name}")), + }; + match result { + Ok(message) => cx.emit(FormEvent::Saved(message)), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not save category: {e}"))), + } + } +} + +impl Render for CategoryForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let picker = div().flex().flex_wrap().gap(rems(0.25)).children(CATEGORY_ICONS.iter().map(|(name, label)| { + let selected = self.selected_icon == *name; + let name_owned = name.to_string(); + let fg = if selected { t.accent } else { t.foreground }; + button( + SharedString::from(format!("pick-icon-{name}")), + "", + if selected { ButtonVariant::Primary } else { ButtonVariant::Secondary }, + cx, + ) + .accessibility_label(*label) + .p(rems(0.375)) + .child(category_icon(name, px(16.), fg)) + .on_click(cx.listener(move |this, _, _, cx| { + this.selected_icon = name_owned.clone(); + cx.notify(); + })) + })); + let editing = self.editing.is_some(); + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Name", input("category-name", &self.name, window, cx), cx)) + .child(field("Type", select("category-type", &self.kind, window, cx), cx)) + .child(field("Icon", picker, cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("category-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel))), + ) + .child( + button("category-submit", if editing { "Save" } else { "Add" }, ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), + ), + ); + let _ = icon; + modal( + if editing { "Edit Category" } else { "Add Category" }, + body, + cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), + cx, + ).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext) -> (Entity, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + cx.add_window_view(|_, _| CategoriesScreen::new(db)) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + #[test] + fn counts_records_per_category() { + let tx = |cat: Option<&str>| Transaction { + id: "t".into(), + account_id: "a".into(), + category_id: cat.map(String::from), + transaction_type: "expense".into(), + amount: 1, + currency: "USD".into(), + description: "d".into(), + merchant: None, + notes: None, + transaction_date: "2026-01-01".into(), + created_at: String::new(), + updated_at: String::new(), + }; + let counts = record_counts(&[tx(Some("a")), tx(Some("a")), tx(Some("b")), tx(None)]); + assert_eq!(counts["a"], 2); + assert_eq!(counts["b"], 1); + assert_eq!(counts.len(), 2); + assert_eq!(records_label(1), "1 record"); + assert_eq!(records_label(0), "0 records"); + } + + #[test] + fn parses_hex_colors() { + assert!(parse_hex("#7aa2f7").is_some()); + assert!(parse_hex("7aa2f7").is_some()); + assert!(parse_hex("nope").is_none()); + assert!(parse_hex("#fff").is_none()); + } + + #[gpui_kit::test] + fn lists_seeded_categories_with_counts(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.categories().len(), 8); + assert!(s.counts.values().sum::() > 0, "seeded transactions reference categories"); + }); + } + + #[gpui_kit::test] + fn add_category_saves(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = cx.update(|_, cx| screen.read(cx).form.clone().expect("form open")); + let name = cx.update(|_, cx| form.read(cx).name.clone()); + type_into(&name, "Pets", cx); + cx.update(|_, cx| form.update(cx, |f, cx| { + f.selected_icon = "dog".into(); + f.submit(cx) + })); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_form()); + let added = s.categories().iter().find(|c| c.name == "Pets").expect("saved"); + assert_eq!(added.icon.as_deref(), Some("dog")); + assert_eq!(added.category_type, "expense"); + }); + } + + #[gpui_kit::test] + fn edit_category_updates_in_place(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + let original = cx.update(|_, cx| screen.read(cx).categories()[0].clone()); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_edit(original.clone(), window, cx))); + let form = cx.update(|_, cx| screen.read(cx).form.clone().expect("form open")); + let name = cx.update(|_, cx| form.read(cx).name.clone()); + cx.update(|_, cx| assert_eq!(name.read(cx).value().as_ref(), original.name)); + type_into(&name, "Renamed", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.categories().len(), 8, "no duplicate created"); + assert_eq!(s.categories().iter().filter(|c| c.id == original.id).next().unwrap().name, "Renamed"); + }); + } + + #[gpui_kit::test] + fn blank_name_keeps_form_open(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = cx.update(|_, cx| screen.read(cx).form.clone().unwrap()); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_form()); + assert_eq!(screen.read(cx).categories().len(), 8); + }); + } + + #[gpui_kit::test] + fn delete_removes_category(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + let id = cx.update(|_, cx| screen.read(cx).categories()[0].id.clone()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.delete(id.clone(), cx))); + cx.update(|_, cx| assert!(screen.read(cx).categories().iter().all(|c| c.id != id))); + } +} diff --git a/src/ui/csv_import.rs b/src/ui/csv_import.rs new file mode 100644 index 0000000..985e081 --- /dev/null +++ b/src/ui/csv_import.rs @@ -0,0 +1,697 @@ +use super::widgets::*; +use chelete_lib::commands::{self, Account, Category, ImportOptions}; +use chelete_lib::database::DbState; +use chelete_lib::format::format_money; +use chelete_lib::import::session::{ + flag_duplicates, is_valid_for_preview, set_column, type_label, valid_count, SavedMappings, +}; +use chelete_lib::import::{parse_csv_preview, CsvField, CsvMapping, CsvPreview, ImportResult}; +use gpui_kit::base::input::InputState; +use gpui_kit::base::CheckboxState; +use gpui_kit::{ + div, prelude::*, rems, Context, Entity, EventEmitter, IntoElement, PathPromptOptions, Render, + Subscription, Window, +}; +use gpui_omarchy::{alert, button, input, select, ActiveTheme as _, ButtonVariant, ChoiceState, Status}; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Stage { + Mapping, + Preview, + Result, +} + +#[derive(Clone, Debug)] +pub enum CsvImportEvent { + /// Closed after an import ran; the list should reload. + Done, + Cancel, +} + +fn field_options() -> Vec<(String, String)> { + let mut items = vec![(String::new(), "— not mapped —".to_string())]; + items.extend( + CsvField::all() + .iter() + .map(|f| (field_value(*f).to_string(), f.label().to_string())), + ); + items +} + +fn field_value(field: CsvField) -> &'static str { + match field { + CsvField::Date => "date", + CsvField::Description => "description", + CsvField::Merchant => "merchant", + CsvField::Amount => "amount", + CsvField::IncomeAmount => "income_amount", + CsvField::ExpenseAmount => "expense_amount", + CsvField::Type => "type", + CsvField::Category => "category", + CsvField::Account => "account", + CsvField::Currency => "currency", + CsvField::Notes => "notes", + CsvField::Ignore => "ignore", + } +} + +fn field_from_value(value: &str) -> Option { + CsvField::all().iter().copied().find(|f| field_value(*f) == value) +} + +pub struct CsvImportDialog { + db: Arc, + stage: Stage, + path: Option, + mapping: CsvMapping, + preview: Option, + duplicates: Vec, + result: Option, + error: Option, + skip_duplicates: bool, + column_choices: Vec<(String, Entity)>, + account: Entity, + category: Entity, + default_type: Entity, + debit_alias: Entity, + credit_alias: Entity, + currency: Entity, + accounts: Vec, + categories: Vec, + saved: SavedMappings, + saved_path: PathBuf, + syncing: bool, + focus: ModalFocus, + _subs: Vec, +} + +impl EventEmitter for CsvImportDialog {} + +impl CsvImportDialog { + pub fn new( + db: Arc, + accounts: Vec, + categories: Vec, + saved_path: PathBuf, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut account_items = vec![(String::new(), "None".to_string())]; + account_items.extend(accounts.iter().map(|a| (a.id.clone(), a.name.clone()))); + let mut category_items = vec![(String::new(), "None".to_string())]; + category_items.extend(categories.iter().map(|c| (c.id.clone(), format!("{} ({})", c.name, c.category_type)))); + let type_items = || vec![("expense".to_string(), "Expense".to_string()), ("income".to_string(), "Income".to_string())]; + let account = choice(account_items, Some(""), window, cx); + let category = choice(category_items, Some(""), window, cx); + let default_type = choice( + vec![(String::new(), "None".into()), ("expense".into(), "Expense".into()), ("income".into(), "Income".into())], + Some(""), + window, + cx, + ); + let debit_alias = choice(type_items(), Some("expense"), window, cx); + let credit_alias = choice(type_items(), Some("income"), window, cx); + let currency = text_input_with("USD", "USD", window, cx); + let mut subs = Vec::new(); + for c in [&account, &category, &default_type, &debit_alias, &credit_alias] { + subs.push(cx.observe(c, |this, _, cx| this.read_controls(cx))); + } + subs.push(cx.observe(¤cy, |this, _, cx| this.read_controls(cx))); + Self { + saved: SavedMappings::load_from(&saved_path), + saved_path, + db, + stage: Stage::Mapping, + path: None, + mapping: CsvMapping::default(), + preview: None, + duplicates: Vec::new(), + result: None, + error: None, + skip_duplicates: true, + column_choices: Vec::new(), + account, + category, + default_type, + debit_alias, + credit_alias, + currency, + accounts, + categories, + syncing: false, + focus: ModalFocus::new(cx), + _subs: subs, + } + } + + #[cfg(test)] + pub fn stage(&self) -> Stage { + self.stage + } + + /// Ask for a file with the system dialog, then load it. + pub fn choose_file(&mut self, window: &mut Window, cx: &mut Context) { + let picked = cx.prompt_for_paths(PathPromptOptions { files: true, directories: false, multiple: false, prompt: None }); + cx.spawn_in(window, async move |this, cx| { + if let Ok(Ok(Some(paths))) = picked.await { + if let Some(path) = paths.into_iter().next() { + this.update_in(cx, |this, window, cx| this.load_file(&path.to_string_lossy(), window, cx)).ok(); + } + } + }) + .detach(); + } + + pub fn load_file(&mut self, path: &str, window: &mut Window, cx: &mut Context) { + self.error = None; + // A first pass with no mapping just reads the headers. + let headers = match parse_csv_preview(path, &CsvMapping::default()) { + Ok(p) => p.headers, + Err(e) => { + self.error = Some(e); + return cx.notify(); + } + }; + self.path = Some(path.to_string()); + self.mapping = self.saved.get(&headers).cloned().unwrap_or_else(|| CsvMapping::from_headers(&headers)); + self.column_choices = headers + .iter() + .map(|h| { + let current = self.mapping.columns.get(h).map(|f| field_value(*f).to_string()).unwrap_or_default(); + let state = choice(field_options(), Some(¤t), window, cx); + let header = h.clone(); + self._subs.push(cx.observe(&state, move |this, _, cx| this.column_changed(&header, cx))); + (h.clone(), state) + }) + .collect(); + self.apply_mapping_to_controls(window, cx); + self.refresh_preview(); + cx.notify(); + } + + fn column_changed(&mut self, header: &str, cx: &mut Context) { + if self.syncing { + return; + } + let Some((_, state)) = self.column_choices.iter().find(|(h, _)| h == header) else { + return; + }; + let field = choice_value(state, cx).and_then(|v| field_from_value(&v)); + set_column(&mut self.mapping, header, field); + self.syncing = true; + // Assigning a field releases it from any other column; reflect that. + for (h, state) in &self.column_choices { + let want = self.mapping.columns.get(h).map(|f| field_value(*f)).unwrap_or(""); + if choice_value(state, cx).as_deref().unwrap_or("") != want { + let index = field_options().iter().position(|(v, _)| v == want); + state.update(cx, |s, cx| s.set_selected(index, cx)); + } + } + self.syncing = false; + cx.notify(); + } + + /// Copy the mapping's defaults into the controls (after loading a saved mapping). + fn apply_mapping_to_controls(&mut self, window: &mut Window, cx: &mut Context) { + self.syncing = true; + let m = &self.mapping; + let index_of = |items: Vec, wanted: &str| items.iter().position(|v| v == wanted); + let accounts: Vec = std::iter::once(String::new()).chain(self.accounts.iter().map(|a| a.id.clone())).collect(); + let categories: Vec = std::iter::once(String::new()).chain(self.categories.iter().map(|c| c.id.clone())).collect(); + let pick = |state: &Entity, idx: Option, cx: &mut Context| state.update(cx, |s, cx| s.set_selected(idx, cx)); + pick(&self.account, index_of(accounts, m.default_account_id.as_deref().unwrap_or("")), cx); + pick(&self.category, index_of(categories, m.default_category_id.as_deref().unwrap_or("")), cx); + pick( + &self.default_type, + index_of(vec![String::new(), "expense".into(), "income".into()], m.default_transaction_type.as_deref().unwrap_or("")), + cx, + ); + let alias = |key: &str, default: &str| m.type_aliases.get(key).cloned().unwrap_or_else(|| default.to_string()); + pick(&self.debit_alias, index_of(vec!["expense".into(), "income".into()], &alias("debit", "expense")), cx); + pick(&self.credit_alias, index_of(vec!["expense".into(), "income".into()], &alias("credit", "income")), cx); + let currency = m.default_currency.clone().unwrap_or_else(|| "USD".into()); + self.currency.update(cx, |s, cx| s.set_value(currency, window, cx)); + self.syncing = false; + } + + /// Controls -> mapping defaults. + fn read_controls(&mut self, cx: &mut Context) { + if self.syncing { + return; + } + let blank = |v: Option| v.filter(|s| !s.is_empty()); + self.mapping.default_account_id = blank(choice_value(&self.account, cx)); + self.mapping.default_category_id = blank(choice_value(&self.category, cx)); + self.mapping.default_transaction_type = blank(choice_value(&self.default_type, cx)); + let currency = text_of(&self.currency, cx).to_uppercase(); + self.mapping.default_currency = Some(if currency.is_empty() { "USD".into() } else { currency }); + for (key, state) in [("debit", &self.debit_alias), ("credit", &self.credit_alias)] { + if let Some(to) = choice_value(state, cx) { + self.mapping.type_aliases.insert(key.to_string(), to); + } + } + cx.notify(); + } + + fn refresh_preview(&mut self) { + let Some(path) = self.path.clone() else { return }; + match parse_csv_preview(&path, &self.mapping) { + Ok(preview) => { + self.duplicates = flag_duplicates(&preview.rows, &self.mapping); + self.preview = Some(preview); + } + Err(e) => self.error = Some(e), + } + } + + pub fn is_ready(&self) -> bool { + self.path.is_some() && is_valid_for_preview(&self.mapping) + } + + pub fn show_preview(&mut self, cx: &mut Context) { + if !self.is_ready() { + return; + } + self.error = None; + self.refresh_preview(); + if self.error.is_none() { + if let Some(p) = &self.preview { + self.saved.insert(&p.headers, self.mapping.clone()); + // Remembering the mapping is a convenience; failing to write it must not block the import. + let _ = self.saved.save_to(&self.saved_path); + } + self.stage = Stage::Preview; + } + cx.notify(); + } + + pub fn import(&mut self, cx: &mut Context) { + let Some(path) = self.path.clone() else { return }; + let options = ImportOptions { + default_account_id: self.mapping.default_account_id.clone(), + default_category_id: self.mapping.default_category_id.clone(), + default_currency: self.mapping.default_currency.clone(), + skip_duplicates: self.skip_duplicates, + }; + match commands::import_transactions(&self.db, path, self.mapping.clone(), options) { + Ok(result) => { + self.result = Some(result); + self.error = None; + self.stage = Stage::Result; + } + Err(e) => self.error = Some(e), + } + cx.notify(); + } + + pub fn ready_count(&self) -> usize { + self.preview.as_ref().map_or(0, |p| valid_count(&p.rows, &self.duplicates)) + } + + fn mapping_stage(&self, window: &mut Window, cx: &mut Context) -> gpui_kit::Div { + let t = cx.omarchy().clone(); + let has_type = self.mapping.field_column(CsvField::Type).is_some(); + let separate = self.mapping.field_column(CsvField::IncomeAmount).is_some() || self.mapping.field_column(CsvField::ExpenseAmount).is_some(); + + let file_row = div() + .flex() + .items_center() + .justify_between() + .gap(rems(0.75)) + .child(match &self.path { + Some(p) => div().text_color(t.secondary).overflow_hidden().child(p.clone()).into_any_element(), + None => div().text_color(t.secondary).child("No file chosen").into_any_element(), + }) + .child( + button("csv-choose", if self.path.is_some() { "Change" } else { "Choose CSV File" }, ButtonVariant::Outline, cx) + .on_click(cx.listener(|this, _, window, cx| this.choose_file(window, cx))), + ); + + let mut body = div().flex().flex_col().gap(rems(0.75)).child(file_row); + if !self.column_choices.is_empty() { + let mut table = div().id("csv-columns").flex().flex_col().max_h(rems(18.)).overflow_y_scroll().border_1().border_color(t.border).child( + div().flex().bg(t.surface).font_weight(gpui_kit::FontWeight::BOLD).child(cell(None, "CSV Column")).child(cell(None, "Chelete Field")), + ); + let choices = self.column_choices.clone(); + for (i, (header, state)) in choices.iter().enumerate() { + table = table.child( + div() + .flex() + .items_center() + .py(rems(0.125)) + .border_t_1() + .border_color(t.border) + .child(cell(None, header.clone())) + .child(cell(None, select(("csv-col", i), state, window, cx))), + ); + } + body = body.child(table); + + let mut defaults = div() + .flex() + .flex_wrap() + .gap(rems(0.5)) + .child(field("Default Account", select("csv-account", &self.account, window, cx), cx)) + .child(field("Default Category", select("csv-category", &self.category, window, cx), cx)) + .child(field("Default Currency", input("csv-currency", &self.currency, window, cx), cx)) + .child(field("Default Type", select("csv-type", &self.default_type, window, cx), cx)); + if has_type { + defaults = defaults + .child(field("Debit means", select("csv-debit", &self.debit_alias, window, cx), cx)) + .child(field("Credit means", select("csv-credit", &self.credit_alias, window, cx), cx)); + } + body = body.child(defaults).child(dim( + if separate { + "Map at least Date and Description plus an amount. Separate Income and Expense columns also satisfy the type requirement." + } else { + "Map at least Date, Description, and an Amount + Type field to continue." + }, + cx, + )); + } + body.child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("csv-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)))) + .child( + button("csv-preview", "Preview", ButtonVariant::Primary, cx) + .disabled(!self.is_ready()) + .on_click(cx.listener(|this, _, _, cx| this.show_preview(cx))), + ), + ) + } + + fn preview_stage(&self, cx: &mut Context) -> gpui_kit::Div { + let t = cx.omarchy().clone(); + let Some(preview) = &self.preview else { + return div(); + }; + let mut table = div().id("csv-preview-rows").flex().flex_col().max_h(rems(20.)).overflow_y_scroll().border_1().border_color(t.border).child( + div() + .flex() + .bg(t.surface) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(6.), "Date")) + .child(cell(None, "Description")) + .child(cell(Some(7.), "Amount")) + .child(cell(Some(5.), "Type")) + .child(cell(Some(8.), "Account")) + .child(cell(Some(8.), "Category")) + .child(cell(Some(10.), "Status")), + ); + for (row, dup) in preview.rows.iter().zip(&self.duplicates) { + let status = if !row.errors.is_empty() { + div().text_color(t.danger).child(row.errors.join("; ")) + } else if *dup { + div().text_color(t.warning).child("Duplicate") + } else { + div().text_color(t.success).child("OK") + }; + table = table.child( + div() + .flex() + .py(rems(0.125)) + .border_t_1() + .border_color(t.border) + .child(cell(Some(6.), row.date.clone().unwrap_or_else(|| "—".into()))) + .child(cell(None, row.description.clone().unwrap_or_else(|| "—".into()))) + .child(cell(Some(7.), row.amount_cents.map(format_money).unwrap_or_else(|| "—".into()))) + .child(cell(Some(5.), type_label(row))) + .child(cell(Some(8.), row.account.clone().unwrap_or_else(|| if self.mapping.default_account_id.is_some() { "(default)".into() } else { "—".into() }))) + .child(cell(Some(8.), row.category.clone().unwrap_or_else(|| if self.mapping.default_category_id.is_some() { "(default)".into() } else { "—".into() }))) + .child(cell(Some(10.), status)), + ); + } + let ready = self.ready_count(); + let screen = cx.entity(); + div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(dim(format!("{} preview rows (of {} total)", preview.rows.len(), preview.total_rows), cx)) + .child(table) + .child(check( + "csv-skip-dupes", + "Skip exact duplicates (same date, amount, description, account)", + if self.skip_duplicates { CheckboxState::Checked } else { CheckboxState::Unchecked }, + move |_, cx| screen.update(cx, |s, cx| { s.skip_duplicates = !s.skip_duplicates; cx.notify(); }), + cx, + )) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("csv-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)))) + .child(button("csv-back", "Back", ButtonVariant::Outline, cx).on_click(cx.listener(|this, _, _, cx| { this.stage = Stage::Mapping; cx.notify(); }))) + .child( + button("csv-import", format!("Import {ready} Transactions"), ButtonVariant::Primary, cx) + .disabled(ready == 0) + .on_click(cx.listener(|this, _, _, cx| this.import(cx))), + ), + ) + } + + fn result_stage(&self, cx: &mut Context) -> gpui_kit::Div { + let t = cx.omarchy().clone(); + let Some(result) = &self.result else { + return div(); + }; + let stat = |value: usize, label: &str, color| { + div().flex_1().flex().flex_col().items_center().child(div().text_size(rems(1.5)).text_color(color).child(value.to_string())).child(dim(label.to_string(), cx)) + }; + let mut body = div().flex().flex_col().gap(rems(0.75)).child( + div() + .flex() + .child(stat(result.imported, "Imported", t.success)) + .child(stat(result.skipped, "Skipped", t.secondary)) + .child(stat(result.errors.len(), "Errors", if result.errors.is_empty() { t.secondary } else { t.danger })), + ); + if !result.errors.is_empty() { + body = body.child( + div() + .id("csv-errors") + .max_h(rems(10.)) + .overflow_y_scroll() + .flex() + .flex_col() + .gap(rems(0.25)) + .children(result.errors.iter().map(|e| div().text_color(t.danger).child(e.clone()))), + ); + } + body.child( + div().flex().justify_end().gap(rems(0.5)).child(button("csv-done", "Done", ButtonVariant::Primary, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Done)))), + ) + } +} + +impl Render for CsvImportDialog { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.focus.ensure(window, cx); + let error = self.error.clone().map(|e| alert(e, Status::Error, cx)); + let stage = match self.stage { + Stage::Mapping => self.mapping_stage(window, cx), + Stage::Preview => self.preview_stage(cx), + Stage::Result => self.result_stage(cx), + }; + let body = div().flex().flex_col().gap(rems(0.75)).children(error).child(stage); + modal_sized( + 52., + "Import Transactions from CSV", + body, + cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)), + cx, + ).track_focus(&self.focus.handle) + .on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(CsvImportEvent::Cancel))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::{TestAppContext, VisualTestContext}; + + struct Env { + dir: PathBuf, + } + + impl Env { + fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("chelete-csv-ui-{name}-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + Self { dir } + } + fn csv(&self, name: &str, body: &str) -> String { + let path = self.dir.join(name); + std::fs::write(&path, body).unwrap(); + path.to_string_lossy().to_string() + } + fn mappings(&self) -> PathBuf { + self.dir.join("mappings.json") + } + } + + impl Drop for Env { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.dir).ok(); + } + } + + fn setup<'a>(cx: &'a mut TestAppContext, env: &Env) -> (Entity, Arc, &'a mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let accounts = commands::get_accounts(&db).unwrap(); + let categories = commands::get_categories(&db).unwrap(); + let path = env.mappings(); + let (dialog, vcx) = cx.add_window_view(move |window, cx| CsvImportDialog::new(db, accounts, categories, path, window, cx)); + (dialog, handle, vcx) + } + + fn load(dialog: &Entity, path: &str, cx: &mut VisualTestContext) { + let path = path.to_string(); + cx.update(|window, cx| dialog.update(cx, |d, cx| d.load_file(&path, window, cx))); + } + + const BANK: &str = "Date,Payee,Debit,Credit,Account\n2026-03-01,Coffee,4.50,,Main\n2026-03-01,Coffee,4.50,,Main\n2026-03-02,Salary,,2000.00,Main\n"; + + #[gpui_kit::test] + fn recognises_a_bank_csv_and_imports_it(cx: &mut TestAppContext) { + let env = Env::new("bank"); + let (dialog, db, cx) = setup(cx, &env); + let account = commands::get_accounts(&db).unwrap().remove(0); + let before = commands::get_transactions(&db).unwrap().len(); + load(&dialog, &env.csv("bank.csv", BANK), cx); + + cx.update(|_, cx| { + let d = dialog.read(cx); + assert!(d.is_ready(), "Date/Payee/Debit/Credit are recognised without any clicks"); + assert_eq!(d.column_choices.len(), 5); + assert_eq!(d.stage(), Stage::Mapping); + }); + // The CSV names an account that does not exist, so fall back to a default. + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert_eq!(d.stage(), Stage::Preview); + assert_eq!(d.duplicates, vec![false, true, false]); + assert_eq!(d.ready_count(), 2, "the repeated coffee is held back"); + }); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.import(cx))); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert_eq!(d.stage(), Stage::Result); + let result = d.result.as_ref().unwrap(); + assert_eq!((result.imported, result.skipped), (2, 1)); + assert!(result.errors.is_empty(), "{:?}", result.errors); + }); + let after = commands::get_transactions(&db).unwrap(); + assert_eq!(after.len(), before + 2); + let salary = after.iter().find(|t| t.description == "Salary").expect("imported"); + assert_eq!((salary.amount, salary.transaction_type.as_str()), (200000, "income")); + assert_eq!(salary.account_id, account.id); + } + + #[gpui_kit::test] + fn without_skipping_duplicates_both_coffees_import(cx: &mut TestAppContext) { + let env = Env::new("nodedupe"); + let (dialog, db, cx) = setup(cx, &env); + let before = commands::get_transactions(&db).unwrap().len(); + load(&dialog, &env.csv("bank.csv", BANK), cx); + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| dialog.update(cx, |d, cx| { d.skip_duplicates = false; d.show_preview(cx); d.import(cx) })); + assert_eq!(commands::get_transactions(&db).unwrap().len(), before + 3); + } + + #[gpui_kit::test] + fn assigning_a_field_releases_it_from_the_other_column(cx: &mut TestAppContext) { + let env = Env::new("conflict"); + let (dialog, _db, cx) = setup(cx, &env); + load(&dialog, &env.csv("bank.csv", BANK), cx); + let payee = cx.update(|_, cx| dialog.read(cx).column_choices.iter().find(|(h, _)| h == "Payee").unwrap().1.clone()); + let date_index = field_options().iter().position(|(v, _)| v == "date").unwrap(); + cx.update(|_, cx| payee.update(cx, |c, cx| c.set_selected(Some(date_index), cx))); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert_eq!(d.mapping.field_column(CsvField::Date), Some("Payee")); + assert_eq!(d.mapping.columns.get("Date"), None, "old date column released"); + let released = d.column_choices.iter().find(|(h, _)| h == "Date").unwrap(); + assert_eq!(choice_value(&released.1, cx).as_deref(), Some(""), "its dropdown shows unmapped"); + assert!(!d.is_ready(), "description is now unmapped"); + }); + } + + #[gpui_kit::test] + fn cannot_preview_until_the_mapping_is_complete(cx: &mut TestAppContext) { + let env = Env::new("incomplete"); + let (dialog, _db, cx) = setup(cx, &env); + load(&dialog, &env.csv("odd.csv", "foo,bar\n1,2\n"), cx); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert!(!d.is_ready()); + }); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + cx.update(|_, cx| assert_eq!(dialog.read(cx).stage(), Stage::Mapping)); + } + + #[gpui_kit::test] + fn a_missing_file_shows_an_error_and_keeps_going(cx: &mut TestAppContext) { + let env = Env::new("missing"); + let (dialog, _db, cx) = setup(cx, &env); + load(&dialog, "/nonexistent/file.csv", cx); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert!(d.error.is_some()); + assert!(d.path.is_none()); + assert!(!d.is_ready()); + }); + } + + #[gpui_kit::test] + fn rows_with_bad_data_are_reported_not_imported(cx: &mut TestAppContext) { + let env = Env::new("baddata"); + let (dialog, db, cx) = setup(cx, &env); + let before = commands::get_transactions(&db).unwrap().len(); + let csv = env.csv("bad.csv", "Date,Description,Amount,Type\n2026-03-01,Fine,10.00,expense\nnot-a-date,Broken,5.00,expense\n"); + load(&dialog, &csv, cx); + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + cx.update(|_, cx| assert_eq!(dialog.read(cx).ready_count(), 1)); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.import(cx))); + assert_eq!(commands::get_transactions(&db).unwrap().len(), before + 1); + } + + #[gpui_kit::test] + fn the_mapping_is_remembered_for_the_same_layout(cx: &mut TestAppContext) { + let env = Env::new("remember"); + let (dialog, db, cx) = setup(cx, &env); + load(&dialog, &env.csv("bank.csv", BANK), cx); + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(2), cx))); + let chosen = cx.update(|_, cx| dialog.read(cx).mapping.default_account_id.clone()); + assert!(chosen.is_some()); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + assert!(env.mappings().exists(), "mapping written on preview"); + + // A second dialog on a same-shaped file starts from the saved choices. + let (db2, accounts, categories) = (db.clone(), commands::get_accounts(&db).unwrap(), commands::get_categories(&db).unwrap()); + let path = env.mappings(); + let second = cx.update(|window, cx| cx.new(|cx| CsvImportDialog::new(db2, accounts, categories, path, window, cx))); + load(&second, &env.csv("bank-again.csv", BANK), cx); + cx.update(|_, cx| { + let d = second.read(cx); + assert!(d.is_ready()); + assert!(d.mapping.default_account_id.is_some(), "default account restored from the saved mapping"); + assert_eq!(choice_value(&d.account, cx).as_deref(), d.mapping.default_account_id.as_deref()); + }); + } +} diff --git a/src/ui/date_field.rs b/src/ui/date_field.rs new file mode 100644 index 0000000..2233544 --- /dev/null +++ b/src/ui/date_field.rs @@ -0,0 +1,43 @@ +//! Thin wrappers over gpui-kit's date picker so screens deal in `NaiveDate`. +use chrono::NaiveDate; +use gpui_kit::component::calendar::Date; +use gpui_kit::component::date_picker::DatePickerState; +use gpui_kit::{prelude::*, App, Context, Entity, Window}; + +pub const FORMAT: &str = "%Y-%m-%d"; + +pub fn single( + initial: Option, + window: &mut Window, + cx: &mut Context, +) -> Entity { + cx.new(|cx| { + let mut state = DatePickerState::new(window, cx).date_format(FORMAT); + if let Some(date) = initial { + state.set_date(Date::Single(Some(date)), window, cx); + } + state + }) +} + +pub fn range(window: &mut Window, cx: &mut Context) -> Entity { + cx.new(|cx| DatePickerState::range(window, cx).date_format(FORMAT).number_of_months(2)) +} + +pub fn picked(state: &Entity, cx: &App) -> Option { + match state.read(cx).date() { + Date::Single(d) => d, + Date::Range(start, _) => start, + } +} + +pub fn picked_range(state: &Entity, cx: &App) -> (Option, Option) { + match state.read(cx).date() { + Date::Range(a, b) => (a, b), + Date::Single(d) => (d, d), + } +} + +pub fn parse_iso(value: &str) -> Option { + NaiveDate::parse_from_str(value, FORMAT).ok() +} diff --git a/src/ui/date_filter_control.rs b/src/ui/date_filter_control.rs new file mode 100644 index 0000000..70239bd --- /dev/null +++ b/src/ui/date_filter_control.rs @@ -0,0 +1,77 @@ +//! Preset dropdown plus a custom date range, shared by list screens. +use super::date_field; +use super::widgets::{choice, choice_value}; +use chelete_lib::date_filter::{date_range, DateFilter}; +use chrono::{Datelike, Duration, NaiveDate}; +use gpui_kit::component::date_picker::{DatePicker, DatePickerState, DateRangePreset}; +use gpui_kit::{div, prelude::*, rems, App, Context, Div, Entity, Subscription, Window}; +use gpui_omarchy::{select, ChoiceState}; + +pub struct DateFilterControl { + pub preset: Entity, + pub range: Entity, +} + +impl DateFilterControl { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let items = DateFilter::ALL_FILTERS.iter().map(|f| (f.value().to_string(), f.label().to_string())).collect(); + Self { + preset: choice(items, Some(DateFilter::All.value()), window, cx), + range: date_field::range(window, cx), + } + } + + pub fn filter(&self, cx: &App) -> DateFilter { + choice_value(&self.preset, cx) + .and_then(|v| DateFilter::from_value(&v)) + .unwrap_or(DateFilter::All) + } + + /// Inclusive ISO `(start, end)` for the current selection. + pub fn bounds(&self, today: NaiveDate, cx: &App) -> (String, String) { + let (start, end) = date_field::picked_range(&self.range, cx); + date_range(self.filter(cx), start, end, today) + } + + /// Re-render `owner` whenever the selection changes. + pub fn observe(&self, cx: &mut Context, on_change: impl Fn(&mut V, &mut Context) + Clone + 'static) -> Vec { + let a = on_change.clone(); + vec![ + cx.observe(&self.preset, move |this, _, cx| a(this, cx)), + cx.observe(&self.range, move |this, _, cx| on_change(this, cx)), + ] + } + + pub fn render(&self, today: NaiveDate, window: &mut Window, cx: &mut App) -> Div { + let custom = self.filter(cx) == DateFilter::Custom; + let presets = range_presets(today); + div() + .flex() + .items_center() + .gap(rems(0.5)) + .child(div().w(rems(13.)).child(select("date-filter", &self.preset, window, cx))) + .when(custom, |row| { + row.child( + div().w(rems(22.)).child( + DatePicker::new(&self.range) + .presets(presets) + .number_of_months(2) + .placeholder("Start — End") + .cleanable(true), + ), + ) + }) + } +} + +fn range_presets(today: NaiveDate) -> Vec { + let first_of_month = today.with_day(1).unwrap(); + let last_month_end = first_of_month - Duration::days(1); + vec![ + DateRangePreset::range("Last 7 days", today - Duration::days(6), today), + DateRangePreset::range("Last 30 days", today - Duration::days(29), today), + DateRangePreset::range("This month", first_of_month, today), + DateRangePreset::range("Last month", last_month_end.with_day(1).unwrap(), last_month_end), + DateRangePreset::range("This year", NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap(), today), + ] +} diff --git a/src/ui/icons.rs b/src/ui/icons.rs new file mode 100644 index 0000000..db83c5b --- /dev/null +++ b/src/ui/icons.rs @@ -0,0 +1,99 @@ +//! Category icons. The database stores Lucide names; they are drawn straight +//! from the asset bundle. +use gpui_kit::{prelude::*, svg, Hsla, Pixels, Svg}; + +/// `(stored name, label)` in the order the picker shows them. +pub const CATEGORY_ICONS: &[(&str, &str)] = &[ + ("utensils", "Food"), + ("car", "Car"), + ("briefcase", "Work"), + ("film", "Film"), + ("shopping-bag", "Shopping"), + ("zap", "Bills"), + ("heart", "Health"), + ("book-open", "Education"), + ("coffee", "Coffee"), + ("plane", "Travel"), + ("home", "Home"), + ("shopping-cart", "Cart"), + ("wifi", "Internet"), + ("smartphone", "Phone"), + ("music", "Music"), + ("gamepad-2", "Gaming"), + ("gift", "Gift"), + ("stethoscope", "Medical"), + ("graduation-cap", "School"), + ("building-2", "Office"), + ("piggy-bank", "Savings"), + ("credit-card", "Credit"), + ("wallet", "Wallet"), + ("wrench", "Repairs"), + ("bot", "AI"), + ("code", "Code"), + ("laptop", "Laptop"), + ("tv", "Streaming"), + ("dumbbell", "Fitness"), + ("dog", "Dog"), + ("cat", "Cat"), + ("leaf", "Nature"), + ("shirt", "Clothing"), + ("scissors", "Grooming"), + ("bus", "Bus"), + ("train", "Train"), + ("hotel", "Hotel"), + ("wine", "Drinks"), + ("landmark", "Taxes"), + ("shield", "Insurance"), + ("cloud", "Cloud"), + ("circle-question-mark", "Unaccounted"), + ("user-round", "Woman"), +]; + +pub const DEFAULT_ICON: &str = "utensils"; + +/// Bundle file stem for a stored name. Two Lucide icons were renamed after +/// categories were first saved with the old names. +fn file_stem(name: &str) -> &str { + match name { + "home" => "house", + "train" => "train-front", + other => other, + } +} + +pub fn asset_path(name: &str) -> String { + format!("icons/{}.svg", file_stem(name)) +} + +pub fn category_icon(name: &str, size: Pixels, color: Hsla) -> Svg { + svg().path(asset_path(name)).size(size).flex_shrink_0().text_color(color) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui_kit::AssetSource as _; + + #[test] + fn every_category_icon_is_in_the_bundle() { + for (name, _) in CATEGORY_ICONS { + let loaded = gpui_kit::assets::AllAssets.load(&asset_path(name)).unwrap(); + assert!(loaded.is_some(), "icon {name} missing from the asset bundle"); + } + } + + #[test] + fn renamed_icons_resolve() { + assert_eq!(asset_path("home"), "icons/house.svg"); + assert_eq!(asset_path("train"), "icons/train-front.svg"); + assert_eq!(asset_path("utensils"), "icons/utensils.svg"); + } + + #[test] + fn names_are_unique() { + let mut names: Vec<_> = CATEGORY_ICONS.iter().map(|(n, _)| *n).collect(); + names.sort(); + names.dedup(); + assert_eq!(names.len(), CATEGORY_ICONS.len()); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..019b484 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,142 @@ +//! GPUI front end: app shell, sidebar, command palette and screens. +mod accounts; +mod categories; +mod csv_import; +mod date_field; +mod date_filter_control; +mod icons; +mod overview; +mod palette; +mod projections; +mod shell; +mod settings; +mod sidebar; +mod subscriptions; +mod theme_bridge; +mod transactions; +mod widgets; + +#[cfg(test)] +mod test_support; + +pub use shell::Shell; + +use gpui_kit::{actions, App, KeyBinding}; +use gpui_omarchy::IconName; + +actions!( + chelete, + [ + GoOverview, + GoTransactions, + GoAccounts, + GoCategories, + GoSubscriptions, + GoProjections, + GoSettings, + TogglePalette, + ToggleSidebar, + Quit, + PaletteNext, + PalettePrev, + PaletteDismiss, + ModalCancel, + ZoomIn, + ZoomOut, + ZoomReset, + ] +); + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum View { + Overview, + Transactions, + Accounts, + Categories, + Subscriptions, + Projections, + Settings, +} + +impl View { + pub const ALL: [View; 7] = [ + View::Overview, + View::Transactions, + View::Accounts, + View::Categories, + View::Subscriptions, + View::Projections, + View::Settings, + ]; + + pub fn label(self) -> &'static str { + match self { + View::Overview => "Overview", + View::Transactions => "Transactions", + View::Accounts => "Accounts", + View::Categories => "Categories", + View::Subscriptions => "Subscriptions", + View::Projections => "Projections", + View::Settings => "Settings", + } + } + + pub fn shortcut(self) -> &'static str { + match self { + View::Overview => "Ctrl+O", + View::Transactions => "Ctrl+T", + View::Accounts => "Ctrl+A", + View::Categories => "Ctrl+C", + View::Subscriptions => "Ctrl+U", + View::Projections => "Ctrl+P", + View::Settings => "Ctrl+S", + } + } + + pub fn icon(self) -> IconName { + match self { + View::Overview => IconName::LayoutDashboard, + View::Transactions => IconName::ArrowLeftRight, + View::Accounts => IconName::Wallet, + View::Categories => IconName::Tag, + View::Subscriptions => IconName::Repeat, + View::Projections => IconName::ChartLine, + View::Settings => IconName::Settings, + } + } +} + +/// Context the shell sets on its root element; shortcuts below only fire inside it, +/// so text inputs keep their own Ctrl+A / Ctrl+C / Ctrl+S behavior. +pub const SHELL_CONTEXT: &str = "Shell"; +pub const PALETTE_CONTEXT: &str = "Palette"; +pub const MODAL_CONTEXT: &str = "Modal"; + +pub fn install(cx: &mut App) { + bind_keys(cx); + theme_bridge::install(cx); +} + +pub fn bind_keys(cx: &mut App) { + let shell = Some(SHELL_CONTEXT); + cx.bind_keys([ + KeyBinding::new("ctrl-o", GoOverview, shell), + KeyBinding::new("ctrl-t", GoTransactions, shell), + KeyBinding::new("ctrl-a", GoAccounts, shell), + KeyBinding::new("ctrl-c", GoCategories, shell), + KeyBinding::new("ctrl-u", GoSubscriptions, shell), + KeyBinding::new("ctrl-p", GoProjections, shell), + KeyBinding::new("ctrl-s", GoSettings, shell), + KeyBinding::new("ctrl-k", TogglePalette, shell), + KeyBinding::new("ctrl-b", ToggleSidebar, shell), + KeyBinding::new("ctrl-=", ZoomIn, shell), + KeyBinding::new("ctrl-+", ZoomIn, shell), + KeyBinding::new("ctrl--", ZoomOut, shell), + KeyBinding::new("ctrl-0", ZoomReset, shell), + KeyBinding::new("ctrl-q", Quit, None), + KeyBinding::new("down", PaletteNext, Some(PALETTE_CONTEXT)), + KeyBinding::new("up", PalettePrev, Some(PALETTE_CONTEXT)), + KeyBinding::new("escape", PaletteDismiss, Some(PALETTE_CONTEXT)), + KeyBinding::new("escape", ModalCancel, Some(MODAL_CONTEXT)), + ]); +} diff --git a/src/ui/overview.rs b/src/ui/overview.rs new file mode 100644 index 0000000..b7c779b --- /dev/null +++ b/src/ui/overview.rs @@ -0,0 +1,417 @@ +use super::categories::parse_hex; +use super::date_filter_control::DateFilterControl; +use super::icons::category_icon; +use super::widgets::*; +use chelete_lib::commands::{self, Account, Category, Overview, Transaction}; +use chelete_lib::database::DbState; +use chelete_lib::format::{format_balance, format_money}; +use chelete_lib::overview::{filter_transactions, summarize, CategoryTotal, TypeFilter}; +use chelete_lib::paging::{page_slice, Pager}; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Div, Entity, Hsla, IntoElement, Render, SharedString, + Subscription, Window, +}; +use gpui_omarchy::chart::PieChart; +use gpui_omarchy::{alert, empty_state, panel, select, ActiveTheme as _, ChoiceState, Status}; +use std::sync::Arc; + +const PER_PAGE: usize = 10; + +pub struct OverviewScreen { + db: Arc, + overview: Result, + transactions: Vec, + accounts: Vec, + categories: Vec, + date_filter: DateFilterControl, + kind: Entity, + account: Entity, + page: usize, + _account_sub: Option, + _subs: Vec, +} + +impl OverviewScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let date_filter = DateFilterControl::new(window, cx); + let kind = choice( + vec![("all".into(), "All Types".into()), ("income".into(), "Income".into()), ("expense".into(), "Expense".into())], + Some("all"), + window, + cx, + ); + let account = choice(vec![("all".into(), "All Accounts".into())], Some("all"), window, cx); + let mut subs = date_filter.observe(cx, |this: &mut Self, cx| { + this.page = 1; + cx.notify(); + }); + subs.push(cx.observe(&kind, |this, _, cx| { + this.page = 1; + cx.notify(); + })); + let mut screen = Self { + db, + overview: Ok(empty_overview()), + transactions: Vec::new(), + accounts: Vec::new(), + categories: Vec::new(), + date_filter, + kind, + account, + page: 1, + _account_sub: None, + _subs: subs, + }; + screen.load(window, cx); + screen + } + + fn load(&mut self, window: &mut Window, cx: &mut Context) { + self.overview = commands::get_overview(&self.db); + self.transactions = commands::get_transactions(&self.db).unwrap_or_default(); + self.accounts = commands::get_accounts(&self.db).unwrap_or_default(); + self.categories = commands::get_categories(&self.db).unwrap_or_default(); + + // The account list can change between visits, so rebuild the dropdown + // and keep the previous choice if that account still exists. + let previous = choice_value(&self.account, cx).unwrap_or_else(|| "all".into()); + let mut items = vec![("all".to_string(), "All Accounts".to_string())]; + items.extend(self.accounts.iter().map(|a| (a.id.clone(), a.name.clone()))); + self.account = choice(items, Some(&previous), window, cx); + self._account_sub = Some(cx.observe(&self.account, |this, _, cx| { + this.page = 1; + cx.notify(); + })); + } + + pub fn reload(&mut self, window: &mut Window, cx: &mut Context) { + self.load(window, cx); + cx.notify(); + } + + #[cfg(test)] + pub fn overview(&self) -> Result<&Overview, &String> { + self.overview.as_ref() + } + + fn bounds(&self, cx: &gpui_kit::App) -> (String, String) { + self.date_filter.bounds(chrono::Local::now().date_naive(), cx) + } + + fn visible_transactions(&self, cx: &gpui_kit::App) -> Vec<&Transaction> { + let (start, end) = self.bounds(cx); + let kind = TypeFilter::from_value(&choice_value(&self.kind, cx).unwrap_or_default()); + let account = choice_value(&self.account, cx).filter(|a| a != "all"); + filter_transactions(&self.transactions, &start, &end, kind, account.as_deref()) + } +} + +fn empty_overview() -> Overview { + Overview { + total_balance: 0, + total_income: 0, + total_expenses: 0, + accounts: vec![], + recent_transactions: vec![], + category_spending: vec![], + } +} + +fn stat(title: &str, value: String, tint: Hsla, note: Option, cx: &gpui_kit::App) -> Div { + let card = panel(title.to_string(), cx) + .flex_1() + .child(div().text_size(rems(1.5)).text_color(tint).child(value)); + match note { + Some(n) => card.child(dim(n, cx)), + None => card, + } +} + +fn donut(title: &str, data: &[CategoryTotal], empty: &str, id: &'static str, cx: &gpui_kit::App) -> Div { + let t = cx.omarchy(); + let slices: Vec<(SharedString, f32, Hsla)> = data + .iter() + .enumerate() + .map(|(i, c)| { + let color = c.color.as_deref().and_then(parse_hex).unwrap_or(t.chart[i % t.chart.len()]); + (SharedString::from(c.name.clone()), c.value as f32 / 100.0, color) + }) + .collect(); + let body = if slices.is_empty() { + div().py(rems(1.)).text_color(t.secondary).child(empty.to_string()).into_any_element() + } else { + div() + .w_full() + .h(rems(15.)) + .child( + PieChart::new(slices) + .id(id) + .name("Total") + .value(|(_, v, _)| *v) + .color(|(_, _, c)| *c) + .inner_radius(48.) + .outer_radius(76.) + .pad_angle(0.02) + .label(|(name, v, _)| format!("{name} ${v:.0}").into()), + ) + .into_any_element() + }; + panel(title.to_string(), cx).flex_1().child(body) +} + +impl Render for OverviewScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let page = div().id("overview").relative().size_full().overflow_y_scroll().p(rems(1.5)).flex().flex_col().gap(rems(1.)); + + let overview = match &self.overview { + Ok(o) => o, + Err(e) => return page.child(alert(format!("Could not load overview: {e}"), Status::Error, cx)), + }; + + let (start, end) = self.bounds(cx); + let summary = summarize(&self.transactions, &self.categories, &start, &end); + let visible = self.visible_transactions(cx); + let pager = Pager::new(self.page, PER_PAGE, visible.len()); + let rows: Vec = page_slice(&visible, &pager).iter().map(|t| (*t).clone()).collect(); + let nothing_at_all = self.accounts.is_empty() && self.transactions.is_empty(); + drop(visible); + + let today = chrono::Local::now().date_naive(); + let header = page_header("Overview", div().text_color(t.secondary).child(today.format("%B %Y").to_string()), cx); + let filter_row = self.date_filter.render(today, window, cx); + + let net_tint = if summary.net >= 0 { t.success } else { t.danger }; + let stats = div() + .flex() + .gap(rems(1.)) + .child(stat("Total Balance", format_balance(overview.total_balance), t.foreground, None, cx)) + .child(stat("Income", format_money(summary.total_income), t.success, None, cx)) + .child(stat("Expenses", format_money(-summary.total_expenses), t.danger, None, cx)) + .child(stat("Net", format_money(summary.net), net_tint, Some(format!("Savings rate: {}%", summary.savings_rate)), cx)); + + let accounts = (!self.accounts.is_empty()).then(|| { + div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(div().font_weight(gpui_kit::FontWeight::BOLD).child("Accounts")) + .child(div().flex().flex_wrap().gap(rems(1.)).children(self.accounts.iter().map(|a| { + panel(a.name.clone(), cx).w(rems(14.)).child(div().text_size(rems(1.25)).child(format_balance(a.balance))) + }))) + }); + + let breakdown = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(div().font_weight(gpui_kit::FontWeight::BOLD).child("Breakdown")) + .child( + div() + .flex() + .gap(rems(1.)) + .child(donut("Income", &summary.income_by_category, "No income for this period", "overview-income", cx)) + .child(donut("Expenses", &summary.expenses_by_category, "No expenses for this period", "overview-expenses", cx)), + ); + + let list = if rows.is_empty() { + div().py(rems(1.)).text_color(t.secondary).child("No transactions match the selected filters.").into_any_element() + } else { + let mut table = div().flex().flex_col().border_1().border_color(t.border).child( + div() + .flex() + .py(rems(0.375)) + .bg(t.surface) + .text_color(t.bright) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(6.), "Date")) + .child(cell(None, "Description")) + .child(cell(None, "Category")) + .child(cell(Some(9.), div().flex().justify_end().child("Amount"))), + ); + for tx in rows { + let category = tx.category_id.as_ref().and_then(|id| self.categories.iter().find(|c| &c.id == id)); + let tint = if tx.transaction_type == "income" { t.success } else { t.danger }; + let amount = if tx.transaction_type == "expense" { -tx.amount } else { tx.amount }; + let date = chrono::NaiveDate::parse_from_str(&tx.transaction_date, "%Y-%m-%d") + .map(|d| d.format("%b %e").to_string()) + .unwrap_or_else(|_| tx.transaction_date.clone()); + table = table.child( + div() + .flex() + .items_center() + .py(rems(0.25)) + .border_t_1() + .border_color(t.border) + .child(cell(Some(6.), date)) + .child(cell(None, tx.description.clone())) + .child(cell(None, match category { + Some(c) => div() + .flex() + .items_center() + .gap(rems(0.25)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(12.), t.secondary))) + .child(c.name.clone()) + .into_any_element(), + None => dim("—", cx).into_any_element(), + })) + .child(cell(Some(9.), div().flex().justify_end().text_color(tint).child(format_money(amount)))), + ); + } + let screen = cx.entity(); + let (prev, next) = (screen.clone(), screen); + div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(table) + .child(simple_pager( + pager, + move |_, _, cx| prev.update(cx, |s, cx| { s.page = s.page.saturating_sub(1).max(1); cx.notify(); }), + move |_, _, cx| next.update(cx, |s, cx| { s.page += 1; cx.notify(); }), + cx, + )) + .into_any_element() + }; + + let recent = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(div().font_weight(gpui_kit::FontWeight::BOLD).child("Recent Transactions")) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(div().w(rems(13.)).child(select("overview-kind", &self.kind, window, cx))) + .child(div().w(rems(13.)).child(select("overview-account", &self.account, window, cx))), + ) + .child(list); + + let welcome = nothing_at_all.then(|| { + empty_state("Welcome to Chelete", "Press Ctrl+K to open the command palette and add your first account.", cx) + }); + + page.child(header) + .child(filter_row) + .child(stats) + .children(accounts) + .child(breakdown) + .child(recent) + .children(welcome) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{empty_db, init, seeded_db}; + use chelete_lib::date_filter::DateFilter; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext, db: Arc) -> (Entity, &mut VisualTestContext) { + init(cx); + cx.add_window_view(move |window, cx| OverviewScreen::new(db, window, cx)) + } + + fn pick(choice: &Entity, index: usize, cx: &mut VisualTestContext) { + cx.update(|_, cx| choice.update(cx, |c, cx| c.set_selected(Some(index), cx))); + } + + #[gpui_kit::test] + fn loads_totals_from_seeded_data(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + cx.update(|_, cx| { + let s = screen.read(cx); + let o = s.overview().expect("loads"); + assert_eq!(o.accounts.len(), 4); + assert_eq!(s.visible_transactions(cx).len(), 25, "All Time shows everything"); + let (start, end) = s.bounds(cx); + let summary = summarize(&s.transactions, &s.categories, &start, &end); + let sum = |kind: &str| s.transactions.iter().filter(|t| t.transaction_type == kind).map(|t| t.amount).sum::(); + // The summary follows the chosen range (all time here); the backend's own + // income/expense figures are current-month only and are not shown. + assert_eq!(summary.total_income, sum("income")); + assert_eq!(summary.total_expenses, sum("expense")); + let balance: i64 = s.accounts.iter().map(|a| a.balance).sum(); + assert_eq!(o.total_balance, balance, "total balance comes from the accounts"); + }); + } + + #[gpui_kit::test] + fn type_and_account_filters_narrow_the_table(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + let (kind, account) = cx.update(|_, cx| (screen.read(cx).kind.clone(), screen.read(cx).account.clone())); + pick(&kind, 1, cx); // Income + cx.update(|_, cx| { + let s = screen.read(cx); + let shown = s.visible_transactions(cx); + assert!(!shown.is_empty() && shown.iter().all(|t| t.transaction_type == "income")); + }); + pick(&kind, 0, cx); + pick(&account, 1, cx); // first real account + cx.update(|_, cx| { + let s = screen.read(cx); + let wanted = s.accounts[0].id.clone(); + let shown = s.visible_transactions(cx); + assert!(!shown.is_empty() && shown.iter().all(|t| t.account_id == wanted)); + }); + } + + #[gpui_kit::test] + fn date_filter_changes_the_summary_window(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + let preset = cx.update(|_, cx| screen.read(cx).date_filter.preset.clone()); + let index = DateFilter::ALL_FILTERS.iter().position(|f| *f == DateFilter::Today).unwrap(); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 2; cx.notify(); })); + pick(&preset, index, cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.page, 1); + let today = chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(); + assert!(s.visible_transactions(cx).iter().all(|t| t.transaction_date == today)); + }); + } + + #[gpui_kit::test] + fn pages_hold_ten_rows(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + cx.update(|_, cx| { + let s = screen.read(cx); + let pager = Pager::new(1, PER_PAGE, s.visible_transactions(cx).len()); + assert_eq!(pager.total_pages(), 3); + assert_eq!(pager.label(), "1–10 of 25"); + }); + } + + #[gpui_kit::test] + fn reload_picks_up_new_accounts_and_keeps_the_selection(cx: &mut TestAppContext) { + let db = seeded_db(); + let (screen, cx) = setup(cx, db.clone()); + let account = cx.update(|_, cx| screen.read(cx).account.clone()); + pick(&account, 2, cx); + let chosen = cx.update(|_, cx| choice_value(&screen.read(cx).account, cx)); + commands::create_account( + &db, + commands::CreateAccountRequest { name: "New".into(), account_type: "cash".into(), currency: "USD".into(), balance: 0, color: None, icon: None }, + ) + .unwrap(); + cx.update(|window, cx| screen.update(cx, |s, cx| s.reload(window, cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.accounts.len(), 5); + assert_eq!(choice_value(&s.account, cx), chosen, "selection survives the rebuild"); + }); + } + + #[gpui_kit::test] + fn an_empty_database_shows_the_welcome_state_without_errors(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, empty_db()); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(s.overview().is_ok()); + assert!(s.accounts.is_empty() && s.transactions.is_empty()); + assert!(s.visible_transactions(cx).is_empty()); + }); + } +} diff --git a/src/ui/palette.rs b/src/ui/palette.rs new file mode 100644 index 0000000..926f8e9 --- /dev/null +++ b/src/ui/palette.rs @@ -0,0 +1,201 @@ +use super::{PaletteDismiss, PaletteNext, PalettePrev, View, PALETTE_CONTEXT}; +use gpui_kit::base::input::{InputEvent, InputState}; +use gpui_kit::{ + div, prelude::*, rems, App, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, MouseButton, Render, SharedString, Subscription, Window, +}; +use gpui_omarchy::{input, keycap, ActiveTheme as _}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Command { + Go(View), + AddTransaction, + ImportTransactions, + AddAccount, + AddCategory, + AddSubscription, +} + +pub enum PaletteEvent { + Run(Command), + Dismiss, +} + +struct Entry { + label: SharedString, + shortcut: Option<&'static str>, + command: Command, +} + +fn entries() -> Vec { + let mut all: Vec = View::ALL + .iter() + .map(|v| Entry { + label: format!("Go to {}", v.label()).into(), + shortcut: Some(v.shortcut()), + command: Command::Go(*v), + }) + .collect(); + for (label, command) in [ + ("Add Transaction", Command::AddTransaction), + ("Import Transactions (CSV)", Command::ImportTransactions), + ("Add Account", Command::AddAccount), + ("Add Category", Command::AddCategory), + ("Add Subscription", Command::AddSubscription), + ] { + all.push(Entry { + label: label.into(), + shortcut: None, + command, + }); + } + all +} + +pub struct Palette { + query: Entity, + selected: usize, + focus: FocusHandle, + _subs: Vec, +} + +impl EventEmitter for Palette {} + +impl Focusable for Palette { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus.clone() + } +} + +impl Palette { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let query = cx.new(|cx| InputState::new(window, cx).placeholder("Type a command...")); + let sub = cx.subscribe_in(&query, window, |this, _, event: &InputEvent, _, cx| match event { + InputEvent::Change => { + this.selected = 0; + cx.notify(); + } + InputEvent::PressEnter { .. } => this.confirm(cx), + _ => {} + }); + query.update(cx, |state, cx| state.focus(window, cx)); + Self { + query, + selected: 0, + focus: cx.focus_handle(), + _subs: vec![sub], + } + } + + fn matches(&self, cx: &App) -> Vec { + let needle = self.query.read(cx).value().to_lowercase(); + entries() + .into_iter() + .filter(|e| e.label.to_lowercase().contains(&needle)) + .collect() + } + + fn confirm(&mut self, cx: &mut Context) { + let matches = self.matches(cx); + if let Some(entry) = matches.get(self.selected) { + cx.emit(PaletteEvent::Run(entry.command)); + } + } + + fn step(&mut self, delta: isize, cx: &mut Context) { + let len = self.matches(cx).len(); + if len > 0 { + self.selected = (self.selected as isize + delta).clamp(0, len as isize - 1) as usize; + cx.notify(); + } + } +} + +impl Render for Palette { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let matches = self.matches(cx); + let selected = self.selected.min(matches.len().saturating_sub(1)); + let empty = matches.is_empty(); + let query = input("palette-input", &self.query, window, cx); + + let rows = matches.into_iter().enumerate().map(|(i, entry)| { + let command = entry.command; + let mut row = div() + .id(("palette-row", i)) + .flex() + .items_center() + .justify_between() + .px(rems(0.75)) + .py(rems(0.375)) + .cursor_pointer() + .child(entry.label) + .on_mouse_move(cx.listener(move |this, _, _, cx| { + if this.selected != i { + this.selected = i; + cx.notify(); + } + })) + .on_click(cx.listener(move |_, _, _, cx| cx.emit(PaletteEvent::Run(command)))); + if i == selected { + row = row.bg(t.selected_fill()); + } + if let Some(shortcut) = entry.shortcut { + row = row.child(keycap(shortcut, cx)); + } + row + }); + + div() + .id("palette-backdrop") + .absolute() + .size_full() + .flex() + .justify_center() + .pt(rems(5.)) + .bg(t.background.opacity(0.6)) + .occlude() + .on_mouse_down( + MouseButton::Left, + cx.listener(|_, _, _, cx| cx.emit(PaletteEvent::Dismiss)), + ) + .child( + div() + .key_context(PALETTE_CONTEXT) + .track_focus(&self.focus) + .on_action(cx.listener(|this, _: &PaletteNext, _, cx| this.step(1, cx))) + .on_action(cx.listener(|this, _: &PalettePrev, _, cx| this.step(-1, cx))) + .on_action(cx.listener(|_, _: &PaletteDismiss, _, cx| { + cx.emit(PaletteEvent::Dismiss) + })) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .w(rems(34.)) + .max_h(rems(24.)) + .flex() + .flex_col() + .border_1() + .border_color(t.border) + .bg(t.background) + .font_family(t.font.clone()) + .text_size(rems(0.75)) + .text_color(t.foreground) + .child(div().p(rems(0.5)).child(query)) + .child( + div() + .id("palette-results") + .flex() + .flex_col() + .overflow_y_scroll() + .children(rows) + .when(empty, |el| { + el.child( + div() + .p(rems(1.)) + .text_color(t.secondary) + .child("No matching commands"), + ) + }), + ), + ) + } +} diff --git a/src/ui/projections.rs b/src/ui/projections.rs new file mode 100644 index 0000000..db54b1f --- /dev/null +++ b/src/ui/projections.rs @@ -0,0 +1,409 @@ +use super::widgets::*; +use chelete_lib::commands::{self, Subscription}; +use chelete_lib::database::DbState; +use chelete_lib::format::{format_balance, format_money, parse_dollars_lenient}; +use chelete_lib::projection::{ + compute_projection, format_axis, normalize_subscription_to_period, PeriodType, ProjectionInput, + ProjectionResult, +}; +use gpui_kit::base::input::InputState; +use gpui_kit::{div, prelude::*, rems, Context, Div, Entity, IntoElement, Render, SharedString, Subscription as GpuiSubscription, Window}; +use gpui_omarchy::chart::LineChart; +use gpui_omarchy::{empty_state, input, panel, select, ActiveTheme as _, ChoiceState}; +use std::sync::Arc; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ExpenseSource { + Manual, + Subscriptions, +} + +/// Everything the form collects, as typed. +pub struct FormValues<'a> { + pub starting_balance: &'a str, + pub income: &'a str, + pub manual_expense: &'a str, + pub source: ExpenseSource, + pub subscriptions_expense: i64, + pub period_type: PeriodType, + pub periods: &'a str, +} + +/// `None` until every field is valid, mirroring the web form. +pub fn build_input(v: &FormValues) -> Option { + let starting_balance = parse_dollars_lenient(v.starting_balance)?; + let income = parse_dollars_lenient(v.income)?; + let expense = match v.source { + ExpenseSource::Subscriptions => v.subscriptions_expense, + ExpenseSource::Manual => parse_dollars_lenient(v.manual_expense)?, + }; + let periods: u32 = v.periods.trim().parse().ok()?; + if periods < 1 || periods > v.period_type.limit() { + return None; + } + Some(ProjectionInput { starting_balance, income, expense, period_type: v.period_type, periods }) +} + +pub struct ProjectionsScreen { + db: Arc, + starting: Entity, + income: Entity, + expense: Entity, + periods: Entity, + source: Entity, + period_type: Entity, + subscriptions: Vec, + _subs: Vec, +} + +impl ProjectionsScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let starting = text_input_with("0.00", "0", window, cx); + let income = text_input("e.g. 1200.00", window, cx); + let expense = text_input("e.g. 100.00", window, cx); + let periods = text_input_with("e.g. 10", "10", window, cx); + let source = choice( + vec![("manual".into(), "Manual".into()), ("subscriptions".into(), "Subscriptions".into())], + Some("manual"), + window, + cx, + ); + let period_type = choice( + PeriodType::ALL.iter().map(|p| (p.value().to_string(), capitalize(p.plural()))).collect(), + Some("months"), + window, + cx, + ); + let mut subs = Vec::new(); + for input in [&starting, &income, &expense, &periods] { + subs.push(cx.observe(input, |_, _, cx| cx.notify())); + } + for choice in [&source, &period_type] { + subs.push(cx.observe(choice, |_, _, cx| cx.notify())); + } + let mut screen = Self { db, starting, income, expense, periods, source, period_type, subscriptions: Vec::new(), _subs: subs }; + screen.load(); + screen + } + + fn load(&mut self) { + self.subscriptions = commands::get_subscriptions(&self.db) + .map(|all| all.into_iter().filter(|s| s.is_active).collect()) + .unwrap_or_default(); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + fn period(&self, cx: &gpui_kit::App) -> PeriodType { + choice_value(&self.period_type, cx) + .and_then(|v| PeriodType::from_value(&v)) + .unwrap_or(PeriodType::Months) + } + + fn expense_source(&self, cx: &gpui_kit::App) -> ExpenseSource { + match choice_value(&self.source, cx).as_deref() { + Some("subscriptions") => ExpenseSource::Subscriptions, + _ => ExpenseSource::Manual, + } + } + + fn subscriptions_expense(&self, period: PeriodType) -> i64 { + self.subscriptions + .iter() + .map(|s| normalize_subscription_to_period(s.amount, &s.frequency, period)) + .sum() + } + + /// The projection for the current form values, if they are valid. + pub fn result(&self, cx: &gpui_kit::App) -> Option<(ProjectionResult, u32)> { + let period_type = self.period(cx); + let (starting, income, expense, periods) = ( + text_of(&self.starting, cx), + text_of(&self.income, cx), + text_of(&self.expense, cx), + text_of(&self.periods, cx), + ); + let input = build_input(&FormValues { + starting_balance: &starting, + income: &income, + manual_expense: &expense, + source: self.expense_source(cx), + subscriptions_expense: self.subscriptions_expense(period_type), + period_type, + periods: &periods, + })?; + Some((compute_projection(input, chrono::Local::now().date_naive()), input.periods)) + } +} + +fn capitalize(word: &str) -> String { + let mut chars = word.chars(); + chars.next().map(|c| c.to_uppercase().collect::() + chars.as_str()).unwrap_or_default() +} + +impl Render for ProjectionsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let period = self.period(cx); + let source = self.expense_source(cx); + let sub_total = self.subscriptions_expense(period); + + let expense_block = match source { + ExpenseSource::Manual => field("Expense per Period", input("proj-expense", &self.expense, window, cx), cx).into_any_element(), + ExpenseSource::Subscriptions => { + let n = self.subscriptions.len(); + let mut list = div() + .flex() + .flex_col() + .gap(rems(0.25)) + .p(rems(0.75)) + .border_1() + .border_color(t.border) + .child(div().text_color(t.secondary).child(format!( + "{n} active subscription{}", + if n == 1 { "" } else { "s" } + ))); + for s in &self.subscriptions { + list = list.child( + div() + .flex() + .justify_between() + .child(s.name.clone()) + .child(format!( + "{}/{}", + format_money(normalize_subscription_to_period(s.amount, &s.frequency, period)), + period.unit() + )), + ); + } + list = list.child(div().font_weight(gpui_kit::FontWeight::BOLD).child(format!( + "Total: {}/{}", + format_money(sub_total), + period.unit() + ))); + if n == 0 { + list = list.child(div().text_color(t.secondary).child("No active subscriptions found")); + } + list.into_any_element() + } + }; + + let form = panel("Inputs", cx) + .w(rems(20.)) + .flex_shrink_0() + .child(field("Starting Balance", input("proj-start", &self.starting, window, cx), cx)) + .child(field("Income per Period", input("proj-income", &self.income, window, cx), cx)) + .child(field("Expense Source", select("proj-source", &self.source, window, cx), cx)) + .child(expense_block) + .child(field("Period Type", select("proj-period", &self.period_type, window, cx), cx)) + .child( + field(&format!("Number of {}", period.plural()), input("proj-periods", &self.periods, window, cx), cx) + .child(div().text_color(t.secondary).child(format!("1 – {} {}", period.limit(), period.plural()))), + ); + + let content = match self.result(cx) { + None => empty_state( + "Nothing to project yet", + "Enter your income, expense and period to see a projection.", + cx, + ) + .into_any_element(), + Some((result, periods)) => { + let s = &result.summary; + let color = |v: i64| if v > 0 { t.success } else if v < 0 { t.danger } else { t.foreground }; + let stat = |title: &str, value: String, note: String, tint| -> Div { + panel(title.to_string(), cx) + .flex_1() + .child(div().text_size(rems(1.25)).text_color(tint).child(value)) + .child(div().text_color(t.secondary).child(note)) + }; + let stats = div() + .flex() + .gap(rems(1.)) + .child(stat( + "Projected Balance", + format_balance(s.final_balance), + format!("in {periods} {} · {}", period.plural(), s.projected_date.format("%b %e, %Y")), + color(s.net_per_period), + )) + .child(stat( + "Total Change", + format_money(s.total_increase), + "from starting balance".into(), + color(s.total_increase), + )) + .child(stat( + &format!("Net per {}", capitalize(period.unit())), + format_money(s.net_per_period), + match s.retention_rate { + Some(r) => format!("Keeping {r:.1}% of income"), + None => "No income entered".into(), + }, + color(s.net_per_period), + )); + + let declining = s.net_per_period < 0; + let mut chart = LineChart::new(result.points.clone()) + .id("projection-line") + .name("Balance") + .x(|p| SharedString::from(p.label.clone())) + .y(|p| p.balance as f64 / 100.0) + .stroke(if declining { t.danger } else { t.accent }) + .tick_margin(2) + .linear() + .dot() + .y_tick_format(|v| format_axis((v * 100.0).round() as i64)) + .tooltip_value(|_, v| format_balance((v * 100.0).round() as i64).into()); + if s.final_balance < 0 { + chart = chart.reference_line(0.0); + } + div() + .flex() + .flex_col() + .gap(rems(1.)) + .child(stats) + .child(panel("Balance Over Time", cx).child(div().w_full().h(rems(20.)).child(chart))) + .into_any_element() + } + }; + + div() + .id("projections") + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Projections", div().text_color(t.secondary).child("Project your balance growth over time"), cx)) + .child(div().flex().gap(rems(1.)).items_start().child(form).child(div().flex_1().min_w_0().child(content))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use chelete_lib::commands::CreateSubscriptionRequest; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn values<'a>(s: &'a str, i: &'a str, e: &'a str, n: &'a str) -> FormValues<'a> { + FormValues { + starting_balance: s, + income: i, + manual_expense: e, + source: ExpenseSource::Manual, + subscriptions_expense: 0, + period_type: PeriodType::Months, + periods: n, + } + } + + #[test] + fn valid_form_builds_an_input() { + let input = build_input(&values("0", "1200", "100", "10")).unwrap(); + assert_eq!((input.starting_balance, input.income, input.expense, input.periods), (0, 120000, 10000, 10)); + } + + #[test] + fn any_empty_or_bad_field_blocks_the_projection() { + assert!(build_input(&values("", "1200", "100", "10")).is_none()); + assert!(build_input(&values("0", "", "100", "10")).is_none()); + assert!(build_input(&values("0", "1200", "", "10")).is_none()); + assert!(build_input(&values("0", "1200", "100", "")).is_none()); + assert!(build_input(&values("0", "abc", "100", "10")).is_none()); + } + + #[test] + fn period_count_is_bounded_by_the_period_type() { + assert!(build_input(&values("0", "1", "1", "0")).is_none()); + assert!(build_input(&values("0", "1", "1", "601")).is_none()); + assert!(build_input(&values("0", "1", "1", "600")).is_some()); + assert!(build_input(&values("0", "1", "1", "2.5")).is_none()); + } + + #[test] + fn subscription_source_ignores_the_manual_expense() { + let mut v = values("0", "1000", "", "3"); + v.source = ExpenseSource::Subscriptions; + v.subscriptions_expense = 4500; + let input = build_input(&v).unwrap(); + assert_eq!(input.expense, 4500, "manual field may be blank in this mode"); + } + + fn setup(cx: &mut TestAppContext) -> (Entity, Arc, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let (screen, vcx) = cx.add_window_view(move |window, cx| ProjectionsScreen::new(db, window, cx)); + (screen, handle, vcx) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + #[gpui_kit::test] + fn typing_income_and_expense_produces_the_canonical_projection(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| assert!(screen.read(cx).result(cx).is_none(), "income and expense start empty")); + let (income, expense) = cx.update(|_, cx| (screen.read(cx).income.clone(), screen.read(cx).expense.clone())); + type_into(&income, "1200", cx); + type_into(&expense, "100", cx); + cx.update(|_, cx| { + let (result, periods) = screen.read(cx).result(cx).expect("valid inputs"); + assert_eq!(periods, 10); + assert_eq!(result.summary.final_balance, 1_100_000); + assert_eq!(result.points.len(), 11); + }); + } + + #[gpui_kit::test] + fn subscription_source_totals_active_subscriptions_only(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let make = |name: &str, amount: i64| { + commands::create_subscription(&db, CreateSubscriptionRequest { + name: name.into(), amount, currency: "USD".into(), frequency: "monthly".into(), + category_id: None, account_id: None, start_date: None, + }).unwrap() + }; + make("Active", 1000); + let inactive = make("Paused", 5000); + commands::update_subscription(&db, commands::UpdateSubscriptionRequest { + id: inactive.id, name: None, amount: None, currency: None, frequency: None, + category_id: None, account_id: None, start_date: None, is_active: Some(false), + }).unwrap(); + cx.update(|_, cx| screen.update(cx, |s, cx| s.reload(cx))); + + let (income, source) = cx.update(|_, cx| (screen.read(cx).income.clone(), screen.read(cx).source.clone())); + type_into(&income, "100", cx); + cx.update(|_, cx| source.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.subscriptions.len(), 1, "paused subscription excluded"); + let (result, _) = s.result(cx).expect("valid without a manual expense"); + assert_eq!(result.summary.net_per_period, 10000 - 1000); + }); + } + + #[gpui_kit::test] + fn switching_period_type_rescales_subscription_cost(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + commands::create_subscription(&db, CreateSubscriptionRequest { + name: "Gym".into(), amount: 12000, currency: "USD".into(), frequency: "monthly".into(), + category_id: None, account_id: None, start_date: None, + }).unwrap(); + cx.update(|_, cx| screen.update(cx, |s, cx| s.reload(cx))); + let period_type = cx.update(|_, cx| screen.read(cx).period_type.clone()); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.subscriptions_expense(PeriodType::Months), 12000); + assert_eq!(s.subscriptions_expense(PeriodType::Weeks), 2769); + }); + cx.update(|_, cx| period_type.update(cx, |c, cx| c.set_selected(Some(0), cx))); + cx.update(|_, cx| assert_eq!(screen.read(cx).period(cx), PeriodType::Weeks)); + } +} diff --git a/src/ui/settings.rs b/src/ui/settings.rs new file mode 100644 index 0000000..2b93bbe --- /dev/null +++ b/src/ui/settings.rs @@ -0,0 +1,135 @@ +use super::widgets::{choice, choice_value, element_tile, page_header, TileDetail}; +use chelete_lib::prefs::TEXT_SCALES; +use gpui_kit::{div, prelude::*, rems, App, Context, Div, Entity, EventEmitter, IntoElement, Render, Subscription, Window}; +use gpui_omarchy::{panel, select, separator, ActiveTheme as _, ChoiceState}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn row(label: &str, description: Option<&str>, value: impl IntoElement, cx: &App) -> Div { + let t = cx.omarchy(); + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .child(label.to_string()) + .children(description.map(|d| div().text_color(t.secondary).child(d.to_string()))), + ) + .child(div().text_color(t.secondary).child(value)) +} + + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SettingsEvent { + TextScale(u16), +} + +pub struct SettingsScreen { + scale: Entity, + /// The size the dropdown currently shows, so showing a size chosen + /// elsewhere (the keyboard) is not mistaken for a new choice. + shown: u16, + _sub: Subscription, +} + +impl EventEmitter for SettingsScreen {} + +fn scale_items() -> Vec<(String, String)> { + TEXT_SCALES.iter().map(|s| (s.to_string(), format!("{s}%"))).collect() +} + +impl SettingsScreen { + pub fn new(text_scale: u16, window: &mut Window, cx: &mut Context) -> Self { + let scale = choice(scale_items(), Some(&text_scale.to_string()), window, cx); + let sub = cx.observe(&scale, |this, state, cx| { + if let Some(percent) = choice_value(&state, cx).and_then(|v| v.parse::().ok()) { + if percent != this.shown { + this.shown = percent; + cx.emit(SettingsEvent::TextScale(percent)); + } + } + }); + Self { scale, shown: text_scale, _sub: sub } + } + + /// Reflect a size that was changed elsewhere (a keyboard shortcut). + pub fn show_scale(&mut self, percent: u16, cx: &mut Context) { + if self.shown == percent { + return; + } + self.shown = percent; + let index = TEXT_SCALES.iter().position(|s| *s == percent); + self.scale.update(cx, |state, cx| state.set_selected(index, cx)); + cx.notify(); + } + + #[cfg(test)] + pub fn shown(&self) -> u16 { + self.shown + } + + #[cfg(test)] + pub fn choose(&mut self, percent: u16, cx: &mut Context) { + let index = TEXT_SCALES.iter().position(|s| *s == percent); + self.scale.update(cx, |state, cx| state.set_selected(index, cx)); + } +} + +impl Render for SettingsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + render_page(&self.scale, window, cx) + } +} + +fn render_page(scale: &Entity, window: &mut Window, cx: &mut App) -> impl IntoElement { + let t = cx.omarchy().clone(); + let theme_name = t.name.to_string(); + + let appearance = panel("Appearance", cx) + .child(row("Theme", Some("Follows the Omarchy system theme"), theme_name, cx)) + .child(separator(cx)) + .child(separator(cx)) + .child(row("Text size", Some("Ctrl + larger, Ctrl - smaller, Ctrl 0 reset"), div().w(rems(7.)).child(select("text-scale", scale, window, cx)), cx)) + .child(separator(cx)) + .child(row("Corners", Some("Square, like the rest of Omarchy"), "Square", cx)); + + let finance = panel("Finance", cx) + .child(row("Base Currency", None, "USD", cx)) + .child(separator(cx)) + .child(row("Month Start", None, "1st", cx)); + + let about = panel("About", cx) + .child( + div() + .flex() + .items_center() + .gap(rems(0.75)) + .child(element_tile(4.5, TileDetail::Full, cx)) + .child( + div() + .child(div().text_size(rems(1.)).font_weight(gpui_kit::FontWeight::BOLD).child("Chelete")) + .child(div().text_color(t.secondary).child(format!("Version {VERSION}"))), + ), + ) + .child(div().text_color(t.secondary).child( + "A fast, keyboard-driven personal finance tracker for Linux. Manage accounts, \ + track transactions, and project your balance over time — without touching the \ + mouse. Built for Omarchy with live theme integration.", + )) + .child(separator(cx)) + .child(row("Author", None, "Victor Phos", cx)) + .child(separator(cx)) + .child(row("License", None, "MIT", cx)); + + div() + .id("settings") + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Settings", div(), cx)) + .child(div().max_w(rems(36.)).flex().flex_col().gap(rems(1.)).child(appearance).child(finance).child(about)) +} diff --git a/src/ui/shell.rs b/src/ui/shell.rs new file mode 100644 index 0000000..50bff61 --- /dev/null +++ b/src/ui/shell.rs @@ -0,0 +1,572 @@ +use super::accounts::AccountsScreen; +use super::categories::CategoriesScreen; +use super::projections::ProjectionsScreen; +use super::settings::{SettingsEvent, SettingsScreen}; +use super::subscriptions::SubscriptionsScreen; +use super::transactions::TransactionsScreen; +use super::overview::OverviewScreen; +use super::widgets::ScreenEvent; +use super::palette::{Command, Palette, PaletteEvent}; +use super::sidebar::sidebar; +use super::*; +use chelete_lib::database::DbState; +use chelete_lib::prefs::{clamp_text_scale, step_text_scale, Prefs, DEFAULT_TEXT_SCALE}; +use gpui_kit::{ + div, prelude::*, rems, Context, Entity, FocusHandle, Focusable, IntoElement, Render, Subscription, + Window, +}; +use gpui_omarchy::ActiveTheme as _; +use std::sync::Arc; + +pub struct Shell { + view: View, + prefs: Prefs, + persist: bool, + overview: Entity, + accounts: Entity, + categories: Entity, + subscriptions: Entity, + projections: Entity, + transactions: Entity, + settings: Entity, + toast: Option<(u64, String, bool)>, + toast_seq: u64, + palette: Option>, + focus: FocusHandle, + _palette_sub: Option, +} + +impl Shell { + pub fn new( + db: Arc, + prefs: Prefs, + persist: bool, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let focus = cx.focus_handle(); + focus.focus(window, cx); + let accounts = cx.new(|_| AccountsScreen::new(db.clone())); + cx.subscribe(&accounts, Self::on_screen_event).detach(); + let categories = cx.new(|_| CategoriesScreen::new(db.clone())); + cx.subscribe(&categories, Self::on_screen_event).detach(); + let subscriptions = cx.new(|cx| SubscriptionsScreen::new(db.clone(), window, cx)); + cx.subscribe(&subscriptions, Self::on_screen_event).detach(); + let projections = cx.new(|cx| ProjectionsScreen::new(db.clone(), window, cx)); + let transactions = cx.new(|cx| TransactionsScreen::new(db.clone(), window, cx)); + let text_scale = clamp_text_scale(prefs.text_scale); + super::theme_bridge::set_text_scale(text_scale, window, cx); + let settings = cx.new(|cx| SettingsScreen::new(text_scale, window, cx)); + cx.subscribe_in(&settings, window, |this, _, event: &SettingsEvent, window, cx| { + let SettingsEvent::TextScale(percent) = event; + this.set_text_scale(*percent, window, cx); + }) + .detach(); + cx.subscribe(&transactions, Self::on_screen_event).detach(); + let overview = cx.new(|cx| OverviewScreen::new(db.clone(), window, cx)); + Self { + overview, + accounts, + categories, + subscriptions, + projections, + transactions, + settings, + toast: None, + toast_seq: 0, + view: View::Overview, + prefs, + persist, + palette: None, + focus, + _palette_sub: None, + } + } + + pub fn view(&self) -> View { + self.view + } + + pub fn collapsed(&self) -> bool { + self.prefs.sidebar_collapsed + } + + pub fn navigate(&mut self, view: View, window: &mut Window, cx: &mut Context) { + self.close_palette(window, cx); + self.view = view; + match view { + View::Overview => self.overview.update(cx, |s, cx| s.reload(window, cx)), + View::Accounts => self.accounts.update(cx, |s, cx| s.reload(cx)), + View::Categories => self.categories.update(cx, |s, cx| s.reload(cx)), + View::Subscriptions => self.subscriptions.update(cx, |s, cx| s.reload(cx)), + View::Projections => self.projections.update(cx, |s, cx| s.reload(cx)), + View::Transactions => self.transactions.update(cx, |s, cx| s.reload(cx)), + _ => {} + } + cx.notify(); + } + + fn on_screen_event(&mut self, _: Entity, event: &ScreenEvent, cx: &mut Context) { + let ScreenEvent::Toast { message, ok } = event; + self.toast_seq += 1; + let seq = self.toast_seq; + self.toast = Some((seq, message.clone(), *ok)); + cx.notify(); + cx.spawn(async move |this, cx| { + cx.background_executor().timer(std::time::Duration::from_secs(3)).await; + this.update(cx, |this, cx| { + if this.toast.as_ref().is_some_and(|(s, ..)| *s == seq) { + this.toast = None; + cx.notify(); + } + }) + .ok(); + }) + .detach(); + } + + #[cfg(test)] + pub(super) fn toast_message(&self) -> Option<&str> { + self.toast.as_ref().map(|(_, m, _)| m.as_str()) + } + + #[cfg(test)] + pub fn text_scale(&self) -> u16 { + self.prefs.text_scale + } + + /// Resize all text by scaling the window's rem, as a browser zoom would. + pub fn set_text_scale(&mut self, percent: u16, window: &mut Window, cx: &mut Context) { + let percent = clamp_text_scale(percent); + self.prefs.text_scale = percent; + super::theme_bridge::set_text_scale(percent, window, cx); + if self.persist { + self.prefs.save(); + } + self.settings.update(cx, |s, cx| s.show_scale(percent, cx)); + cx.notify(); + } + + fn toggle_sidebar(&mut self, cx: &mut Context) { + self.prefs.sidebar_collapsed = !self.prefs.sidebar_collapsed; + if self.persist { + self.prefs.save(); + } + cx.notify(); + } + + fn toggle_palette(&mut self, window: &mut Window, cx: &mut Context) { + if self.palette.is_some() { + self.close_palette(window, cx); + return; + } + let palette = cx.new(|cx| Palette::new(window, cx)); + self._palette_sub = Some(cx.subscribe_in( + &palette, + window, + |this, _, event: &PaletteEvent, window, cx| match event { + PaletteEvent::Dismiss => this.close_palette(window, cx), + PaletteEvent::Run(command) => this.run(*command, window, cx), + }, + )); + self.palette = Some(palette); + cx.notify(); + } + + fn close_palette(&mut self, window: &mut Window, cx: &mut Context) { + if self.palette.take().is_some() { + self._palette_sub = None; + self.focus.focus(window, cx); + cx.notify(); + } + } + + /// Creation flows live on their own screens; until those are ported the + /// "Add …" commands just take you there. + fn run(&mut self, command: Command, window: &mut Window, cx: &mut Context) { + let target = match command { + Command::Go(view) => view, + Command::AddTransaction => { + self.navigate(View::Transactions, window, cx); + self.transactions.update(cx, |s, cx| s.open_add(window, cx)); + return; + } + Command::ImportTransactions => { + self.navigate(View::Transactions, window, cx); + self.transactions.update(cx, |s, cx| s.open_import(window, cx)); + return; + } + Command::AddAccount => { + self.navigate(View::Accounts, window, cx); + self.accounts.update(cx, |s, cx| s.open_add(window, cx)); + return; + } + Command::AddCategory => { + self.navigate(View::Categories, window, cx); + self.categories.update(cx, |s, cx| s.open_add(window, cx)); + return; + } + Command::AddSubscription => { + self.navigate(View::Subscriptions, window, cx); + self.subscriptions.update(cx, |s, cx| s.open_add(window, cx)); + return; + } + }; + self.navigate(target, window, cx); + } +} + +impl Focusable for Shell { + fn focus_handle(&self, _: &gpui_kit::App) -> FocusHandle { + self.focus.clone() + } +} + +impl Render for Shell { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let content = match self.view { + View::Overview => self.overview.clone().into_any_element(), + View::Accounts => self.accounts.clone().into_any_element(), + View::Categories => self.categories.clone().into_any_element(), + View::Subscriptions => self.subscriptions.clone().into_any_element(), + View::Projections => self.projections.clone().into_any_element(), + View::Transactions => self.transactions.clone().into_any_element(), + View::Settings => self.settings.clone().into_any_element(), + }; + div() + .key_context(SHELL_CONTEXT) + .track_focus(&self.focus) + .on_action(cx.listener(|this, _: &GoOverview, w, cx| this.navigate(View::Overview, w, cx))) + .on_action(cx.listener(|this, _: &GoTransactions, w, cx| this.navigate(View::Transactions, w, cx))) + .on_action(cx.listener(|this, _: &GoAccounts, w, cx| this.navigate(View::Accounts, w, cx))) + .on_action(cx.listener(|this, _: &GoCategories, w, cx| this.navigate(View::Categories, w, cx))) + .on_action(cx.listener(|this, _: &GoSubscriptions, w, cx| this.navigate(View::Subscriptions, w, cx))) + .on_action(cx.listener(|this, _: &GoProjections, w, cx| this.navigate(View::Projections, w, cx))) + .on_action(cx.listener(|this, _: &GoSettings, w, cx| this.navigate(View::Settings, w, cx))) + .on_action(cx.listener(|this, _: &TogglePalette, w, cx| this.toggle_palette(w, cx))) + .on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| this.toggle_sidebar(cx))) + .on_action(cx.listener(|this, _: &ZoomIn, window, cx| { + this.set_text_scale(step_text_scale(this.prefs.text_scale, 1), window, cx) + })) + .on_action(cx.listener(|this, _: &ZoomOut, window, cx| { + this.set_text_scale(step_text_scale(this.prefs.text_scale, -1), window, cx) + })) + .on_action(cx.listener(|this, _: &ZoomReset, window, cx| { + this.set_text_scale(DEFAULT_TEXT_SCALE, window, cx) + })) + .size_full() + .relative() + .flex() + .bg(t.background) + .text_color(t.foreground) + .font_family(t.font.clone()) + // Everything not sized explicitly (tables, lists, labels) inherits the + // same 12px base the Omarchy controls use. + .text_size(rems(0.75)) + .child(sidebar(self, cx)) + .child(div().flex_1().min_w_0().h_full().child(content)) + .when_some(self.palette.clone(), |el, palette| el.child(palette)) + .when_some(self.toast.clone(), |el, (_, message, ok)| { + el.child( + div() + .absolute() + .bottom(rems(1.25)) + .right(rems(1.25)) + .px(rems(0.875)) + .py(rems(0.625)) + .border_1() + .border_color(if ok { t.success } else { t.danger }) + .bg(t.surface) + .text_color(if ok { t.success } else { t.danger }) + .child(message), + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::TestAppContext; + + fn setup(cx: &mut TestAppContext) -> (Entity, &mut gpui_kit::VisualTestContext) { + init(cx); + let db = seeded_db(); + cx.add_window_view(|window, cx| Shell::new(db, Prefs::default(), false, window, cx)) + } + + fn view_of(shell: &Entity, cx: &mut gpui_kit::VisualTestContext) -> View { + cx.update(|_, cx| shell.read(cx).view()) + } + + #[gpui_kit::test] + fn starts_on_overview_with_seeded_data(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.update(|_, cx| { + let shell = shell.read(cx); + assert_eq!(shell.view(), View::Overview); + let overview = shell.overview.read(cx).overview().expect("overview loads"); + assert_eq!(overview.accounts.len(), 4); + }); + } + + #[gpui_kit::test] + fn ctrl_shortcuts_switch_views(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + for (keys, expected) in [ + ("ctrl-t", View::Transactions), + ("ctrl-a", View::Accounts), + ("ctrl-c", View::Categories), + ("ctrl-u", View::Subscriptions), + ("ctrl-p", View::Projections), + ("ctrl-s", View::Settings), + ("ctrl-o", View::Overview), + ] { + cx.simulate_keystrokes(keys); + assert_eq!(view_of(&shell, cx), expected, "after {keys}"); + } + } + + #[gpui_kit::test] + fn ctrl_b_toggles_sidebar(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-b"); + cx.update(|_, cx| assert!(shell.read(cx).collapsed())); + cx.simulate_keystrokes("ctrl-b"); + cx.update(|_, cx| assert!(!shell.read(cx).collapsed())); + } + + #[gpui_kit::test] + fn palette_opens_filters_and_navigates(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.update(|_, cx| assert!(shell.read(cx).palette.is_some(), "palette opens")); + + cx.simulate_input("go to acc"); + cx.simulate_keystrokes("enter"); + cx.update(|_, cx| { + assert!(shell.read(cx).palette.is_none(), "palette closes after running"); + }); + assert_eq!(view_of(&shell, cx), View::Accounts); + } + + #[gpui_kit::test] + fn palette_escape_closes_without_navigating(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.simulate_keystrokes("escape"); + cx.update(|_, cx| assert!(shell.read(cx).palette.is_none())); + assert_eq!(view_of(&shell, cx), View::Overview); + } + + #[gpui_kit::test] + fn palette_arrow_keys_pick_the_command(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.simulate_keystrokes("down down enter"); + // Entries are ordered like View::ALL: Overview, Transactions, Accounts. + assert_eq!(view_of(&shell, cx), View::Accounts); + } + + #[gpui_kit::test] + fn palette_add_account_opens_the_form(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.simulate_input("add account"); + cx.simulate_keystrokes("enter"); + assert_eq!(view_of(&shell, cx), View::Accounts); + cx.update(|_, cx| { + let accounts = shell.read(cx).accounts.clone(); + assert!(accounts.read(cx).has_dialog(), "add form is open"); + }); + } + + #[gpui_kit::test] + fn toasts_show_then_clear_themselves(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let accounts = cx.update(|_, cx| shell.read(cx).accounts.clone()); + cx.update(|_, cx| accounts.update(cx, |_, cx| cx.emit(ScreenEvent::ok("Saved it")))); + cx.update(|_, cx| assert_eq!(shell.read(cx).toast_message(), Some("Saved it"))); + cx.executor().advance_clock(std::time::Duration::from_secs(4)); + cx.run_until_parked(); + cx.update(|_, cx| assert_eq!(shell.read(cx).toast_message(), None, "gone after three seconds")); + } + + #[gpui_kit::test] + fn a_newer_toast_is_not_cleared_by_an_older_timer(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let accounts = cx.update(|_, cx| shell.read(cx).accounts.clone()); + cx.update(|_, cx| accounts.update(cx, |_, cx| cx.emit(ScreenEvent::ok("first")))); + cx.executor().advance_clock(std::time::Duration::from_secs(2)); + cx.update(|_, cx| accounts.update(cx, |_, cx| cx.emit(ScreenEvent::error("second")))); + cx.executor().advance_clock(std::time::Duration::from_millis(1500)); + cx.run_until_parked(); + cx.update(|_, cx| assert_eq!(shell.read(cx).toast_message(), Some("second"), "first timer fired but must not clear the newer toast")); + } + + #[gpui_kit::test] + fn every_view_renders(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + for view in View::ALL { + cx.update(|window, cx| shell.update(cx, |s, cx| s.navigate(view, window, cx))); + cx.update(|window, cx| { + window.draw(cx).clear(cx); + }); + assert_eq!(view_of(&shell, cx), view); + } + } + + fn rem(cx: &mut gpui_kit::VisualTestContext) -> f32 { + cx.update(|window, _| f32::from(window.rem_size())) + } + + #[gpui_kit::test] + fn text_starts_at_the_default_size(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 110)); + assert!((rem(cx) - 17.6).abs() < 1e-3); + } + + #[gpui_kit::test] + fn a_saved_size_is_applied_on_startup(cx: &mut TestAppContext) { + init(cx); + let db = seeded_db(); + let prefs = Prefs { text_scale: 150, ..Prefs::default() }; + let (shell, cx) = cx.add_window_view(move |window, cx| Shell::new(db, prefs, false, window, cx)); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 150)); + assert_eq!(rem(cx), 24.0); + } + + #[gpui_kit::test] + fn a_nonsense_saved_size_is_pulled_into_range(cx: &mut TestAppContext) { + init(cx); + let db = seeded_db(); + let prefs = Prefs { text_scale: 9000, ..Prefs::default() }; + let (_shell, cx) = cx.add_window_view(move |window, cx| Shell::new(db, prefs, false, window, cx)); + assert_eq!(rem(cx), 32.0, "clamped to 200%"); + } + + #[gpui_kit::test] + fn ctrl_plus_minus_and_zero_change_the_text_size(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-="); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 125)); + assert_eq!(rem(cx), 20.0); + cx.simulate_keystrokes("ctrl--"); + cx.simulate_keystrokes("ctrl--"); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 100)); + assert_eq!(rem(cx), 16.0); + cx.simulate_keystrokes("ctrl-0"); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 110), ); + } + + #[gpui_kit::test] + fn zooming_stops_at_the_smallest_and_largest_sizes(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + for _ in 0..12 { + cx.simulate_keystrokes("ctrl--"); + } + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 70)); + for _ in 0..12 { + cx.simulate_keystrokes("ctrl-="); + } + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 200)); + assert_eq!(rem(cx), 32.0); + } + + #[gpui_kit::test] + fn the_settings_dropdown_changes_the_text_size(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.update(|_, cx| settings.update(cx, |s, cx| s.choose(150, cx))); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 150)); + assert_eq!(rem(cx), 24.0); + } + + #[gpui_kit::test] + fn the_keyboard_updates_the_settings_dropdown_without_looping(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.simulate_keystrokes("ctrl-="); + cx.update(|_, cx| { + assert_eq!(settings.read(cx).shown(), 125, "dropdown follows the shortcut"); + assert_eq!(shell.read(cx).text_scale(), 125, "and does not push a second change back"); + }); + cx.simulate_keystrokes("ctrl-0"); + cx.update(|_, cx| assert_eq!(settings.read(cx).shown(), 110)); + } + + fn measure(cx: &mut gpui_kit::VisualTestContext, selector: &'static str) -> f32 { + cx.update(|window, cx| window.draw(cx).clear(cx)); + f32::from(cx.debug_bounds(selector).unwrap_or_else(|| panic!("{selector} not drawn")).size.width) + } + + #[gpui_kit::test] + fn zooming_actually_resizes_what_is_drawn(cx: &mut TestAppContext) { + // Navigate somewhere with a page title first. + let (shell, cx) = setup(cx); + cx.update(|window, cx| shell.update(cx, |s, cx| s.navigate(View::Accounts, window, cx))); + let (sidebar, title) = (measure(cx, "sidebar"), measure(cx, "page-title")); + + cx.simulate_keystrokes("ctrl-="); + let (sidebar_big, title_big) = (measure(cx, "sidebar"), measure(cx, "page-title")); + // 110% -> 125% is a 13.6% increase. + assert!(sidebar_big > sidebar * 1.1, "sidebar should widen: {sidebar} -> {sidebar_big}"); + assert!(title_big > title * 1.1, "title text should grow: {title} -> {title_big}"); + + cx.simulate_keystrokes("ctrl-0"); + let (sidebar_back, title_back) = (measure(cx, "sidebar"), measure(cx, "page-title")); + assert!((sidebar_back - sidebar).abs() < 0.5, "reset restores the width"); + assert!((title_back - title).abs() < 0.5, "reset restores the text"); + } + + #[gpui_kit::test] + fn the_settings_dropdown_resizes_the_layout_too(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let before = measure(cx, "sidebar"); + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.update(|_, cx| settings.update(cx, |s, cx| s.choose(200, cx))); + let after = measure(cx, "sidebar"); + assert!(after > before * 1.5, "200% vs 110% should nearly double the sidebar: {before} -> {after}"); + } + + /// The real app opens its window through `gpui_kit::open_window`, which + /// wraps the shell in a Root view. Tests normally skip that wrapper. + #[gpui_kit::test] + fn zooming_works_in_a_window_opened_the_way_main_opens_it(cx: &mut TestAppContext) { + init(cx); + let db = seeded_db(); + let (window, shell) = cx + .update(|cx| { + gpui_kit::open_window(gpui_kit::WindowOptions::default(), cx, |window, cx| { + cx.new(|cx| Shell::new(db, Prefs::default(), false, window, cx)) + }) + }) + .expect("window opens"); + let cx = &mut gpui_kit::VisualTestContext::from_window(window, cx); + let sidebar_is = |cx: &mut gpui_kit::VisualTestContext, expected: f32| { + // Draw a few frames: Root re-applies its own rem size on every one. + for _ in 0..3 { + cx.update(|window, cx| window.draw(cx).clear(cx)); + } + let width = measure(cx, "sidebar"); + assert!((width - expected).abs() < 0.6, "sidebar is 15rem = {expected}px, was {width}px"); + }; + + sidebar_is(cx, 15.0 * 17.6); // 110% is the default + cx.simulate_keystrokes("ctrl-="); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 125)); + sidebar_is(cx, 15.0 * 20.0); + cx.simulate_keystrokes("ctrl--"); + cx.simulate_keystrokes("ctrl--"); + sidebar_is(cx, 15.0 * 16.0); + cx.simulate_keystrokes("ctrl-0"); + sidebar_is(cx, 15.0 * 17.6); + + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.update(|_, cx| settings.update(cx, |s, cx| s.choose(200, cx))); + sidebar_is(cx, 15.0 * 32.0); + } +} diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs new file mode 100644 index 0000000..9c2fdb0 --- /dev/null +++ b/src/ui/sidebar.rs @@ -0,0 +1,125 @@ +use super::shell::Shell; +use super::widgets::{element_tile, TileDetail}; +use super::{View, TogglePalette, ToggleSidebar}; +use gpui_kit::{div, prelude::*, rems, Context, Div, SharedString}; +use gpui_omarchy::{button, icon, keycap, ActiveTheme as _, ButtonVariant, IconName}; + +fn item( + id: impl Into, + icon_name: IconName, + label: &'static str, + shortcut: &'static str, + active: bool, + collapsed: bool, + cx: &mut Context, +) -> gpui_omarchy::Button { + let id: SharedString = id.into(); + let t = cx.omarchy().clone(); + let variant = if active { + ButtonVariant::Primary + } else { + ButtonVariant::Secondary + }; + button(id, "", variant, cx) + .accessibility_label(label) + .w_full() + .justify_start() + .child(icon(icon_name).size(rems(1.))) + .when(!collapsed, |b| { + b.child(div().flex_1().child(label)) + .child(div().text_color(t.secondary).child(keycap(shortcut, cx))) + }) +} + +pub fn sidebar(shell: &Shell, cx: &mut Context) -> Div { + let t = cx.omarchy().clone(); + let collapsed = shell.collapsed(); + let current = shell.view(); + + let mut nav = Vec::new(); + for view in View::ALL { + nav.push( + item( + format!("nav-{}", view.label()), + view.icon(), + view.label(), + view.shortcut(), + current == view, + collapsed, + cx, + ) + .on_click(cx.listener(move |this, _, window, cx| this.navigate(view, window, cx))), + ); + } + + let command = item( + "nav-command", + IconName::Command, + "Command", + "Ctrl+K", + false, + collapsed, + cx, + ) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(Box::new(TogglePalette), cx); + })); + + let collapse = item( + "nav-collapse", + if collapsed { + IconName::PanelLeftOpen + } else { + IconName::PanelLeftClose + }, + "Collapse", + "Ctrl+B", + false, + collapsed, + cx, + ) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(Box::new(ToggleSidebar), cx); + })); + + div() + .debug_selector(|| "sidebar".into()) + .flex() + .flex_col() + .flex_shrink_0() + .w(if collapsed { rems(3.25) } else { rems(15.) }) + .h_full() + .gap(rems(0.25)) + .p(rems(0.5)) + .border_r_1() + .border_color(t.border) + .bg(t.surface) + .font_family(t.font.clone()) + .text_size(rems(0.75)) + .text_color(t.foreground) + .child( + div() + .flex() + .items_center() + .gap(rems(0.625)) + .py(rems(0.5)) + .when(collapsed, |row| row.justify_center()) + .child(if collapsed { + element_tile(2.25, TileDetail::Compact, cx) + } else { + element_tile(3.25, TileDetail::Named, cx) + }) + .when(!collapsed, |row| { + row.child( + div() + .child(div().text_size(rems(0.9375)).font_weight(gpui_kit::FontWeight::BOLD).text_color(t.foreground).child("Chelete")) + .child(div().text_color(t.secondary).child("Personal finance")), + ) + }), + ) + .children(nav) + .child(div().flex_1()) + .child(command) + .child(div().h(rems(0.0625)).bg(t.border)) + .child(collapse) +} diff --git a/src/ui/subscriptions.rs b/src/ui/subscriptions.rs new file mode 100644 index 0000000..4ec217c --- /dev/null +++ b/src/ui/subscriptions.rs @@ -0,0 +1,807 @@ +use super::accounts::FormEvent; +use super::date_field; +use super::icons::category_icon; +use super::widgets::*; +use chelete_lib::commands::{ + self, Account, Category, CreateSubscriptionRequest, CreateTransactionRequest, Subscription, + UpdateSubscriptionRequest, +}; +use chelete_lib::database::DbState; +use chelete_lib::format::{format_money, parse_cents}; +use chelete_lib::paging::{page_slice, Pager}; +use chelete_lib::subscriptions::{filter_by_frequency, frequency_label, next_payment_date, total_amount, FREQUENCIES}; +use gpui_kit::base::input::InputState; +use gpui_kit::component::date_picker::{DatePicker, DatePickerState}; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Entity, EventEmitter, IntoElement, Render, + SharedString, Subscription as GpuiSubscription, Window, +}; +use gpui_omarchy::{ + button, icon_button, input, panel, select, ActiveTheme as _, ButtonVariant, ChoiceState, IconName, +}; +use std::collections::HashSet; +use std::sync::Arc; + +enum Dialog { + Form(Entity), + View(Subscription), + Confirm(Entity, Vec), +} + +pub struct SubscriptionsScreen { + db: Arc, + subscriptions: Vec, + categories: Vec, + accounts: Vec, + filter: Entity, + applied_filter: String, + per_page: Entity, + applied_per_page: usize, + page: usize, + selected: HashSet, + dialog: Option, + view_focus: gpui_kit::FocusHandle, + _dialog_sub: Option, + _subs: Vec, +} + +impl EventEmitter for SubscriptionsScreen {} + +impl SubscriptionsScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let mut items = vec![("all".to_string(), "All Periods".to_string())]; + items.extend(FREQUENCIES.iter().map(|(v, l)| (v.to_string(), l.to_string()))); + let filter = choice(items, Some("all"), window, cx); + let per_page = per_page_choice(25, window, cx); + let subs = vec![ + cx.observe(&filter, |this, _, cx| this.on_controls_changed(cx)), + cx.observe(&per_page, |this, _, cx| this.on_controls_changed(cx)), + ]; + let mut screen = Self { + db, + subscriptions: Vec::new(), + categories: Vec::new(), + accounts: Vec::new(), + filter, + applied_filter: "all".into(), + per_page, + applied_per_page: 25, + page: 1, + selected: HashSet::new(), + dialog: None, + view_focus: cx.focus_handle(), + _dialog_sub: None, + _subs: subs, + }; + screen.load(); + screen + } + + fn load(&mut self) { + self.subscriptions = commands::get_subscriptions(&self.db).unwrap_or_default(); + self.categories = commands::get_categories(&self.db).unwrap_or_default(); + self.accounts = commands::get_accounts(&self.db).unwrap_or_default(); + let live: HashSet<&String> = self.subscriptions.iter().map(|s| &s.id).collect(); + self.selected.retain(|id| live.contains(id)); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + fn on_controls_changed(&mut self, cx: &mut Context) { + let filter = choice_value(&self.filter, cx).unwrap_or_else(|| "all".into()); + let per_page = choice_value(&self.per_page, cx) + .and_then(|v| v.parse().ok()) + .unwrap_or(self.applied_per_page); + if filter != self.applied_filter || per_page != self.applied_per_page { + self.applied_filter = filter; + self.applied_per_page = per_page; + self.page = 1; + } + cx.notify(); + } + + #[cfg(test)] + pub fn subscriptions(&self) -> &[Subscription] { + &self.subscriptions + } + + fn filtered(&self) -> Vec<&Subscription> { + filter_by_frequency(&self.subscriptions, &self.applied_filter) + } + + fn pager(&self) -> Pager { + Pager::new(self.page, self.applied_per_page, self.filtered().len()) + } + + #[cfg(test)] + fn has_dialog(&self) -> bool { + self.dialog.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + self.open_form(None, window, cx); + } + + fn open_form(&mut self, editing: Option, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| SubscriptionForm::new(self.db.clone(), &self.categories, &self.accounts, editing, window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, |this, _, event: &FormEvent, cx| { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + this.load(); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Form(form)); + cx.notify(); + } + + fn close_dialog(&mut self, cx: &mut Context) { + self.dialog = None; + self._dialog_sub = None; + cx.notify(); + } + + fn ask_delete(&mut self, ids: Vec, cx: &mut Context) { + if ids.is_empty() { + return; + } + let noun = plural(ids.len(), "subscription"); + let dialog = cx.new(|cx| { + ConfirmDialog::new( + format!("Delete {noun}?"), + format!( + "This will permanently remove the selected subscription{}. This action cannot be undone.", + if ids.len() == 1 { "" } else { "s" } + ), + "Delete", + cx, + ) + }); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ConfirmEvent, cx| { + if *event == ConfirmEvent::Confirm { + if let Some(Dialog::Confirm(_, ids)) = this.dialog.take() { + this.delete(ids, cx); + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Confirm(dialog, ids)); + cx.notify(); + } + + fn delete(&mut self, ids: Vec, cx: &mut Context) { + match commands::delete_subscriptions(&self.db, ids.clone()) { + Ok(()) => { + for id in &ids { + self.selected.remove(id); + } + self.load(); + let pager = self.pager(); + self.page = pager.page; + cx.emit(ScreenEvent::ok(format!("Deleted {}", plural(ids.len(), "subscription")))); + } + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete: {e}"))), + } + } + + fn pay(&mut self, sub: &Subscription, cx: &mut Context) { + let Some(account_id) = sub.account_id.clone().or_else(|| self.accounts.first().map(|a| a.id.clone())) else { + return cx.emit(ScreenEvent::error("Add an account before recording a payment")); + }; + let result = commands::create_transaction( + &self.db, + CreateTransactionRequest { + account_id, + category_id: sub.category_id.clone(), + transaction_type: "expense".into(), + amount: sub.amount, + currency: sub.currency.clone(), + description: sub.name.clone(), + merchant: None, + notes: None, + transaction_date: chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(), + }, + ); + match result { + Ok(_) => { + self.load(); + cx.emit(ScreenEvent::ok(format!("Payment recorded for {}", sub.name))); + } + Err(_) => cx.emit(ScreenEvent::error("Failed to record payment")), + } + cx.notify(); + } + + fn toggle(&mut self, id: &str, cx: &mut Context) { + if !self.selected.remove(id) { + self.selected.insert(id.to_string()); + } + cx.notify(); + } + + fn toggle_page(&mut self, cx: &mut Context) { + let filtered = self.filtered(); + let ids: Vec = page_slice(&filtered, &self.pager()).iter().map(|s| s.id.clone()).collect(); + let all_selected = !ids.is_empty() && ids.iter().all(|id| self.selected.contains(id)); + for id in ids { + if all_selected { + self.selected.remove(&id); + } else { + self.selected.insert(id); + } + } + cx.notify(); + } + + fn category_name(&self, id: &Option) -> Option<&Category> { + id.as_ref().and_then(|id| self.categories.iter().find(|c| &c.id == id)) + } + + fn account_name(&self, id: &Option) -> Option<&str> { + id.as_ref() + .and_then(|id| self.accounts.iter().find(|a| &a.id == id)) + .map(|a| a.name.as_str()) + } +} + +impl Render for SubscriptionsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let today = chrono::Local::now().date_naive(); + let filtered = self.filtered(); + let pager = self.pager(); + let rows: Vec = page_slice(&filtered, &pager).iter().map(|s| (*s).clone()).collect(); + let total = total_amount(&filtered); + let count = filtered.len(); + let selected_on_page = rows.iter().filter(|s| self.selected.contains(&s.id)).count(); + let header_state = selection_state(selected_on_page, rows.len()); + let filter_label = if self.applied_filter == "all" { "All Periods".to_string() } else { frequency_label(&self.applied_filter) }; + + let actions = if self.selected.is_empty() { + button("add-subscription", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))) + .into_any_element() + } else { + let ids: Vec = self.selected.iter().cloned().collect(); + button("delete-selected", format!("Delete {} selected", self.selected.len()), ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(ids.clone(), cx))) + .into_any_element() + }; + + let stats = div() + .flex() + .gap(rems(1.)) + .child(panel(format!("Total ({filter_label})"), cx).flex_1().child( + div().text_size(rems(1.5)).child(format_money(total)), + )) + .child(panel("Subscriptions", cx).flex_1().child(div().text_size(rems(1.5)).child(count.to_string()))); + + let table = if rows.is_empty() { + div().py(rems(3.)).flex().justify_center().text_color(t.secondary).child("No subscriptions found.").into_any_element() + } else { + let header = div() + .flex() + .items_center() + .py(rems(0.375)) + .bg(t.surface) + .text_color(t.bright) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(2.5), check("select-page", "", header_state, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle_page(cx)) + }, cx))) + .child(cell(None, "Name")) + .child(cell(None, "Category")) + .child(cell(None, "Account")) + .child(cell(Some(7.), "Period")) + .child(cell(Some(7.), "Start Date")) + .child(cell(Some(7.), "Next Payment")) + .child(cell(Some(7.), div().flex().justify_end().child("Amount"))) + .child(cell(Some(9.5), "")); + + let mut list = div().flex().flex_col().border_1().border_color(t.border).child(header); + for s in rows { + let next = next_payment_date(s.start_date.as_deref(), &s.frequency, today) + .map(|d| d.format("%Y-%m-%d").to_string()); + let category = self.category_name(&s.category_id).cloned(); + let account = self.account_name(&s.account_id).map(str::to_string); + let checked = self.selected.contains(&s.id); + let row_id = s.id.clone(); + let key = |prefix: &str| SharedString::from(format!("{prefix}-{}", s.id)); + let (view, edit, pay, del) = (s.clone(), s.clone(), s.clone(), s.id.clone()); + list = list.child( + div() + .flex() + .items_center() + .py(rems(0.25)) + .border_t_1() + .border_color(t.border) + .hover(|st| st.bg(t.surface)) + .child(cell(Some(2.5), check(key("sel"), "", if checked { gpui_kit::base::CheckboxState::Checked } else { gpui_kit::base::CheckboxState::Unchecked }, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle(&row_id, cx)) + }, cx))) + .child(cell(None, s.name.clone())) + .child(cell(None, match &category { + Some(c) => div() + .flex() + .items_center() + .gap(rems(0.25)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(12.), t.secondary))) + .child(c.name.clone()) + .into_any_element(), + None => dim("—", cx).into_any_element(), + })) + .child(cell(None, account.map(|a| div().child(a)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(7.), frequency_label(&s.frequency))) + .child(cell(Some(7.), s.start_date.clone().map(|d| div().child(d)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(7.), next.map(|d| div().child(d)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(7.), div().flex().justify_end().child(format_money(s.amount)))) + .child(cell( + Some(9.5), + div() + .flex() + .justify_end() + .gap(rems(0.125)) + .child(icon_button(key("view"), IconName::Eye, "View subscription", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, window, cx| { this.dialog = Some(Dialog::View(view.clone())); this.view_focus.focus(window, cx); cx.notify(); }))) + .child(icon_button(key("edit"), IconName::Pencil, "Edit subscription", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, window, cx| this.open_form(Some(edit.clone()), window, cx)))) + .child(icon_button(key("pay"), IconName::CreditCard, "Record payment", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, _, cx| this.pay(&pay.clone(), cx)))) + .child(icon_button(key("del"), IconName::Trash, "Delete subscription", ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(vec![del.clone()], cx)))), + )), + ); + } + let screen = cx.entity(); + let (prev, next) = (screen.clone(), screen); + div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(list) + .child(pagination_bar( + pager, + &self.per_page, + move |_, _, cx| prev.update(cx, |s, cx| { s.page = s.page.saturating_sub(1).max(1); cx.notify(); }), + move |_, _, cx| next.update(cx, |s, cx| { s.page += 1; cx.notify(); }), + window, + cx, + )) + .into_any_element() + }; + + let dialog = self.dialog.as_ref().map(|d| match d { + Dialog::Form(f) => f.clone().into_any_element(), + Dialog::Confirm(c, _) => c.clone().into_any_element(), + Dialog::View(s) => self.view_dialog(s, cx).into_any_element(), + }); + + div() + .id("subscriptions") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Subscriptions", actions, cx)) + .child(div().w(rems(14.)).child(select("sub-filter", &self.filter, window, cx))) + .child(stats) + .child(table) + .children(dialog) + } +} + +impl SubscriptionsScreen { + fn view_dialog(&self, s: &Subscription, cx: &mut Context) -> impl IntoElement { + let today = chrono::Local::now().date_naive(); + let next = next_payment_date(s.start_date.as_deref(), &s.frequency, today) + .map(|d| d.format("%Y-%m-%d").to_string()); + let category = self.category_name(&s.category_id).map(|c| c.name.clone()); + let account = self.account_name(&s.account_id).map(str::to_string); + let row = |label: &str, value: String| { + div() + .flex() + .justify_between() + .child(dim(label.to_string(), cx)) + .child(value) + }; + let body = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(row("Name", s.name.clone())) + .child(row("Amount", format_money(s.amount))) + .child(row("Period", frequency_label(&s.frequency))) + .child(row("Start Date", s.start_date.clone().unwrap_or_else(|| "—".into()))) + .child(row("Next Payment", next.unwrap_or_else(|| "—".into()))) + .child(row("Account", account.unwrap_or_else(|| "—".into()))) + .child(row("Category", category.unwrap_or_else(|| "—".into()))) + .child( + div().flex().justify_end().pt(rems(0.5)).child( + button("view-close", "Close", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.close_dialog(cx))), + ), + ); + modal("Subscription", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx).track_focus(&self.view_focus) + .on_action(cx.listener(|this, _: &super::ModalCancel, _, cx| this.close_dialog(cx))) + } +} + +pub struct SubscriptionForm { + db: Arc, + editing: Option, + name: Entity, + amount: Entity, + frequency: Entity, + category: Entity, + account: Entity, + start_date: Entity, + _subs: Vec, +} + +impl EventEmitter for SubscriptionForm {} + +impl SubscriptionForm { + fn new( + db: Arc, + categories: &[Category], + accounts: &[Account], + editing: Option, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut category_items = vec![(String::new(), "None".to_string())]; + category_items.extend(categories.iter().filter(|c| c.category_type == "expense").map(|c| (c.id.clone(), c.name.clone()))); + let mut account_items = vec![(String::new(), "None".to_string())]; + account_items.extend(accounts.iter().map(|a| (a.id.clone(), a.name.clone()))); + let frequency_items = FREQUENCIES.iter().map(|(v, l)| (v.to_string(), l.to_string())).collect(); + + let (name, amount, frequency, category, account, start) = match &editing { + Some(s) => ( + s.name.clone(), + format!("{:.2}", s.amount as f64 / 100.0), + s.frequency.clone(), + s.category_id.clone().unwrap_or_default(), + s.account_id.clone().unwrap_or_default(), + s.start_date.as_deref().and_then(date_field::parse_iso), + ), + None => (String::new(), String::new(), "monthly".into(), String::new(), String::new(), None), + }; + let name_input = text_input_with("e.g. Netflix", &name, window, cx); + name_input.update(cx, |s, cx| s.focus(window, cx)); + let amount_input = text_input_with("0.00", &amount, window, cx); + let _subs = submit_on_enter(&[&name_input, &amount_input], Self::submit, cx); + Self { + db, + editing: editing.map(|s| s.id), + name: name_input, + amount: amount_input, + frequency: choice(frequency_items, Some(&frequency), window, cx), + category: choice(category_items, Some(&category), window, cx), + account: choice(account_items, Some(&account), window, cx), + start_date: date_field::single(start, window, cx), + _subs, + } + } + + fn submit(&mut self, cx: &mut Context) { + let name = text_of(&self.name, cx); + let amount_text = text_of(&self.amount, cx); + if name.is_empty() || amount_text.is_empty() { + return; + } + let Some(amount) = parse_cents(&amount_text) else { + return cx.emit(FormEvent::Failed("Amount is not valid".into())); + }; + let frequency = choice_value(&self.frequency, cx).unwrap_or_else(|| "monthly".into()); + let none_if_blank = |v: Option| v.filter(|s| !s.is_empty()); + let category_id = none_if_blank(choice_value(&self.category, cx)); + let account_id = none_if_blank(choice_value(&self.account, cx)); + let start_date = date_field::picked(&self.start_date, cx).map(|d| d.format(date_field::FORMAT).to_string()); + + let result = match &self.editing { + None => commands::create_subscription( + &self.db, + CreateSubscriptionRequest { + name: name.clone(), + amount, + currency: "USD".into(), + frequency, + category_id, + account_id, + start_date, + }, + ) + .map(|_| format!("Added subscription {name}")), + Some(id) => commands::update_subscription( + &self.db, + UpdateSubscriptionRequest { + id: id.clone(), + name: Some(name.clone()), + amount: Some(amount), + currency: Some("USD".into()), + frequency: Some(frequency), + category_id: Some(category_id), + account_id: Some(account_id), + start_date: Some(start_date), + is_active: None, + }, + ) + .map(|_| format!("Saved subscription {name}")), + }; + match result { + Ok(message) => cx.emit(FormEvent::Saved(message)), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not save subscription: {e}"))), + } + } +} + +impl Render for SubscriptionForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let editing = self.editing.is_some(); + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Name", input("sub-name", &self.name, window, cx), cx)) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(field("Amount", input("sub-amount", &self.amount, window, cx), cx)) + .child(field("Period", select("sub-frequency", &self.frequency, window, cx), cx)), + ) + .child(field("Category", select("sub-category", &self.category, window, cx), cx)) + .child(field("Account", select("sub-account", &self.account, window, cx), cx)) + .child(field("Start Date", DatePicker::new(&self.start_date).placeholder("Pick a date").cleanable(true), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("sub-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)))) + .child(button("sub-submit", if editing { "Save" } else { "Add" }, ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.submit(cx)))), + ); + modal( + if editing { "Edit Subscription" } else { "Add Subscription" }, + body, + cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), + cx, + ).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::component::calendar::Date; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext) -> (Entity, Arc, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let (screen, vcx) = cx.add_window_view(move |window, cx| SubscriptionsScreen::new(db, window, cx)); + (screen, handle, vcx) + } + + fn make(db: &DbState, name: &str, amount: i64, frequency: &str) -> Subscription { + commands::create_subscription( + db, + CreateSubscriptionRequest { + name: name.into(), + amount, + currency: "USD".into(), + frequency: frequency.into(), + category_id: None, + account_id: None, + start_date: Some("2026-01-15".into()), + }, + ) + .unwrap() + } + + fn reload(screen: &Entity, cx: &mut VisualTestContext) { + cx.update(|_, cx| screen.update(cx, |s, cx| s.reload(cx))); + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + fn form_of(screen: &Entity, cx: &mut VisualTestContext) -> Entity { + cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Form(f)) => f.clone(), + _ => panic!("form should be open"), + }) + } + + #[gpui_kit::test] + fn add_subscription_with_start_date(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let (name, amount, date) = cx.update(|_, cx| { + let f = form.read(cx); + (f.name.clone(), f.amount.clone(), f.start_date.clone()) + }); + type_into(&name, "Netflix", cx); + type_into(&amount, "15.99", cx); + cx.update(|window, cx| { + date.update(cx, |d, cx| d.set_date(Date::Single(chrono::NaiveDate::from_ymd_opt(2026, 3, 9)), window, cx)) + }); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + let added = s.subscriptions().iter().find(|x| x.name == "Netflix").expect("saved"); + assert_eq!(added.amount, 1599); + assert_eq!(added.frequency, "monthly"); + assert_eq!(added.start_date.as_deref(), Some("2026-03-09")); + }); + } + + #[gpui_kit::test] + fn invalid_or_blank_input_keeps_form_open(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog(), "blank form is ignored")); + let (name, amount) = cx.update(|_, cx| (form.read(cx).name.clone(), form.read(cx).amount.clone())); + type_into(&name, "Bad", cx); + type_into(&amount, "abc", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_dialog()); + assert!(screen.read(cx).subscriptions().is_empty()); + }); + } + + #[gpui_kit::test] + fn edit_updates_and_can_clear_the_category(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let cat = commands::get_categories(&db).unwrap().into_iter().find(|c| c.category_type == "expense").unwrap(); + let created = commands::create_subscription(&db, CreateSubscriptionRequest { + name: "Gym".into(), amount: 3000, currency: "USD".into(), frequency: "monthly".into(), + category_id: Some(cat.id.clone()), account_id: None, start_date: None, + }).unwrap(); + reload(&screen, cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_form(Some(created.clone()), window, cx))); + let form = form_of(&screen, cx); + let (amount, category) = cx.update(|_, cx| { + let f = form.read(cx); + assert_eq!(f.amount.read(cx).value().as_ref(), "30.00"); + assert_eq!(choice_value(&f.category, cx).as_deref(), Some(cat.id.as_str())); + (f.amount.clone(), f.category.clone()) + }); + type_into(&amount, "35", cx); + cx.update(|_, cx| category.update(cx, |c, cx| c.set_selected(Some(0), cx))); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + let saved = s.subscriptions().iter().find(|x| x.id == created.id).unwrap(); + assert_eq!(saved.amount, 3500); + assert_eq!(saved.category_id, None, "choosing None clears the category"); + assert_eq!(s.subscriptions().len(), 1); + }); + } + + #[gpui_kit::test] + fn recording_a_payment_creates_an_expense(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let sub = make(&db, "Spotify", 999, "monthly"); + reload(&screen, cx); + let before = commands::get_transactions(&db).unwrap().len(); + cx.update(|_, cx| screen.update(cx, |s, cx| s.pay(&sub, cx))); + let after = commands::get_transactions(&db).unwrap(); + assert_eq!(after.len(), before + 1); + let paid = after.iter().find(|t| t.description == "Spotify").expect("payment transaction"); + assert_eq!(paid.amount, 999); + assert_eq!(paid.transaction_type, "expense"); + } + + #[gpui_kit::test] + fn bulk_delete_goes_through_confirmation(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + for n in ["A", "B", "C"] { + make(&db, n, 100, "monthly"); + } + reload(&screen, cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.toggle_page(cx))); + cx.update(|_, cx| assert_eq!(screen.read(cx).selected.len(), 3)); + let ids: Vec = cx.update(|_, cx| screen.read(cx).selected.iter().cloned().collect()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(ids, cx))); + + // Cancelling keeps everything. + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Confirm(d, _)) => d.clone(), + _ => panic!("confirm dialog should be open"), + }); + cx.update(|_, cx| dialog.update(cx, |_, cx| cx.emit(ConfirmEvent::Cancel))); + cx.update(|_, cx| { + assert!(!screen.read(cx).has_dialog()); + assert_eq!(screen.read(cx).subscriptions().len(), 3); + }); + + // Confirming deletes them and clears the selection. + let ids: Vec = cx.update(|_, cx| screen.read(cx).selected.iter().cloned().collect()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(ids, cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Confirm(d, _)) => d.clone(), + _ => panic!("confirm dialog should be open"), + }); + cx.update(|_, cx| dialog.update(cx, |_, cx| cx.emit(ConfirmEvent::Confirm))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(s.subscriptions().is_empty()); + assert!(s.selected.is_empty()); + assert!(!s.has_dialog()); + }); + } + + #[gpui_kit::test] + fn frequency_filter_narrows_the_list_and_resets_the_page(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + make(&db, "M1", 1000, "monthly"); + make(&db, "M2", 500, "monthly"); + make(&db, "Y1", 12000, "yearly"); + reload(&screen, cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.filtered().len(), 3); + assert_eq!(total_amount(&s.filtered()), 13500); + }); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 2; cx.notify(); })); + let filter = cx.update(|_, cx| screen.read(cx).filter.clone()); + // Items: all, weekly, bi_weekly, monthly, ... so index 3 is monthly. + cx.update(|_, cx| filter.update(cx, |f, cx| f.set_selected(Some(3), cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.applied_filter, "monthly"); + assert_eq!(s.page, 1, "changing the filter returns to page one"); + assert_eq!(s.filtered().len(), 2); + assert_eq!(total_amount(&s.filtered()), 1500); + }); + } + + #[gpui_kit::test] + fn open_add_shows_the_form(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); + } + + #[gpui_kit::test] + fn escape_dismisses_the_delete_confirmation_without_deleting(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let sub = make(&db, "Keep me", 100, "monthly"); + reload(&screen, cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(vec![sub.id.clone()], cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); + // The dialog takes focus while drawing its first frame; a person presses + // a key some frames later. + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("escape"); + cx.update(|_, cx| { + assert!(!screen.read(cx).has_dialog()); + assert_eq!(screen.read(cx).subscriptions().len(), 1); + }); + } +} diff --git a/src/ui/test_support.rs b/src/ui/test_support.rs new file mode 100644 index 0000000..105178a --- /dev/null +++ b/src/ui/test_support.rs @@ -0,0 +1,32 @@ +//! Shared helpers for headless UI tests. +use chelete_lib::database::{self, DbState}; +use chelete_lib::seed; +use gpui_kit::TestAppContext; +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; + +pub fn seeded_db() -> Arc { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + database::run_migrations(&conn).unwrap(); + seed::seed_database(&conn).unwrap(); + Arc::new(DbState(Mutex::new(conn))) +} + +pub fn empty_db() -> Arc { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + database::run_migrations(&conn).unwrap(); + Arc::new(DbState(Mutex::new(conn))) +} + +pub fn init(cx: &mut TestAppContext) { + cx.update(|cx| { + gpui_kit::init(cx); + gpui_omarchy::init(cx); + // Applying a fixed theme stops the filesystem watcher, which the + // deterministic test scheduler rejects. + gpui_omarchy::Theme::tokyo_night().apply(cx); + super::install(cx); + }); +} diff --git a/src/ui/theme_bridge.rs b/src/ui/theme_bridge.rs new file mode 100644 index 0000000..1c5f2f9 --- /dev/null +++ b/src/ui/theme_bridge.rs @@ -0,0 +1,94 @@ +//! gpui-omarchy themes its own controls; gpui-component widgets (the date +//! picker) read a separate theme. Copy the Omarchy palette across and keep it +//! in step when the system theme changes. +//! +//! It also swaps the UI font. gpui-omarchy defaults to the platform UI font, +//! which resolves to Adwaita Sans on GNOME-flavoured systems, whereas Omarchy +//! itself uses the one font chosen with `omarchy font set`. Use that everywhere. +use gpui_kit::component::Theme as ComponentTheme; +use gpui_kit::{App, BorrowAppContext as _}; +use gpui_omarchy::ActiveTheme as _; + +/// Make the Omarchy font the UI font for both control libraries. +fn use_omarchy_font(cx: &mut App) { + let mono = cx.omarchy().mono_font.clone(); + if cx.omarchy().font != mono { + // Observers see this change too, but by then the fonts already match. + cx.update_global::(|theme, _| theme.font = mono.clone()); + } + // gpui-omarchy copies its UI font into the base theme whenever it applies a palette. + gpui_kit::base::Theme::global_mut(cx).tokens.typography.sans = mono.clone(); + let t = ComponentTheme::global_mut(cx); + t.font_family = mono.clone(); + t.mono_font_family = mono; +} + +pub fn sync(cx: &mut App) { + use_omarchy_font(cx); + let o = cx.omarchy().clone(); + let t = ComponentTheme::global_mut(cx); + t.background = o.background; + t.foreground = o.foreground; + t.popover = o.surface; + t.popover_foreground = o.foreground; + t.primary = o.accent; + t.primary_foreground = o.on_accent; + t.accent = o.selection; + t.accent_foreground = o.foreground; + t.muted = o.surface; + t.muted_foreground = o.secondary; + t.border = o.border; + t.input = o.border; + t.ring = o.accent; + t.danger = o.danger; + t.chart_1 = o.chart[0]; + t.chart_2 = o.chart[1]; + t.chart_3 = o.chart[2]; + t.chart_4 = o.chart[3]; + t.chart_5 = o.chart[4]; +} + +/// Set the size of one rem for the whole app from a text size percentage. +/// +/// The window's root re-applies `ComponentTheme.font_size` as the rem size on +/// every frame, so that value has to carry the setting; setting only the +/// window's own rem size is undone on the next frame. +pub fn set_text_scale(percent: u16, window: &mut gpui_kit::Window, cx: &mut App) { + let rem = gpui_kit::px(chelete_lib::prefs::rem_size_px(percent)); + ComponentTheme::global_mut(cx).font_size = rem; + window.set_rem_size(rem); + window.refresh(); +} + +/// Sync now and again whenever the Omarchy theme changes. +pub fn install(cx: &mut App) { + sync(cx); + cx.observe_global::(sync).detach(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::init; + use gpui_kit::TestAppContext; + + #[gpui_kit::test] + fn the_ui_uses_the_omarchy_font_even_after_a_theme_change(cx: &mut TestAppContext) { + init(cx); + cx.update(|cx| { + let omarchy = cx.omarchy().clone(); + assert_ne!(omarchy.mono_font.as_ref(), ".SystemUIFont"); + assert_eq!(omarchy.font, omarchy.mono_font, "omarchy controls use the mono font"); + assert_eq!(ComponentTheme::global(cx).font_family, omarchy.mono_font, "so do gpui-component widgets"); + assert_eq!(gpui_kit::base::Theme::global(cx).tokens.typography.sans, omarchy.mono_font); + }); + // Applying a palette resets the font to the platform UI font; it must be put back. + cx.update(|cx| gpui_omarchy::Theme::flexoki_light().apply(cx)); + cx.update(|cx| { + let omarchy = cx.omarchy().clone(); + assert_eq!(omarchy.font, omarchy.mono_font); + assert_eq!(ComponentTheme::global(cx).font_family, omarchy.mono_font); + assert_eq!(gpui_kit::base::Theme::global(cx).tokens.typography.sans, omarchy.mono_font); + }); + } +} diff --git a/src/ui/transactions.rs b/src/ui/transactions.rs new file mode 100644 index 0000000..0cdc486 --- /dev/null +++ b/src/ui/transactions.rs @@ -0,0 +1,993 @@ +use super::accounts::FormEvent; +use super::csv_import::{CsvImportDialog, CsvImportEvent}; +use super::date_field; +use super::date_filter_control::DateFilterControl; +use super::icons::category_icon; +use super::widgets::*; +use chelete_lib::commands::{ + self, Account, Category, CreateTransactionRequest, ExportData, Transaction, UpdateTransactionRequest, +}; +use chelete_lib::database::DbState; +use chelete_lib::date_filter::is_date_in_range; +use chelete_lib::export::ExportFormat; +use chelete_lib::format::{format_money, parse_cents}; +use chelete_lib::paging::{page_slice, Pager}; +use gpui_kit::base::input::InputState; +use gpui_kit::component::date_picker::DatePicker; +use gpui_kit::component::date_picker::DatePickerState; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Entity, EventEmitter, IntoElement, Render, SharedString, + Subscription, Window, +}; +use gpui_omarchy::{ + button, icon, icon_button, input, select, ActiveTheme as _, ButtonVariant, ChoiceState, IconName, +}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::Arc; + +enum Dialog { + Form(Entity), + View(Transaction), + Confirm(Entity, Vec), + Export(Entity), + Import(Entity), +} + +pub struct TransactionsScreen { + db: Arc, + transactions: Vec, + accounts: Vec, + categories: Vec, + date_filter: DateFilterControl, + per_page: Entity, + applied_per_page: usize, + page: usize, + selected: HashSet, + dialog: Option, + view_focus: gpui_kit::FocusHandle, + mappings_path: PathBuf, + _dialog_sub: Option, + _subs: Vec, +} + +impl EventEmitter for TransactionsScreen {} + +impl TransactionsScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let date_filter = DateFilterControl::new(window, cx); + let per_page = per_page_choice(25, window, cx); + let mut subs = date_filter.observe(cx, |this: &mut Self, cx| { + this.page = 1; + cx.notify(); + }); + subs.push(cx.observe(&per_page, |this, _, cx| { + let n = choice_value(&this.per_page, cx).and_then(|v| v.parse().ok()).unwrap_or(this.applied_per_page); + if n != this.applied_per_page { + this.applied_per_page = n; + this.page = 1; + } + cx.notify(); + })); + let mut screen = Self { + db, + transactions: Vec::new(), + accounts: Vec::new(), + categories: Vec::new(), + date_filter, + per_page, + applied_per_page: 25, + page: 1, + selected: HashSet::new(), + dialog: None, + view_focus: cx.focus_handle(), + mappings_path: chelete_lib::import::session::SavedMappings::default_path(), + _dialog_sub: None, + _subs: subs, + }; + screen.load(); + screen + } + + fn load(&mut self) { + self.transactions = commands::get_transactions(&self.db).unwrap_or_default(); + self.accounts = commands::get_accounts(&self.db).unwrap_or_default(); + self.categories = commands::get_categories(&self.db).unwrap_or_default(); + let live: HashSet<&String> = self.transactions.iter().map(|t| &t.id).collect(); + self.selected.retain(|id| live.contains(id)); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + #[cfg(test)] + pub fn transactions(&self) -> &[Transaction] { + &self.transactions + } + + fn filtered(&self, cx: &gpui_kit::App) -> Vec<&Transaction> { + let (start, end) = self.date_filter.bounds(chrono::Local::now().date_naive(), cx); + self.transactions + .iter() + .filter(|t| is_date_in_range(&t.transaction_date, &start, &end)) + .collect() + } + + fn pager(&self, cx: &gpui_kit::App) -> Pager { + Pager::new(self.page, self.applied_per_page, self.filtered(cx).len()) + } + + #[cfg(test)] + fn has_dialog(&self) -> bool { + self.dialog.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + self.open_form(None, window, cx); + } + + fn open_form(&mut self, editing: Option, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| TransactionForm::new(self.db.clone(), &self.accounts, &self.categories, editing, window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, |this, _, event: &FormEvent, cx| { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + this.load(); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Form(form)); + cx.notify(); + } + + fn close_dialog(&mut self, cx: &mut Context) { + self.dialog = None; + self._dialog_sub = None; + cx.notify(); + } + + fn ask_delete(&mut self, ids: Vec, cx: &mut Context) { + if ids.is_empty() { + return; + } + let noun = plural(ids.len(), "transaction"); + let plural_s = if ids.len() == 1 { "" } else { "s" }; + let dialog = cx.new(|cx| { + ConfirmDialog::new( + format!("Delete {noun}?"), + format!("This will permanently remove the selected transaction{plural_s} and adjust account balances. This action cannot be undone."), + "Delete", + cx, + ) + }); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ConfirmEvent, cx| { + if *event == ConfirmEvent::Confirm { + if let Some(Dialog::Confirm(_, ids)) = this.dialog.take() { + this.delete(ids, cx); + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Confirm(dialog, ids)); + cx.notify(); + } + + fn delete(&mut self, ids: Vec, cx: &mut Context) { + match commands::delete_transactions(&self.db, ids.clone()) { + Ok(()) => { + for id in &ids { + self.selected.remove(id); + } + self.load(); + self.page = self.pager(cx).page; + cx.emit(ScreenEvent::ok(format!("Deleted {}", plural(ids.len(), "transaction")))); + } + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete: {e}"))), + } + } + + /// Open the CSV import dialog and ask for a file straight away. + pub fn open_import(&mut self, window: &mut Window, cx: &mut Context) { + let dialog = self.new_import_dialog(window, cx); + dialog.update(cx, |d, cx| d.choose_file(window, cx)); + } + + fn new_import_dialog(&mut self, window: &mut Window, cx: &mut Context) -> Entity { + let dialog = cx.new(|cx| { + CsvImportDialog::new(self.db.clone(), self.accounts.clone(), self.categories.clone(), self.mappings_path.clone(), window, cx) + }); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &CsvImportEvent, cx| { + if matches!(event, CsvImportEvent::Done) { + this.load(); + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Import(dialog.clone())); + cx.notify(); + dialog + } + + pub fn open_export(&mut self, cx: &mut Context) { + let data = match commands::export_data(&self.db) { + Ok(d) => d, + Err(e) => return cx.emit(ScreenEvent::error(format!("Could not read data: {e}"))), + }; + let dialog = cx.new(|cx| ExportDialog::new(data, cx)); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ExportEvent, cx| { + match event { + ExportEvent::Cancel => {} + ExportEvent::Written(path) => cx.emit(ScreenEvent::ok(format!("Exported to {}", path.display()))), + ExportEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Export(dialog)); + cx.notify(); + } + + fn toggle(&mut self, id: &str, cx: &mut Context) { + if !self.selected.remove(id) { + self.selected.insert(id.to_string()); + } + cx.notify(); + } + + fn toggle_page(&mut self, cx: &mut Context) { + let filtered = self.filtered(cx); + let ids: Vec = page_slice(&filtered, &self.pager(cx)).iter().map(|t| t.id.clone()).collect(); + let all = !ids.is_empty() && ids.iter().all(|id| self.selected.contains(id)); + for id in ids { + if all { + self.selected.remove(&id); + } else { + self.selected.insert(id); + } + } + cx.notify(); + } + + fn category(&self, id: &Option) -> Option<&Category> { + id.as_ref().and_then(|id| self.categories.iter().find(|c| &c.id == id)) + } + + fn account_name(&self, id: &str) -> Option<&str> { + self.accounts.iter().find(|a| a.id == id).map(|a| a.name.as_str()) + } +} + +fn signed(t: &Transaction) -> i64 { + if t.transaction_type == "expense" { + -t.amount + } else { + t.amount + } +} + +fn short_date(iso: &str) -> String { + date_field::parse_iso(iso) + .map(|d| d.format("%b %e").to_string()) + .unwrap_or_else(|| iso.to_string()) +} + +impl Render for TransactionsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let today = chrono::Local::now().date_naive(); + let filtered = self.filtered(cx); + let pager = self.pager(cx); + let rows: Vec = page_slice(&filtered, &pager).iter().map(|t| (*t).clone()).collect(); + let none_at_all = self.transactions.is_empty(); + let selected_on_page = rows.iter().filter(|r| self.selected.contains(&r.id)).count(); + let header_state = selection_state(selected_on_page, rows.len()); + drop(filtered); + + let actions = if self.selected.is_empty() { + div() + .flex() + .gap(rems(0.5)) + .child( + button("import-csv", "Import CSV", ButtonVariant::Outline, cx) + .child(icon(IconName::Upload).size(rems(0.875))) + .on_click(cx.listener(|this, _, window, cx| this.open_import(window, cx))), + ) + .child( + button("export-data", "Export Data", ButtonVariant::Outline, cx) + .child(icon(IconName::Download).size(rems(0.875))) + .on_click(cx.listener(|this, _, _, cx| this.open_export(cx))), + ) + .child( + button("add-transaction", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))), + ) + .into_any_element() + } else { + let ids: Vec = self.selected.iter().cloned().collect(); + button("delete-selected", format!("Delete {} selected", self.selected.len()), ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(ids.clone(), cx))) + .into_any_element() + }; + + let body = if rows.is_empty() { + let text = if none_at_all { "No transactions yet. Add one to get started." } else { "No transactions match the selected filters." }; + div().py(rems(3.)).flex().justify_center().text_color(t.secondary).child(text).into_any_element() + } else { + let header = div() + .flex() + .items_center() + .py(rems(0.375)) + .bg(t.surface) + .text_color(t.bright) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(2.5), check("select-page", "", header_state, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle_page(cx)) + }, cx))) + .child(cell(Some(6.), "Date")) + .child(cell(None, "Description")) + .child(cell(Some(6.), "Type")) + .child(cell(None, "Category")) + .child(cell(None, "Account")) + .child(cell(Some(8.), div().flex().justify_end().child("Amount"))) + .child(cell(Some(7.5), "")); + let mut list = div().flex().flex_col().border_1().border_color(t.border).child(header); + for tx in rows { + let category = self.category(&tx.category_id).cloned(); + let account = self.account_name(&tx.account_id).map(str::to_string); + let checked = self.selected.contains(&tx.id); + let row_id = tx.id.clone(); + let key = |prefix: &str| SharedString::from(format!("{prefix}-{}", tx.id)); + let (view, edit, del) = (tx.clone(), tx.clone(), tx.id.clone()); + let tint = if tx.transaction_type == "income" { t.success } else { t.danger }; + list = list.child( + div() + .flex() + .items_center() + .py(rems(0.25)) + .border_t_1() + .border_color(t.border) + .hover(|st| st.bg(t.surface)) + .child(cell(Some(2.5), check(key("sel"), "", if checked { gpui_kit::base::CheckboxState::Checked } else { gpui_kit::base::CheckboxState::Unchecked }, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle(&row_id, cx)) + }, cx))) + .child(cell(Some(6.), short_date(&tx.transaction_date))) + .child(cell(None, tx.description.clone())) + .child(cell(Some(6.), div().text_color(tint).child(tx.transaction_type.clone()))) + .child(cell(None, match &category { + Some(c) => div() + .flex() + .items_center() + .gap(rems(0.25)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(12.), t.secondary))) + .child(c.name.clone()) + .into_any_element(), + None => dim("—", cx).into_any_element(), + })) + .child(cell(None, account.map(|a| div().child(a)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(8.), div().flex().justify_end().text_color(tint).child(format_money(signed(&tx))))) + .child(cell( + Some(7.5), + div() + .flex() + .justify_end() + .gap(rems(0.125)) + .child(icon_button(key("view"), IconName::Eye, "View transaction", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, window, cx| { this.dialog = Some(Dialog::View(view.clone())); this.view_focus.focus(window, cx); cx.notify(); }))) + .child(icon_button(key("edit"), IconName::Pencil, "Edit transaction", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, window, cx| this.open_form(Some(edit.clone()), window, cx)))) + .child(icon_button(key("del"), IconName::Trash, "Delete transaction", ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(vec![del.clone()], cx)))), + )), + ); + } + let screen = cx.entity(); + let (prev, next) = (screen.clone(), screen); + div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(list) + .child(pagination_bar( + pager, + &self.per_page, + move |_, _, cx| prev.update(cx, |s, cx| { s.page = s.page.saturating_sub(1).max(1); cx.notify(); }), + move |_, _, cx| next.update(cx, |s, cx| { s.page += 1; cx.notify(); }), + window, + cx, + )) + .into_any_element() + }; + + let dialog = self.dialog.as_ref().map(|d| match d { + Dialog::Form(f) => f.clone().into_any_element(), + Dialog::Confirm(c, _) => c.clone().into_any_element(), + Dialog::Export(e) => e.clone().into_any_element(), + Dialog::Import(i) => i.clone().into_any_element(), + Dialog::View(tx) => self.view_dialog(tx, cx).into_any_element(), + }); + let filter_row = self.date_filter.render(today, window, cx); + + div() + .id("transactions") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Transactions", actions, cx)) + .child(filter_row) + .child(body) + .children(dialog) + } +} + +impl TransactionsScreen { + fn view_dialog(&self, tx: &Transaction, cx: &mut Context) -> impl IntoElement { + let category = self.category(&tx.category_id).map(|c| c.name.clone()); + let account = self.account_name(&tx.account_id).map(str::to_string); + let row = |label: &str, value: String| div().flex().justify_between().child(dim(label.to_string(), cx)).child(value); + let mut body = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(row("Date", short_date(&tx.transaction_date))) + .child(row("Description", tx.description.clone())); + if let Some(m) = &tx.merchant { + body = body.child(row("Merchant", m.clone())); + } + body = body + .child(row("Type", tx.transaction_type.clone())) + .child(row("Amount", format_money(signed(tx)))) + .child(row("Account", account.unwrap_or_else(|| "—".into()))) + .child(row("Category", category.unwrap_or_else(|| "—".into()))); + if let Some(n) = &tx.notes { + body = body.child(row("Notes", n.clone())); + } + let body = body.child( + div().flex().justify_end().pt(rems(0.5)).child( + button("view-close", "Close", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.close_dialog(cx))), + ), + ); + modal("Transaction", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx).track_focus(&self.view_focus) + .on_action(cx.listener(|this, _: &super::ModalCancel, _, cx| this.close_dialog(cx))) + } +} + +// ── add / edit ────────────────────────────────────────────────────── + +pub struct TransactionForm { + db: Arc, + categories: Vec, + editing: Option, + kind: String, + amount: Entity, + description: Entity, + account: Entity, + category: Entity, + date: Entity, + _subs: Vec, +} + +impl EventEmitter for TransactionForm {} + +fn category_items(categories: &[Category], kind: &str) -> Vec<(String, String)> { + let mut items = vec![(String::new(), "None".to_string())]; + items.extend(categories.iter().filter(|c| c.category_type == kind).map(|c| (c.id.clone(), c.name.clone()))); + items +} + +impl TransactionForm { + fn new( + db: Arc, + accounts: &[Account], + categories: &[Category], + editing: Option, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let kind = editing.as_ref().map(|t| t.transaction_type.clone()).unwrap_or_else(|| "expense".into()); + let amount = editing.as_ref().map(|t| format!("{:.2}", t.amount as f64 / 100.0)).unwrap_or_default(); + let description = editing.as_ref().map(|t| t.description.clone()).unwrap_or_default(); + let account_id = editing.as_ref().map(|t| t.account_id.clone()).or_else(|| accounts.first().map(|a| a.id.clone())); + let category_id = editing.as_ref().and_then(|t| t.category_id.clone()).unwrap_or_default(); + let date = editing + .as_ref() + .and_then(|t| date_field::parse_iso(&t.transaction_date)) + .unwrap_or_else(|| chrono::Local::now().date_naive()); + let amount_input = text_input_with("0.00", &amount, window, cx); + amount_input.update(cx, |s, cx| s.focus(window, cx)); + let description_input = text_input_with("e.g. Groceries", &description, window, cx); + let _subs = submit_on_enter(&[&amount_input, &description_input], Self::submit, cx); + Self { + db, + categories: categories.to_vec(), + kind: kind.clone(), + amount: amount_input, + description: description_input, + account: choice(accounts.iter().map(|a| (a.id.clone(), a.name.clone())).collect(), account_id.as_deref(), window, cx), + category: choice(category_items(categories, &kind), Some(&category_id), window, cx), + date: date_field::single(Some(date), window, cx), + editing, + _subs, + } + } + + fn set_kind(&mut self, kind: &str, window: &mut Window, cx: &mut Context) { + if self.kind == kind { + return; + } + self.kind = kind.to_string(); + // Categories belong to one type, so the list is rebuilt on a switch. + self.category = choice(category_items(&self.categories, kind), Some(""), window, cx); + cx.notify(); + } + + fn submit(&mut self, cx: &mut Context) { + let amount_text = text_of(&self.amount, cx); + let cents = match parse_cents(&amount_text) { + Some(c) if c > 0 => c, + _ => return, + }; + let Some(account_id) = choice_value(&self.account, cx) else { + return cx.emit(FormEvent::Failed("Add an account first".into())); + }; + let category_id = choice_value(&self.category, cx).filter(|c| !c.is_empty()); + let description = text_of(&self.description, cx); + let date = date_field::picked(&self.date, cx) + .unwrap_or_else(|| chrono::Local::now().date_naive()) + .format(date_field::FORMAT) + .to_string(); + + let result = match &self.editing { + None => commands::create_transaction( + &self.db, + CreateTransactionRequest { + account_id, + category_id, + transaction_type: self.kind.clone(), + amount: cents, + currency: "USD".into(), + description, + merchant: None, + notes: None, + transaction_date: date, + }, + ) + .map(|_| "Transaction added".to_string()), + Some(existing) => commands::update_transaction( + &self.db, + UpdateTransactionRequest { + id: existing.id.clone(), + account_id: Some(account_id), + category_id: Some(category_id), + transaction_type: Some(self.kind.clone()), + amount: Some(cents), + currency: Some("USD".into()), + description: Some(description), + merchant: None, + notes: None, + transaction_date: Some(date), + }, + ) + .map(|_| "Transaction saved".to_string()), + }; + match result { + Ok(message) => cx.emit(FormEvent::Saved(message)), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not save transaction: {e}"))), + } + } +} + +impl Render for TransactionForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let editing = self.editing.is_some(); + let kind_button = |id: &'static str, label: &'static str, value: &'static str, cx: &mut Context| { + button(id, label, if self.kind == value { ButtonVariant::Primary } else { ButtonVariant::Outline }, cx) + .flex_1() + .justify_center() + .on_click(cx.listener(move |this, _, window, cx| this.set_kind(value, window, cx))) + }; + let toggle = div() + .flex() + .gap(rems(0.5)) + .child(kind_button("kind-expense", "Expense", "expense", cx)) + .child(kind_button("kind-income", "Income", "income", cx)); + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Type", toggle, cx)) + .child(field("Amount", input("tx-amount", &self.amount, window, cx), cx)) + .child(field("Description", input("tx-description", &self.description, window, cx), cx)) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(field("Account", select("tx-account", &self.account, window, cx), cx)) + .child(field("Category", select("tx-category", &self.category, window, cx), cx)), + ) + .child(field("Date", DatePicker::new(&self.date).placeholder("Pick a date"), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("tx-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)))) + .child(button("tx-submit", if editing { "Save" } else { "Add" }, ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.submit(cx)))), + ); + modal( + if editing { "Edit Transaction" } else { "Add Transaction" }, + body, + cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), + cx, + ).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) + } +} + +// ── export ────────────────────────────────────────────────────────── + +#[derive(Clone, Debug)] +pub enum ExportEvent { + Cancel, + Written(PathBuf), + Failed(String), +} + +pub struct ExportDialog { + data: ExportData, + format: ExportFormat, + focus: ModalFocus, +} + +impl EventEmitter for ExportDialog {} + +impl ExportDialog { + fn new(data: ExportData, cx: &mut Context) -> Self { + Self { data, format: ExportFormat::Csv, focus: ModalFocus::new(cx) } + } + + /// Render the chosen format and write it to `path`. + pub fn write_to(&self, path: &std::path::Path) -> Result<(), String> { + let contents = self.format.contents(&self.data)?; + commands::write_export_file(path.to_string_lossy().to_string(), contents) + } + + fn choose_path_and_write(&mut self, cx: &mut Context) { + let directory = dirs::download_dir().or_else(dirs::home_dir).unwrap_or_else(|| PathBuf::from(".")); + let picked = cx.prompt_for_new_path(&directory, Some(self.format.default_file_name())); + cx.spawn(async move |this, cx| { + let outcome = match picked.await { + Ok(Ok(Some(path))) => this.update(cx, |this, cx| match this.write_to(&path) { + Ok(()) => cx.emit(ExportEvent::Written(path)), + Err(e) => cx.emit(ExportEvent::Failed(format!("Export failed: {e}"))), + }), + Ok(Ok(None)) | Err(_) => Ok(()), + Ok(Err(e)) => this.update(cx, |_, cx| cx.emit(ExportEvent::Failed(format!("Export failed: {e}")))), + }; + outcome.ok(); + }) + .detach(); + } +} + +impl Render for ExportDialog { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.focus.ensure(window, cx); + let format_button = |id: &'static str, label: &'static str, format: ExportFormat, cx: &mut Context| { + button(id, label, if self.format == format { ButtonVariant::Primary } else { ButtonVariant::Outline }, cx) + .flex_1() + .justify_center() + .on_click(cx.listener(move |this, _, _, cx| { this.format = format; cx.notify(); })) + }; + let summary = match self.format { + ExportFormat::Csv => format!("{} transactions, one row each.", self.data.transactions.len()), + ExportFormat::Json => "Accounts, categories, transactions and subscriptions.".to_string(), + }; + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field( + "Format", + div().flex().gap(rems(0.5)).child(format_button("fmt-csv", "CSV", ExportFormat::Csv, cx)).child(format_button("fmt-json", "JSON", ExportFormat::Json, cx)), + cx, + )) + .child(dim(summary, cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("export-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)))) + .child(button("export-go", "Export", ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.choose_path_and_write(cx)))), + ); + modal("Export Data", body, cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)), cx).track_focus(&self.focus.handle) + .on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(ExportEvent::Cancel))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use chelete_lib::date_filter::DateFilter; + use gpui_kit::component::calendar::Date; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext) -> (Entity, Arc, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let (screen, vcx) = cx.add_window_view(move |window, cx| TransactionsScreen::new(db, window, cx)); + (screen, handle, vcx) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + fn form_of(screen: &Entity, cx: &mut VisualTestContext) -> Entity { + cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Form(f)) => f.clone(), + _ => panic!("form should be open"), + }) + } + + fn select_filter(screen: &Entity, filter: DateFilter, cx: &mut VisualTestContext) { + let index = DateFilter::ALL_FILTERS.iter().position(|f| *f == filter).unwrap(); + let preset = cx.update(|_, cx| screen.read(cx).date_filter.preset.clone()); + cx.update(|_, cx| preset.update(cx, |c, cx| c.set_selected(Some(index), cx))); + } + + #[gpui_kit::test] + fn lists_all_seeded_transactions(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.transactions().len(), 25); + assert_eq!(s.filtered(cx).len(), 25, "All Time shows everything"); + assert_eq!(s.pager(cx).label(), "1–25 of 25"); + }); + } + + #[gpui_kit::test] + fn add_expense_updates_the_account_balance(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let account = commands::get_accounts(&db).unwrap().remove(0); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let (amount, description) = cx.update(|_, cx| (form.read(cx).amount.clone(), form.read(cx).description.clone())); + type_into(&amount, "42.50", cx); + type_into(&description, "Test purchase", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + assert_eq!(s.transactions().len(), 26); + let added = s.transactions().iter().find(|t| t.description == "Test purchase").expect("saved"); + assert_eq!(added.amount, 4250); + assert_eq!(added.transaction_type, "expense"); + assert_eq!(added.account_id, account.id, "defaults to the first account"); + }); + let after = commands::get_accounts(&db).unwrap().remove(0); + assert_eq!(after.balance, account.balance - 4250); + } + + #[gpui_kit::test] + fn zero_or_blank_amount_is_ignored(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + let amount = cx.update(|_, cx| form.read(cx).amount.clone()); + type_into(&amount, "0", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_dialog()); + assert_eq!(screen.read(cx).transactions().len(), 25); + }); + } + + #[gpui_kit::test] + fn switching_to_income_swaps_the_category_list(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let expense_names: Vec = commands::get_categories(&db).unwrap().into_iter().filter(|c| c.category_type == "expense").map(|c| c.id).collect(); + cx.update(|window, cx| form.update(cx, |f, cx| f.set_kind("income", window, cx))); + let (amount, description, category) = cx.update(|_, cx| { + let f = form.read(cx); + (f.amount.clone(), f.description.clone(), f.category.clone()) + }); + // Pick the first real income category (index 0 is "None"). + cx.update(|_, cx| category.update(cx, |c, cx| c.set_selected(Some(1), cx))); + type_into(&amount, "100", cx); + type_into(&description, "Paycheck", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let added = screen.read(cx).transactions().iter().find(|t| t.description == "Paycheck").cloned().expect("saved"); + assert_eq!(added.transaction_type, "income"); + let cat = added.category_id.expect("income category chosen"); + assert!(!expense_names.contains(&cat), "income form must not offer expense categories"); + }); + } + + #[gpui_kit::test] + fn edit_changes_the_transaction_in_place(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + let original = cx.update(|_, cx| screen.read(cx).transactions()[0].clone()); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_form(Some(original.clone()), window, cx))); + let form = form_of(&screen, cx); + let (amount, date) = cx.update(|_, cx| { + let f = form.read(cx); + assert_eq!(f.amount.read(cx).value().as_ref(), format!("{:.2}", original.amount as f64 / 100.0)); + (f.amount.clone(), f.date.clone()) + }); + type_into(&amount, "1.23", cx); + cx.update(|window, cx| date.update(cx, |d, cx| d.set_date(Date::Single(chrono::NaiveDate::from_ymd_opt(2026, 1, 2)), window, cx))); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.transactions().len(), 25, "no duplicate"); + let saved = s.transactions().iter().find(|t| t.id == original.id).unwrap(); + assert_eq!(saved.amount, 123); + assert_eq!(saved.transaction_date, "2026-01-02"); + }); + } + + #[gpui_kit::test] + fn date_presets_filter_the_list(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + select_filter(&screen, DateFilter::Today, cx); + let today = chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(s.filtered(cx).iter().all(|t| t.transaction_date == today)); + assert!(s.filtered(cx).len() < 25); + }); + select_filter(&screen, DateFilter::LastYear, cx); + cx.update(|_, cx| { + let s = screen.read(cx); + let year = (chrono::Local::now().date_naive().format("%Y").to_string().parse::().unwrap() - 1).to_string(); + assert!(s.filtered(cx).iter().all(|t| t.transaction_date.starts_with(&year))); + }); + } + + #[gpui_kit::test] + fn custom_range_uses_the_range_picker(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + let all = cx.update(|_, cx| screen.read(cx).transactions().to_vec()); + let mut dates: Vec<_> = all.iter().map(|t| t.transaction_date.clone()).collect(); + dates.sort(); + let target = dates[dates.len() / 2].clone(); + let target_date = date_field::parse_iso(&target).unwrap(); + select_filter(&screen, DateFilter::Custom, cx); + let range = cx.update(|_, cx| screen.read(cx).date_filter.range.clone()); + cx.update(|window, cx| range.update(cx, |r, cx| r.set_date(Date::Range(Some(target_date), Some(target_date)), window, cx))); + cx.update(|_, cx| { + let shown = screen.read(cx).filtered(cx); + assert!(!shown.is_empty()); + assert!(shown.iter().all(|t| t.transaction_date == target)); + assert_eq!(shown.len(), all.iter().filter(|t| t.transaction_date == target).count()); + }); + } + + #[gpui_kit::test] + fn changing_the_filter_resets_to_page_one(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 3; cx.notify(); })); + select_filter(&screen, DateFilter::ThisYear, cx); + cx.update(|_, cx| assert_eq!(screen.read(cx).page, 1)); + } + + #[gpui_kit::test] + fn per_page_paginates_the_list(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + let per_page = cx.update(|_, cx| screen.read(cx).per_page.clone()); + cx.update(|_, cx| per_page.update(cx, |c, cx| c.set_selected(Some(0), cx))); // 10 per page + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.applied_per_page, 10); + assert_eq!(s.pager(cx).total_pages(), 3); + assert_eq!(s.pager(cx).label(), "1–10 of 25"); + }); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 3; cx.notify(); })); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(page_slice(&s.filtered(cx), &s.pager(cx)).len(), 5); + }); + } + + #[gpui_kit::test] + fn bulk_delete_removes_transactions_and_restores_balances(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let before: i64 = commands::get_accounts(&db).unwrap().iter().map(|a| a.balance).sum(); + let victim = cx.update(|_, cx| screen.read(cx).transactions()[0].clone()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.toggle(&victim.id, cx))); + let ids: Vec = cx.update(|_, cx| screen.read(cx).selected.iter().cloned().collect()); + assert_eq!(ids.len(), 1); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(ids, cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Confirm(d, _)) => d.clone(), + _ => panic!("confirm dialog should be open"), + }); + cx.update(|_, cx| dialog.update(cx, |_, cx| cx.emit(ConfirmEvent::Confirm))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.transactions().len(), 24); + assert!(s.selected.is_empty()); + }); + let after: i64 = commands::get_accounts(&db).unwrap().iter().map(|a| a.balance).sum(); + let delta = if victim.transaction_type == "expense" { victim.amount } else { -victim.amount }; + assert_eq!(after, before + delta, "deleting reverses the balance effect"); + } + + #[gpui_kit::test] + fn export_writes_csv_and_json_files(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.open_export(cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Export(d)) => d.clone(), + _ => panic!("export dialog should be open"), + }); + let dir = std::env::temp_dir().join(format!("chelete-export-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + + let csv_path = dir.join("out.csv"); + cx.update(|_, cx| dialog.read(cx).write_to(&csv_path)).unwrap(); + let csv = std::fs::read_to_string(&csv_path).unwrap(); + assert_eq!(csv.lines().count(), 26, "header plus 25 transactions"); + assert!(csv.starts_with("id,transaction_date,")); + + cx.update(|_, cx| dialog.update(cx, |d, _| d.format = ExportFormat::Json)); + let json_path = dir.join("out.json"); + cx.update(|_, cx| dialog.read(cx).write_to(&json_path)).unwrap(); + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&json_path).unwrap()).unwrap(); + assert_eq!(json["transactions"].as_array().unwrap().len(), 25); + assert_eq!(json["accounts"].as_array().unwrap().len(), 4); + std::fs::remove_dir_all(&dir).ok(); + } + + #[gpui_kit::test] + fn export_to_an_unwritable_path_reports_an_error(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.open_export(cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Export(d)) => d.clone(), + _ => panic!("export dialog should be open"), + }); + let result = cx.update(|_, cx| dialog.read(cx).write_to(std::path::Path::new("/nonexistent-dir/x/out.csv"))); + assert!(result.is_err()); + } + + #[gpui_kit::test] + fn enter_in_the_description_field_saves_the_transaction(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let (amount, description) = cx.update(|_, cx| (form.read(cx).amount.clone(), form.read(cx).description.clone())); + type_into(&amount, "9.99", cx); + type_into(&description, "Enter key", cx); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("enter"); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + assert!(s.transactions().iter().any(|t| t.description == "Enter key" && t.amount == 999)); + }); + } +} diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs new file mode 100644 index 0000000..6df3a3b --- /dev/null +++ b/src/ui/widgets.rs @@ -0,0 +1,474 @@ +//! Small building blocks shared by the screens: page headers, form fields, +//! modals, inputs and choice lists. +use gpui_kit::base::input::InputState; +use gpui_kit::{ + div, prelude::*, rems, App, Context, Div, Entity, FontWeight, IntoElement, MouseButton, + MouseDownEvent, SharedString, Window, +}; +use gpui_omarchy::{ActiveTheme as _, ChoiceItem, ChoiceState}; + +/// Emitted by screens and forms so the shell can show a toast. +#[derive(Clone, Debug)] +pub enum ScreenEvent { + Toast { message: String, ok: bool }, +} + +impl ScreenEvent { + pub fn ok(message: impl Into) -> Self { + Self::Toast { message: message.into(), ok: true } + } + pub fn error(message: impl Into) -> Self { + Self::Toast { message: message.into(), ok: false } + } +} + +pub fn page_header(title: &str, actions: impl IntoElement, cx: &App) -> Div { + let t = cx.omarchy(); + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .debug_selector(|| "page-title".into()) + .text_size(rems(1.25)) + .font_weight(FontWeight::BOLD) + .text_color(t.foreground) + .child(title.to_string()), + ) + .child(div().flex().gap(rems(0.5)).child(actions)) +} + +pub fn field(label: &str, body: impl IntoElement, cx: &App) -> Div { + div() + .flex() + .flex_col() + .flex_1() + .gap(rems(0.25)) + .child(div().text_color(cx.omarchy().secondary).child(label.to_string())) + .child(body) +} + +/// A centered dialog over a dimmed, click-to-dismiss backdrop. Place it as the +/// last child of a `relative` container. +pub fn modal( + title: &str, + body: impl IntoElement, + on_dismiss: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> gpui_kit::Stateful
{ + modal_sized(26., title, body, on_dismiss, cx) +} + +/// Like [`modal`] with an explicit width in rems. +pub fn modal_sized( + width: f32, + title: &str, + body: impl IntoElement, + on_dismiss: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> gpui_kit::Stateful
{ + let t = cx.omarchy(); + div() + .id("modal-backdrop") + .key_context(super::MODAL_CONTEXT) + .absolute() + .top_0() + .left_0() + .size_full() + .flex() + .justify_center() + .items_center() + .bg(t.background.opacity(0.6)) + .occlude() + .on_mouse_down(MouseButton::Left, on_dismiss) + .child( + div() + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .w(rems(width)) + .max_w_full() + .max_h_full() + .flex() + .flex_col() + .gap(rems(0.875)) + .p(rems(1.125)) + .border_1() + .border_color(t.border) + .bg(t.background) + .font_family(t.font.clone()) + .text_size(rems(0.75)) + .text_color(t.foreground) + .child( + div() + .text_size(rems(1.)) + .font_weight(FontWeight::BOLD) + .child(title.to_string()), + ) + .child(body), + ) +} + +pub fn text_input( + placeholder: &str, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let placeholder = placeholder.to_string(); + cx.new(|cx| InputState::new(window, cx).placeholder(placeholder)) +} + +pub fn text_input_with( + placeholder: &str, + value: &str, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let placeholder = placeholder.to_string(); + let value = value.to_string(); + cx.new(|cx| InputState::new(window, cx).placeholder(placeholder).default_value(value)) +} + +pub fn text_of(input: &Entity, cx: &App) -> String { + input.read(cx).value().trim().to_string() +} + +/// A select over `(value, label)` pairs with `default` pre-selected when it is +/// one of the values. +pub fn choice( + items: Vec<(String, String)>, + default: Option<&str>, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let selected = default.and_then(|d| items.iter().position(|(v, _)| v == d)); + let items: Vec = items + .into_iter() + .map(|(v, l)| ChoiceItem::new(SharedString::from(v), SharedString::from(l))) + .collect(); + cx.new(|cx| { + let state = ChoiceState::new(items, window, cx); + match selected { + Some(i) => state.default_selected(i), + None => state, + } + }) +} + +pub fn choice_value(state: &Entity, cx: &App) -> Option { + state.read(cx).selected().map(|i| i.value.to_string()) +} + +/// The standard account types, in the order the web UI listed them. +pub fn account_type_options() -> Vec<(String, String)> { + [ + ("cash", "Cash"), + ("bank", "Bank"), + ("savings", "Savings"), + ("credit_card", "Credit Card"), + ("mobile_money", "Mobile Money"), + ("investment", "Investment"), + ("other", "Other"), + ] + .into_iter() + .map(|(v, l)| (v.to_string(), l.to_string())) + .collect() +} + +// ── lists ─────────────────────────────────────────────────────────── + +use chelete_lib::paging::{Pager, PER_PAGE_OPTIONS}; +use gpui_kit::base::CheckboxState; +use gpui_kit::{ClickEvent, EventEmitter}; +use gpui_kit::base::Checkbox; +use gpui_omarchy::{button, checkbox, select, ButtonVariant}; + +pub fn check( + id: impl Into, + label: &str, + state: CheckboxState, + on_toggle: impl Fn(&mut Window, &mut App) + 'static, + cx: &App, +) -> Checkbox { + checkbox(id, label.to_string(), state, cx) + .on_change(move |_, _: &ClickEvent, window, cx| on_toggle(window, cx)) +} + +pub fn per_page_choice(current: usize, window: &mut Window, cx: &mut Context) -> Entity { + choice( + PER_PAGE_OPTIONS.iter().map(|n| (n.to_string(), n.to_string())).collect(), + Some(¤t.to_string()), + window, + cx, + ) +} + +/// "Show [25] per page Previous 1–25 of 61 Next" +pub fn pagination_bar( + pager: Pager, + per_page: &Entity, + on_prev: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_next: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + window: &mut Window, + cx: &mut App, +) -> Div { + let t = cx.omarchy().clone(); + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .flex() + .items_center() + .gap(rems(0.5)) + .child(div().text_color(t.secondary).child("Show")) + .child(div().w(rems(5.)).child(select("per-page", per_page, window, cx))) + .child(div().text_color(t.secondary).child("per page")), + ) + .child( + div() + .flex() + .items_center() + .gap(rems(0.75)) + .child( + button("page-prev", "Previous", ButtonVariant::Outline, cx) + .disabled(!pager.has_prev()) + .on_click(on_prev), + ) + .child(div().text_color(t.secondary).child(pager.label())) + .child( + button("page-next", "Next", ButtonVariant::Outline, cx) + .disabled(!pager.has_next()) + .on_click(on_next), + ), + ) +} + +pub fn selection_state(selected_on_page: usize, page_len: usize) -> CheckboxState { + if page_len > 0 && selected_on_page == page_len { + CheckboxState::Checked + } else if selected_on_page > 0 { + CheckboxState::Indeterminate + } else { + CheckboxState::Unchecked + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConfirmEvent { + Confirm, + Cancel, +} + +/// A yes/no dialog for destructive actions. +pub struct ConfirmDialog { + title: SharedString, + message: SharedString, + confirm_label: SharedString, + focus: ModalFocus, +} + +impl EventEmitter for ConfirmDialog {} +impl ConfirmDialog { + pub fn new( + title: impl Into, + message: impl Into, + confirm_label: impl Into, + cx: &mut Context, + ) -> Self { + Self { title: title.into(), message: message.into(), confirm_label: confirm_label.into(), focus: ModalFocus::new(cx) } + } +} + +impl Render for ConfirmDialog { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.focus.ensure(window, cx); + let body = div() + .flex() + .flex_col() + .gap(rems(1.)) + .child(div().text_color(cx.omarchy().secondary).child(self.message.clone())) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("confirm-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Cancel))), + ) + .child( + button("confirm-ok", self.confirm_label.clone(), ButtonVariant::Danger, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Confirm))), + ), + ); + modal(&self.title, body, cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Cancel)), cx).track_focus(&self.focus.handle) + .on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(ConfirmEvent::Cancel))) + } +} + +pub fn plural(count: usize, singular: &str) -> String { + if count == 1 { + format!("{count} {singular}") + } else { + format!("{count} {singular}s") + } +} + +pub fn dim(text: impl Into, cx: &App) -> Div { + div().text_color(cx.omarchy().secondary).child(text.into()) +} + +/// A table cell: fixed width in rems, or flexible when `None`. +pub fn cell(width: Option, child: impl IntoElement) -> Div { + let base = div().min_w_0().px(rems(0.5)); + match width { + Some(w) => base.w(rems(w)).flex_shrink_0().child(child), + None => base.flex_1().child(child), + } +} + +/// Previous / range / Next, for lists with a fixed page size. +pub fn simple_pager( + pager: Pager, + on_prev: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_next: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> Div { + div() + .flex() + .items_center() + .justify_end() + .gap(rems(0.75)) + .child(button("pager-prev", "Previous", ButtonVariant::Outline, cx).disabled(!pager.has_prev()).on_click(on_prev)) + .child(dim(pager.label(), cx)) + .child(button("pager-next", "Next", ButtonVariant::Outline, cx).disabled(!pager.has_next()).on_click(on_next)) +} + +/// Keyboard focus for a dialog that has no text field of its own, so Escape +/// still reaches it. Call [`ModalFocus::ensure`] from `render`. +pub struct ModalFocus { + pub handle: gpui_kit::FocusHandle, + taken: bool, +} + +impl ModalFocus { + pub fn new(cx: &mut Context) -> Self { + Self { handle: cx.focus_handle(), taken: false } + } + + pub fn ensure(&mut self, window: &mut Window, cx: &mut Context) { + if !self.taken { + self.taken = true; + self.handle.focus(window, cx); + } + } +} + +/// Run `submit` when Enter is pressed in any of `inputs`. +pub fn submit_on_enter( + inputs: &[&Entity], + submit: fn(&mut V, &mut Context), + cx: &mut Context, +) -> Vec { + use gpui_kit::base::input::InputEvent; + inputs + .iter() + .map(|input| { + cx.subscribe(*input, move |this, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::PressEnter { .. }) { + submit(this, cx); + } + }) + }) + .collect() +} + +// ── logo ──────────────────────────────────────────────────────────── + +pub const ELEMENT_NUMBER: &str = "17"; +pub const ELEMENT_SYMBOL: &str = "Ch"; +pub const ELEMENT_NAME: &str = "Chlorine"; +pub const ELEMENT_MASS: &str = "35.45"; + +/// How much of the element tile to draw. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TileDetail { + /// Atomic number and symbol. + Compact, + /// Adds the element name along the bottom. + Named, + /// Adds the atomic mass in the top right corner as well. + Full, +} + +/// A periodic-table tile: atomic number top left, the symbol large in the +/// middle and the name along the bottom. It is set in the font chosen with +/// `omarchy font set` (read from fontconfig by gpui-omarchy), so it matches the +/// terminal and the rest of the desktop. `size` is the side length in rems. +pub fn element_tile(size: f32, detail: TileDetail, cx: &App) -> Div { + let t = cx.omarchy(); + let small = |factor: f32| rems((size * factor).max(0.5)); + div() + .relative() + .size(rems(size)) + .flex_shrink_0() + .border_1() + .border_color(t.accent) + .text_color(t.accent) + .font_family(t.mono_font.clone()) + .child( + div() + .absolute() + .top(rems(size * 0.05)) + .left(rems(size * 0.09)) + .text_size(small(0.19)) + .child(ELEMENT_NUMBER), + ) + .child( + div() + .size_full() + .flex() + .items_center() + .justify_center() + .when(detail != TileDetail::Compact, |symbol| symbol.pb(rems(size * 0.1))) + .text_size(rems(size * 0.4)) + .font_weight(FontWeight::BOLD) + .child(ELEMENT_SYMBOL), + ) + .when(detail == TileDetail::Full, |tile| { + tile.child( + div() + .absolute() + .top(rems(size * 0.05)) + .right(rems(size * 0.09)) + .text_size(small(0.15)) + .child(ELEMENT_MASS), + ) + }) + .when(detail != TileDetail::Compact, |tile| { + tile.child( + div() + .absolute() + .bottom(rems(size * 0.05)) + .w_full() + .flex() + .justify_center() + .text_size(small(0.15)) + .child(ELEMENT_NAME), + ) + }) +} + +#[cfg(test)] +mod logo_tests { + use super::*; + + #[test] + fn the_tile_reads_17_ch_chlorine() { + assert_eq!(ELEMENT_NUMBER, "17"); + assert_eq!(ELEMENT_SYMBOL, "Ch"); + assert_eq!(ELEMENT_NAME, "Chlorine"); + assert_eq!(ELEMENT_MASS, "35.45"); + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 20fb0a0..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src"] -} diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 656069f..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -const host = process.env.TAURI_DEV_HOST; - -export default defineConfig(async () => ({ - plugins: [react()], - clearScreen: false, - server: { - port: 1420, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - ignored: ["**/src-tauri/**"], - }, - }, -})); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 7ed11ae..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "jsdom", - include: ["src/**/*.test.ts", "src/**/*.test.tsx"], - setupFiles: [], - }, -});