diff --git a/.github/workflows/native-beta-android.yml b/.github/workflows/native-beta-android.yml new file mode 100644 index 00000000..5c753ade --- /dev/null +++ b/.github/workflows/native-beta-android.yml @@ -0,0 +1,89 @@ +name: Native 3.0 Android Beta + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: native-3-android-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-android: + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest-8-cores + env: + MAPLE_API_ENV: dev + MAPLE_CI_BUILD_ROOT: ${{ github.workspace }}/native/target/maple-ci + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android + + - name: Cache Rust artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + native -> target + native/rmp-cli -> target + + - name: Setup Android NDK + uses: nttld/setup-ndk@v1.5.0 + id: setup-ndk + with: + ndk-version: r28c + add-to-path: true + local-cache: true + + - name: Install native Android build tools + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev pkg-config + cargo install cargo-ndk --locked + + - name: Configure Android signing + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/maple-upload.jks" + cat > native/android/keystore.properties < target + native/rmp-cli -> target + + - name: Set up App Store Connect API key + env: + APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + run: | + mkdir -p "$RUNNER_TEMP/app-store-connect" + echo "$APPLE_API_PRIVATE_KEY" | base64 --decode > "$RUNNER_TEMP/app-store-connect/AuthKey_${APPLE_API_KEY}.p8" + chmod 600 "$RUNNER_TEMP/app-store-connect/AuthKey_${APPLE_API_KEY}.p8" + echo "APPLE_API_KEY_PATH=$RUNNER_TEMP/app-store-connect/AuthKey_${APPLE_API_KEY}.p8" >> "$GITHUB_ENV" + + - name: Build and upload native iOS beta + env: + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_PATH: ${{ env.APPLE_API_KEY_PATH }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: bash native/scripts/ci/build-ios-testflight.sh + + - name: Upload iOS artifacts + uses: actions/upload-artifact@v4 + with: + name: native-ios-beta + path: | + native/target/maple-ci/ios/export/*.ipa + retention-days: 7 diff --git a/.github/workflows/native-beta-linux.yml b/.github/workflows/native-beta-linux.yml new file mode 100644 index 00000000..51a573f5 --- /dev/null +++ b/.github/workflows/native-beta-linux.yml @@ -0,0 +1,57 @@ +name: Native 3.0 Linux Beta + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: native-3-linux-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-linux: + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest-8-cores + env: + MAPLE_API_ENV: dev + MAPLE_CI_BUILD_ROOT: ${{ github.workspace }}/native/target/maple-ci + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + native -> target + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Install Linux desktop build tools + env: + CARGO_CFG_TARGET_OS: linux + run: cargo install tauri-cli --version 2.9.2 --locked + + - name: Build Linux beta artifacts + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: bash native/scripts/ci/package-linux.sh + + - name: Upload Linux artifacts + uses: actions/upload-artifact@v4 + with: + name: native-desktop-linux-beta + path: | + native/target/maple-ci/desktop-linux/*.AppImage + native/target/maple-ci/desktop-linux/*.deb + native/target/maple-ci/desktop-linux/*.sig + retention-days: 7 diff --git a/.github/workflows/native-beta-macos.yml b/.github/workflows/native-beta-macos.yml new file mode 100644 index 00000000..654c85dd --- /dev/null +++ b/.github/workflows/native-beta-macos.yml @@ -0,0 +1,95 @@ +name: Native 3.0 macOS Beta + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: native-3-macos-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-macos: + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false + runs-on: macos-26-xlarge + env: + MAPLE_API_ENV: dev + MAPLE_CI_BUILD_ROOT: ${{ github.workspace }}/native/target/maple-ci + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.2" + + - name: Install xcodegen + run: brew install xcodegen + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin,aarch64-apple-ios,aarch64-apple-ios-sim + + - name: Cache Rust artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + native -> target + native/rmp-cli -> target + + - name: Install desktop packaging tools + env: + CARGO_CFG_TARGET_OS: macos + run: cargo install tauri-cli --version 2.9.2 --locked + + - name: Import Apple Developer certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + echo "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/maple-desktop-cert.p12" + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import "$RUNNER_TEMP/maple-desktop-cert.p12" -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + CERT_ID=$(security find-identity -v -p codesigning build.keychain | awk -F '"' '/Developer ID Application/ { print $2; exit }') + echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV" + + - name: Set up App Store Connect API key + env: + APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + run: | + mkdir -p "$RUNNER_TEMP/app-store-connect" + echo "$APPLE_API_PRIVATE_KEY" | base64 --decode > "$RUNNER_TEMP/app-store-connect/AuthKey_${APPLE_API_KEY}.p8" + chmod 600 "$RUNNER_TEMP/app-store-connect/AuthKey_${APPLE_API_KEY}.p8" + echo "APPLE_API_KEY_PATH=$RUNNER_TEMP/app-store-connect/AuthKey_${APPLE_API_KEY}.p8" >> "$GITHUB_ENV" + + - name: Build notarized macOS beta artifacts + env: + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_PATH: ${{ env.APPLE_API_KEY_PATH }} + APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: bash native/scripts/ci/package-macos.sh + + - name: Upload macOS artifacts + uses: actions/upload-artifact@v4 + with: + name: native-desktop-macos-beta + path: | + native/target/maple-ci/desktop-macos/*.dmg + native/target/maple-ci/desktop-macos/*.zip + native/target/maple-ci/desktop-macos/*.sig + retention-days: 7 diff --git a/.gitignore b/.gitignore index 04321fd8..fd7f6c4d 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,14 @@ frontend/*.local # iOS build backups frontend/src-tauri/gen/apple/maple.xcodeproj/project.pbxproj.backup + +# Native (3.0) build artifacts +native/target/ +native/rmp-cli/target/ +native/android/app/build/ +native/android/.gradle/ +native/ios/App.xcodeproj/ +native/ios/Frameworks/ +native/ios/Bindings/ + +.factory/ diff --git a/justfile b/justfile index de9e4810..fe3cd709 100644 --- a/justfile +++ b/justfile @@ -207,3 +207,90 @@ release version: git commit -m "chore: bump version to {{version}}" git tag -a "v{{version}}" -m "Release v{{version}}" echo "Release v{{version}} created! Don't forget to push tags: git push && git push --tags" + +# ┌─────────────────────────────────────────────────────────────────────┐ +# │ Maple 3.0 (native/) - Native Rust/SwiftUI/Compose/GTK4 │ +# └─────────────────────────────────────────────────────────────────────┘ + +native_nix := "cd native && nix develop --command bash -c" + +# Run native rmp doctor +native-doctor: + {{native_nix}} "rmp doctor" + +# Regenerate native FFI bindings (Swift + Kotlin) (api_env: local|dev|prod) +native-bindings api_env="local": + {{native_nix}} "unset CC CXX AR RANLIB && rmp bindings all" + +# Regenerate native Swift FFI bindings (api_env: local|dev|prod) +native-bindings-swift api_env="local": + {{native_nix}} "unset CC CXX AR RANLIB && rmp bindings swift" + +# Regenerate native Kotlin FFI bindings (api_env: local|dev|prod) +native-bindings-kotlin api_env="local": + {{native_nix}} "unset CC CXX AR RANLIB && rmp bindings kotlin" + +# Build native desktop for the current host platform (api_env: local|dev|prod) +native-build-desktop api_env="local": + just -f {{justfile_directory()}}/native/justfile build-desktop "{{api_env}}" + +# Run native desktop for the current host platform (api_env: local|dev|prod) +native-run-desktop api_env="local": + just -f {{justfile_directory()}}/native/justfile run-desktop "{{api_env}}" + +# Run native iOS on simulator (api_env: local|dev|prod) +native-run-ios api_env="local": + cd native && api_env="{{api_env}}"; while [[ "$api_env" == api_env=* ]]; do api_env="${api_env#api_env=}"; done; api_url="http://0.0.0.0:3000"; if [ "$api_env" = "dev" ]; then api_url="https://enclave.secretgpt.ai"; elif [ "$api_env" = "prod" ]; then api_url="https://enclave.trymaple.ai"; fi; OPEN_SECRET_API_URL="$api_url" nix develop --command bash -c "unset CC CXX AR RANLIB && rmp run ios" + +# Run native Android on emulator (api_env: local|dev|prod) +native-run-android serial="emulator-5554" api_env="local": + cd native && api_env="{{api_env}}"; while [[ "$api_env" == api_env=* ]]; do api_env="${api_env#api_env=}"; done; api_url="http://0.0.0.0:3000"; if [ "$api_env" = "dev" ]; then api_url="https://enclave.secretgpt.ai"; elif [ "$api_env" = "prod" ]; then api_url="https://enclave.trymaple.ai"; fi; OPEN_SECRET_API_URL="$api_url" nix develop --command bash -c "unset CC CXX AR RANLIB && export PATH=\$HOME/.cargo/bin:\$PATH && rmp run android --serial {{serial}}" + +# Build native Android APK +native-build-android api_env="local": + cd native/android && api_env="{{api_env}}"; while [[ "$api_env" == api_env=* ]]; do api_env="${api_env#api_env=}"; done; api_url="http://0.0.0.0:3000"; if [ "$api_env" = "dev" ]; then api_url="https://enclave.secretgpt.ai"; elif [ "$api_env" = "prod" ]; then api_url="https://enclave.trymaple.ai"; fi; OPEN_SECRET_API_URL="$api_url" ./gradlew assembleDebug + +# Build all native platforms +native-build-all api_env="local": + just native-build-desktop "{{api_env}}" + just native-build-android "{{api_env}}" + @echo "Native desktop and Android built. iOS is build+run only via rmp." + +# Run all native platforms +native-run-all serial="emulator-5554" api_env="local": + just native-bindings "{{api_env}}" + just native-run-ios "{{api_env}}" + just native-run-android serial={{serial}} "{{api_env}}" + just native-run-desktop "{{api_env}}" + +# ┌─────────────────────────────────────────────────────────────────────┐ +# │ OrbStack → macOS wrappers (run from Linux container) │ +# │ Usage: just mac-native-run-ios │ +# └─────────────────────────────────────────────────────────────────────┘ + +mac-native-run-ios api_env="local": + mac just -f {{justfile_directory()}}/native/justfile run-ios "{{api_env}}" + +mac-native-run-android serial="emulator-5554" api_env="local": + mac just -f {{justfile_directory()}}/native/justfile run-android serial={{serial}} "{{api_env}}" + +mac-native-run-desktop api_env="local": + mac just -f {{justfile_directory()}}/native/justfile run-desktop "{{api_env}}" + +mac-native-build-desktop api_env="local": + mac just -f {{justfile_directory()}}/native/justfile build-desktop "{{api_env}}" + +mac-native-build-android api_env="local": + mac just -f {{justfile_directory()}}/native/justfile build-android "{{api_env}}" + +mac-native-bindings api_env="local": + mac just -f {{justfile_directory()}}/native/justfile bindings "{{api_env}}" + +mac-native-bindings-swift api_env="local": + mac just -f {{justfile_directory()}}/native/justfile bindings-swift "{{api_env}}" + +mac-native-bindings-kotlin api_env="local": + mac just -f {{justfile_directory()}}/native/justfile bindings-kotlin "{{api_env}}" + +mac-native-doctor: + mac just -f {{justfile_directory()}}/native/justfile doctor diff --git a/native/.env.example b/native/.env.example new file mode 100644 index 00000000..d949397c --- /dev/null +++ b/native/.env.example @@ -0,0 +1,3 @@ +MAPLE_BILLING_API_URL=http://0.0.0.0:3001 +OPEN_SECRET_API_URL=http://0.0.0.0:3000 +CLIENT_ID=ba5a14b5-d915-47b1-b7b1-afda52bc5fc6 diff --git a/native/.gitignore b/native/.gitignore new file mode 100644 index 00000000..a5d5903c --- /dev/null +++ b/native/.gitignore @@ -0,0 +1,18 @@ +/target +.env +.direnv/ +.DS_Store +*.swp +*.swo +flake.lock +staging/ +ios/Bindings/ +ios/Frameworks/ +ios/.build/ +ios/*.xcodeproj +android/app/src/main/jniLibs/ +android/.gradle/ +android/build/ +android/app/build/ +android/local.properties +emulator.log diff --git a/native/Cargo.lock b/native/Cargo.lock new file mode 100644 index 00000000..7aa9f0f8 --- /dev/null +++ b/native/Cargo.lock @@ -0,0 +1,3583 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cairo-rs" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "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 = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "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", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "graphene-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "linux-keyutils", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "maple_core" +version = "3.0.0" +dependencies = [ + "base64", + "chrono", + "flume", + "futures", + "opensecret", + "reqwest", + "serde_cbor", + "serde_json", + "tokio", + "uniffi", + "uuid", +] + +[[package]] +name = "maple_desktop_gtk" +version = "3.0.0" +dependencies = [ + "dirs-next", + "dotenvy", + "flume", + "gtk4", + "keyring", + "maple_core", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[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 = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "opensecret" +version = "3.0.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbacacf2d939e25ce433c6a926630c2cc650af4e1431cc1e407f14a53f65e5ca" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bytes", + "chacha20poly1305", + "chrono", + "eventsource-stream", + "futures", + "getrandom 0.2.17", + "hex", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "reqwest", + "ring", + "serde", + "serde_cbor", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", + "x25519-dalek", + "x509-parser", + "yasna", +] + +[[package]] +name = "pango" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[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 = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "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 = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "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 = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "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 = "system-deps" +version = "7.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uniffi" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c6dec3fc6645f71a16a3fa9ff57991028153bd194ca97f4b55e610c73ce66a" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi-bindgen" +version = "0.1.0" +dependencies = [ + "uniffi", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed0150801958d4825da56a41c71f000a457ac3a4613fa9647df78ac4b6b6881" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ef62e69762fbb9386dcb6c87cd3dd05d525fa8a3a579a290892e60ddbda47e" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f51ebca0d9a4b2aa6c644d5ede45c56f73906b96403c08a1985e75ccb64a01" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9d12529f1223d014fd501e5f29ca0884d15d6ed5ddddd9f506e55350327dc3" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df6d413db2827c68588f8149d30d49b71d540d46539e435b23a7f7dbd4d4f86" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a806dddc8208f22efd7e95a5cdf88ed43d0f3271e8f63b47e757a8bbdb43b63a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d1a7339539bf6f6fa3e9b534dece13f778bda2d54b1a6d4e40b4d6090ac26e7" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[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 = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" +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 = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[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", + "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_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.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.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.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.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.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.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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/native/Cargo.toml b/native/Cargo.toml new file mode 100644 index 00000000..8996fa78 --- /dev/null +++ b/native/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] +resolver = "2" +members = [ + "rust", + "uniffi-bindgen", + "desktop/gtk", +] +exclude = [ + "rmp-cli", +] diff --git a/native/README.md b/native/README.md new file mode 100644 index 00000000..74a912ea --- /dev/null +++ b/native/README.md @@ -0,0 +1,101 @@ +# Maple 3.0 (Native) + +Native cross-platform AI agent chat app built with Rust core + platform-native UIs. + +## Architecture + +``` +native/ +├── rust/ # Shared Rust core (maple_core) - all business logic +│ ├── src/lib.rs # AppState, actions, reducers, SSE streaming, pagination +│ └── uniffi.toml # FFI bindings config (cloud.opensecret.maple.rust) +├── ios/ # SwiftUI frontend (iOS 26+, liquid glass) +│ ├── Sources/ # ContentView, AppManager, MapleTheme, MapleWordmark +│ └── project.yml # XcodeGen project (cloud.opensecret.maple) +├── android/ # Jetpack Compose frontend (Material 3) +│ └── app/src/main/java/cloud/opensecret/maple/ +├── desktop/iced/ # iced 0.14 desktop frontend +│ └── src/main.rs # Single-file app with theme.rs +├── rmp.toml # RMP project config +├── justfile # Build commands (also accessible from repo root) +└── Cargo.toml # Workspace: rust, uniffi-bindgen, desktop/iced +``` + +**Design principle**: All logic lives in `rust/src/lib.rs`. Platforms are thin +renderers that dispatch `AppAction`s and render `AppState`. No business logic +in Swift/Kotlin/Rust-iced code. + +See `rmp-architecture-bible.md` for the full RMP framework reference. + +## Bundle IDs & Versions + +| Platform | Bundle ID | Version | +|----------|-----------|---------| +| iOS | `cloud.opensecret.maple` (debug: `.dev` suffix) | 3.0.0 | +| Android | `cloud.opensecret.maple` (debug: `.dev` suffix) | 3.0.0 (versionCode: 3000000000) | +| Desktop | `cloud.opensecret.maple.desktop` (keyring) | 3.0.0 | + +These match Maple 2.0's App Store / Play Store listings for upgrade continuity. +Team ID: `X773Y823TN`. + +## Dependencies + +- **OpenSecret SDK**: `opensecret = "3.0.0-alpha.0"` from crates.io +- **FFI**: UniFFI 0.31 (generates Swift + Kotlin bindings) +- **Desktop**: iced 0.14 with tokio + canvas features +- **Build tooling**: RMP CLI, Nix flake, XcodeGen + +## Quick Start + +From the repo root (`maple/`): + +```bash +# Run on each platform +just native-run-ios +just native-run-android +just native-run-desktop + +# Regenerate FFI bindings after changing rust/src/lib.rs +just native-bindings + +# Or from inside native/ directly +just run-ios +just run-android +just run-desktop +``` + +From OrbStack (Linux container with macOS host): + +```bash +mac just -f /path/to/maple/native/justfile run-ios +mac just -f /path/to/maple/native/justfile run-android +# Desktop builds natively on mac: +mac just -f /path/to/maple/native/justfile run-desktop +``` + +## Key Features + +- **Single-agent chat** with SSE streaming via OpenSecret API +- **Session persistence** (keychain on iOS, EncryptedSharedPreferences on Android, keyring on desktop) +- **Cursor-based pagination** (20 messages per page, prefetch within 3 of top) +- **iMessage-style timestamps** with smart grouping (5+ min gap threshold) +- **Settings** with delete agent + sign out +- **OAuth** (GitHub, Google, Apple) + email/password auth +- **Splash screen** with radial gradient and SVG wordmark + +## Design System + +- **iOS**: Native iOS 26 Liquid Glass (`.glassEffect()`, `GlassEffectContainer`) +- **Android**: Simulated frosted glass (translucent containers, scale animations) +- **Desktop**: Simulated frosted glass (translucent containers, subtle borders/shadows) +- **Brand**: Manrope body font, Array-Bold display font, MPL app icon +- **Colors**: Maple (primary), Pebble (secondary), Bark (tertiary), Grove, Neutral +- **Bubbles**: User = Maple 400→600 gradient, Agent = Pebble-50 + +## Relationship to Maple 2.0 + +Maple 2.0 lives in `frontend/` (Tauri + React/TypeScript web app). +Maple 3.0 lives in `native/` (pure native apps). + +Both share the same bundle IDs and App Store listings. When 3.0 is ready, +it will replace 2.0 as the shipped product. Until then, both coexist. diff --git a/native/android/app/build.gradle.kts b/native/android/app/build.gradle.kts new file mode 100644 index 00000000..4739db27 --- /dev/null +++ b/native/android/app/build.gradle.kts @@ -0,0 +1,135 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +fun String.toKotlinStringLiteral(): String = + "\"${replace("\\", "\\\\").replace("\"", "\\\"")}\"" + +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("keystore.properties") + +if (keystorePropertiesFile.exists()) { + keystorePropertiesFile.inputStream().use(keystoreProperties::load) +} + +val openSecretApiUrl = providers.gradleProperty("openSecretApiUrl") + .orElse(providers.environmentVariable("OPEN_SECRET_API_URL")) + .getOrElse("") + +val mapleVersionName = providers.gradleProperty("mapleVersionName") + .orElse(providers.environmentVariable("MAPLE_ANDROID_VERSION_NAME")) + .orNull + ?: "3.0.0" + +val mapleVersionCode = providers.gradleProperty("mapleVersionCode") + .orElse(providers.environmentVariable("MAPLE_ANDROID_VERSION_CODE")) + .orNull + ?.toInt() + ?: 300000000 + +android { + namespace = "cloud.opensecret.maple" + compileSdk = 35 + ndkVersion = "28.2.13676358" + + defaultConfig { + applicationId = "cloud.opensecret.maple" + minSdk = 26 + targetSdk = 35 + versionCode = mapleVersionCode + versionName = mapleVersionName + buildConfigField("String", "OPEN_SECRET_API_URL", openSecretApiUrl.toKotlinStringLiteral()) + } + + signingConfigs { + if (keystorePropertiesFile.exists()) { + create("release") { + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("password") + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("password") + } + } + } + + buildTypes { + debug { + applicationIdSuffix = ".dev" + versionNameSuffix = "-dev" + } + release { + isMinifyEnabled = false + signingConfig = signingConfigs.findByName("release") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + ) + } + } + + buildFeatures { + compose = true + buildConfig = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + packaging { + resources.excludes.addAll( + listOf("/META-INF/{AL2.0,LGPL2.1}", "META-INF/DEPENDENCIES"), + ) + } + + sourceSets { + getByName("main") { + jniLibs.srcDirs("src/main/jniLibs") + } + } +} + +tasks.register("ensureUniffiGenerated") { + doLast { + val out = file("src/main/java/cloud/opensecret/maple/rust/maple_core.kt") + if (!out.exists()) { + throw GradleException("Missing UniFFI Kotlin bindings. Run `rmp bindings kotlin` first.") + } + } +} + +tasks.named("preBuild") { + dependsOn("ensureUniffiGenerated") +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2024.06.00") + implementation(composeBom) + + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.activity:activity-compose:1.9.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3") + + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + + debugImplementation("androidx.compose.ui:ui-tooling") + + // UniFFI JNA + implementation("net.java.dev.jna:jna:5.14.0@aar") + + // Secure credential storage + implementation("androidx.security:security-crypto:1.1.0-alpha06") +} diff --git a/native/android/app/src/main/AndroidManifest.xml b/native/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..360936d0 --- /dev/null +++ b/native/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt b/native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt new file mode 100644 index 00000000..3943b9eb --- /dev/null +++ b/native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt @@ -0,0 +1,128 @@ +package cloud.opensecret.maple + +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import cloud.opensecret.maple.rust.AppAction +import cloud.opensecret.maple.rust.AppReconciler +import cloud.opensecret.maple.rust.AppState +import cloud.opensecret.maple.rust.AppUpdate +import cloud.opensecret.maple.rust.AuthState +import cloud.opensecret.maple.rust.FfiApp +import cloud.opensecret.maple.rust.Router +import cloud.opensecret.maple.rust.Screen + +class AppManager private constructor(context: Context) : AppReconciler { + private val mainHandler = Handler(Looper.getMainLooper()) + private val rust: FfiApp + private var lastRevApplied: ULong = 0UL + + private val securePrefs = EncryptedSharedPreferences.create( + context.applicationContext, + "maple_secure_prefs", + MasterKey.Builder(context.applicationContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + + var state: AppState by mutableStateOf( + AppState( + rev = 0UL, + auth = AuthState.Initializing, + pendingAuthUrl = null, + router = Router( + defaultScreen = Screen.LOADING, + screenStack = emptyList(), + ), + messages = emptyList(), + isAgentTyping = false, + isLoadingHistory = false, + hasOlderMessages = false, + composeText = "", + toast = null, + showSettings = false, + confirmDeleteAgent = false, + isDeletingAgent = false, + ), + ) + private set + + private fun configuredApiUrl(): String { + val configured = BuildConfig.OPEN_SECRET_API_URL.trim() + val apiUrl = if (configured.isNotEmpty()) configured else "http://0.0.0.0:3000" + + return if (apiUrl == "http://0.0.0.0:3000") "http://10.0.2.2:3000" else apiUrl + } + + init { + val dataDir = context.filesDir.absolutePath + + val apiUrl = configuredApiUrl() + val clientId = "ba5a14b5-d915-47b1-b7b1-afda52bc5fc6" + + rust = FfiApp(apiUrl = apiUrl, clientId = clientId, dataDir = dataDir) + val initial = rust.state() + state = initial + lastRevApplied = initial.rev + rust.listenForUpdates(this) + + // Attempt session restore from EncryptedSharedPreferences + val access = securePrefs.getString("access_token", null) + val refresh = securePrefs.getString("refresh_token", null) + if (access != null && refresh != null) { + rust.dispatch(AppAction.RestoreSession(accessToken = access, refreshToken = refresh)) + } else { + if (access != null || refresh != null) { + securePrefs.edit().remove("access_token").remove("refresh_token").apply() + } + rust.dispatch(AppAction.CompleteStartup) + } + } + + fun dispatch(action: AppAction) { + rust.dispatch(action) + } + + override fun reconcile(update: AppUpdate) { + mainHandler.post { + when (update) { + is AppUpdate.SessionTokens -> { + // Persist side-effect BEFORE rev guard (per bible 6.6) + if (update.accessToken.isEmpty()) { + securePrefs.edit().remove("access_token").remove("refresh_token").apply() + } else { + securePrefs.edit() + .putString("access_token", update.accessToken) + .putString("refresh_token", update.refreshToken) + .apply() + } + if (update.rev > lastRevApplied) { + lastRevApplied = update.rev + } + } + is AppUpdate.FullState -> { + if (update.v1.rev <= lastRevApplied) return@post + lastRevApplied = update.v1.rev + state = update.v1 + } + } + } + } + + companion object { + @Volatile + private var instance: AppManager? = null + + fun getInstance(context: Context): AppManager = + instance ?: synchronized(this) { + instance ?: AppManager(context.applicationContext).also { instance = it } + } + } +} diff --git a/native/android/app/src/main/java/cloud/opensecret/maple/MainActivity.kt b/native/android/app/src/main/java/cloud/opensecret/maple/MainActivity.kt new file mode 100644 index 00000000..2d0c3db9 --- /dev/null +++ b/native/android/app/src/main/java/cloud/opensecret/maple/MainActivity.kt @@ -0,0 +1,35 @@ +package cloud.opensecret.maple + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import cloud.opensecret.maple.ui.MainApp +import cloud.opensecret.maple.ui.theme.AppTheme + +class MainActivity : ComponentActivity() { + private lateinit var manager: AppManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + manager = AppManager.getInstance(applicationContext) + setContent { + val darkTheme = isSystemInDarkTheme() + val view = LocalView.current + + SideEffect { + WindowCompat.getInsetsController(window, view).apply { + isAppearanceLightStatusBars = !darkTheme + isAppearanceLightNavigationBars = !darkTheme + } + } + + AppTheme(darkTheme = darkTheme) { + MainApp(manager = manager) + } + } + } +} diff --git a/native/android/app/src/main/java/cloud/opensecret/maple/rust/maple_core.kt b/native/android/app/src/main/java/cloud/opensecret/maple/rust/maple_core.kt new file mode 100644 index 00000000..2373a138 --- /dev/null +++ b/native/android/app/src/main/java/cloud/opensecret/maple/rust/maple_core.kt @@ -0,0 +1,2436 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package cloud.opensecret.maple.rust + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.IntegerType +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.Callback +import com.sun.jna.ptr.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +/** + * @suppress + */ +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + // Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values. + // When dealing with these fields, make sure to call `toULong()`. + @JvmField var capacity: Long = 0 + @JvmField var len: Long = 0 + @JvmField var data: Pointer? = null + + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference + + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + + companion object { + internal fun alloc(size: ULong = 0UL) = uniffiRustCall() { status -> + // Note: need to convert the size to a `Long` value to make this work with JVM. + UniffiLib.ffi_maple_core_rustbuffer_alloc(size.toLong(), status) + }.also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + + internal fun create(capacity: ULong, len: ULong, data: Pointer?): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity.toLong() + buf.len = len.toLong() + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = uniffiRustCall() { status -> + UniffiLib.ffi_maple_core_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len)?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +internal open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} +/** + * The FfiConverter interface handles converter types to and from the FFI + * + * All implementing objects should be public to support external types. When a + * type is external we need to import it's FfiConverter. + * + * @suppress + */ +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): ULong + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position().toLong()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +/** + * FfiConverter that uses `RustBuffer` as the FfiType + * + * @suppress + */ +public interface FfiConverterRustBuffer: FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + +@Structure.FieldOrder("code", "error_buf") +internal open class UniffiRustCallStatus : Structure() { + @JvmField var code: Byte = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue: UniffiRustCallStatus(), Structure.ByValue + + fun isSuccess(): Boolean { + return code == UNIFFI_CALL_SUCCESS + } + + fun isError(): Boolean { + return code == UNIFFI_CALL_ERROR + } + + fun isPanic(): Boolean { + return code == UNIFFI_CALL_UNEXPECTED_ERROR + } + + companion object { + fun create(code: Byte, errorBuf: RustBuffer.ByValue): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } +} + +class InternalException(message: String) : kotlin.Exception(message) + +/** + * Each top-level error class has a companion object that can lift the error from the call status's rust buffer + * + * @suppress + */ +interface UniffiRustCallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun uniffiRustCallWithError(errorHandler: UniffiRustCallStatusErrorHandler, callback: (UniffiRustCallStatus) -> U): U { + var status = UniffiRustCallStatus() + val return_value = callback(status) + uniffiCheckCallStatus(errorHandler, status) + return return_value +} + +// Check UniffiRustCallStatus and throw an error if the call wasn't successful +private fun uniffiCheckCallStatus(errorHandler: UniffiRustCallStatusErrorHandler, status: UniffiRustCallStatus) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +/** + * UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR + * + * @suppress + */ +object UniffiNullRustCallStatusErrorHandler: UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun uniffiRustCall(callback: (UniffiRustCallStatus) -> U): U { + return uniffiRustCallWithError(UniffiNullRustCallStatusErrorHandler, callback) +} + +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + val err = try { e.stackTraceToString() } catch(_: Throwable) { "" } + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(err) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + val err = try { e.stackTraceToString() } catch(_: Throwable) { "" } + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(err) + } + } +} +// Initial value and increment amount for handles. +// These ensure that Kotlin-generated handles always have the lowest bit set +private const val UNIFFI_HANDLEMAP_INITIAL = 1.toLong() +private const val UNIFFI_HANDLEMAP_DELTA = 2.toLong() + +// Map handles to objects +// +// This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. +internal class UniffiHandleMap { + private val map = ConcurrentHashMap() + // Start + private val counter = java.util.concurrent.atomic.AtomicLong(UNIFFI_HANDLEMAP_INITIAL) + + val size: Int + get() = map.size + + // Insert a new object into the handle map and get a handle for it + fun insert(obj: T): Long { + val handle = counter.getAndAdd(UNIFFI_HANDLEMAP_DELTA) + map.put(handle, obj) + return handle + } + + // Clone a handle, creating a new one + fun clone(handle: Long): Long { + val obj = map.get(handle) ?: throw InternalException("UniffiHandleMap.clone: Invalid handle") + return insert(obj) + } + + // Get an object from the handle map + fun get(handle: Long): T { + return map.get(handle) ?: throw InternalException("UniffiHandleMap.get: Invalid handle") + } + + // Remove an entry from the handlemap and get the Kotlin object back + fun remove(handle: Long): T { + return map.remove(handle) ?: throw InternalException("UniffiHandleMap: Invalid handle") + } +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "maple_core" +} + +// Define FFI callback types +internal interface UniffiRustFutureContinuationCallback : com.sun.jna.Callback { + fun callback(`data`: Long,`pollResult`: Byte,) +} +internal interface UniffiForeignFutureDroppedCallback : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +internal interface UniffiCallbackInterfaceFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +internal interface UniffiCallbackInterfaceClone : com.sun.jna.Callback { + fun callback(`handle`: Long,) + : Long +} +@Structure.FieldOrder("handle", "free") +internal open class UniffiForeignFutureDroppedCallbackStruct( + @JvmField internal var `handle`: Long = 0.toLong(), + @JvmField internal var `free`: UniffiForeignFutureDroppedCallback? = null, +) : Structure() { + class UniffiByValue( + `handle`: Long = 0.toLong(), + `free`: UniffiForeignFutureDroppedCallback? = null, + ): UniffiForeignFutureDroppedCallbackStruct(`handle`,`free`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureDroppedCallbackStruct) { + `handle` = other.`handle` + `free` = other.`free` + } + +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultU8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultU8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultU8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultU8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultI8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultI8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultI8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultI8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultU16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultU16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultU16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultU16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultI16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultI16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultI16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultI16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultU32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultU32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultU32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultU32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultI32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultI32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultI32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultI32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultU64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultU64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultU64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultU64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultI64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultI64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultI64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultI64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultF32( + @JvmField internal var `returnValue`: Float = 0.0f, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Float = 0.0f, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultF32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultF32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultF32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultF64( + @JvmField internal var `returnValue`: Double = 0.0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Double = 0.0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultF64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultF64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultF64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureResultRustBuffer( + @JvmField internal var `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultRustBuffer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultRustBuffer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteRustBuffer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultRustBuffer.UniffiByValue,) +} +@Structure.FieldOrder("callStatus") +internal open class UniffiForeignFutureResultVoid( + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureResultVoid(`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureResultVoid) { + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureResultVoid.UniffiByValue,) +} +internal interface UniffiCallbackInterfaceAppReconcilerMethod0 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`update`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +@Structure.FieldOrder("uniffiFree", "uniffiClone", "reconcile") +internal open class UniffiVTableCallbackInterfaceAppReconciler( + @JvmField internal var `uniffiFree`: UniffiCallbackInterfaceFree? = null, + @JvmField internal var `uniffiClone`: UniffiCallbackInterfaceClone? = null, + @JvmField internal var `reconcile`: UniffiCallbackInterfaceAppReconcilerMethod0? = null, +) : Structure() { + class UniffiByValue( + `uniffiFree`: UniffiCallbackInterfaceFree? = null, + `uniffiClone`: UniffiCallbackInterfaceClone? = null, + `reconcile`: UniffiCallbackInterfaceAppReconcilerMethod0? = null, + ): UniffiVTableCallbackInterfaceAppReconciler(`uniffiFree`,`uniffiClone`,`reconcile`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiVTableCallbackInterfaceAppReconciler) { + `uniffiFree` = other.`uniffiFree` + `uniffiClone` = other.`uniffiClone` + `reconcile` = other.`reconcile` + } + +} + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +// For large crates we prevent `MethodTooLargeException` (see #2340) +// N.B. the name of the extension is very misleading, since it is +// rather `InterfaceTooLargeException`, caused by too many methods +// in the interface for large crates. +// +// By splitting the otherwise huge interface into two parts +// * UniffiLib (this) +// * IntegrityCheckingUniffiLib +// And all checksum methods are put into `IntegrityCheckingUniffiLib` +// we allow for ~2x as many methods in the UniffiLib interface. +// +// Note: above all written when we used JNA's `loadIndirect` etc. +// We now use JNA's "direct mapping" - unclear if same considerations apply exactly. +internal object IntegrityCheckingUniffiLib { + init { + Native.register(IntegrityCheckingUniffiLib::class.java, findLibraryName(componentName = "maple_core")) + uniffiCheckContractApiVersion(this) + uniffiCheckApiChecksums(this) + } + external fun uniffi_maple_core_checksum_func_default_api_url( + ): Short + external fun uniffi_maple_core_checksum_method_ffiapp_dispatch( + ): Short + external fun uniffi_maple_core_checksum_method_ffiapp_listen_for_updates( + ): Short + external fun uniffi_maple_core_checksum_method_ffiapp_state( + ): Short + external fun uniffi_maple_core_checksum_constructor_ffiapp_new( + ): Short + external fun uniffi_maple_core_checksum_method_appreconciler_reconcile( + ): Short + external fun ffi_maple_core_uniffi_contract_version( + ): Int + + +} + +internal object UniffiLib { + + // The Cleaner for the whole library + internal val CLEANER: UniffiCleaner by lazy { + UniffiCleaner.create() + } + + + init { + Native.register(UniffiLib::class.java, findLibraryName(componentName = "maple_core")) + uniffiCallbackInterfaceAppReconciler.register(this) + + } + external fun uniffi_maple_core_fn_clone_ffiapp(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + external fun uniffi_maple_core_fn_free_ffiapp(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + external fun uniffi_maple_core_fn_constructor_ffiapp_new(`apiUrl`: RustBuffer.ByValue,`clientId`: RustBuffer.ByValue,`dataDir`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Long + external fun uniffi_maple_core_fn_method_ffiapp_dispatch(`ptr`: Long,`action`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + external fun uniffi_maple_core_fn_method_ffiapp_listen_for_updates(`ptr`: Long,`reconciler`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + external fun uniffi_maple_core_fn_method_ffiapp_state(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + external fun uniffi_maple_core_fn_init_callback_vtable_appreconciler(`vtable`: UniffiVTableCallbackInterfaceAppReconciler, + ): Unit + external fun uniffi_maple_core_fn_func_default_api_url(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + external fun ffi_maple_core_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + external fun ffi_maple_core_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + external fun ffi_maple_core_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + external fun ffi_maple_core_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + external fun ffi_maple_core_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_u8(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_u8(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + external fun ffi_maple_core_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_i8(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_i8(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + external fun ffi_maple_core_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_u16(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_u16(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + external fun ffi_maple_core_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_i16(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_i16(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + external fun ffi_maple_core_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_u32(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_u32(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + external fun ffi_maple_core_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_i32(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_i32(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + external fun ffi_maple_core_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_u64(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_u64(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + external fun ffi_maple_core_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_i64(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_i64(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + external fun ffi_maple_core_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_f32(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_f32(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Float + external fun ffi_maple_core_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_f64(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_f64(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Double + external fun ffi_maple_core_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_rust_buffer(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_rust_buffer(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + external fun ffi_maple_core_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + external fun ffi_maple_core_rust_future_cancel_void(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_free_void(`handle`: Long, + ): Unit + external fun ffi_maple_core_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + + +} + +private fun uniffiCheckContractApiVersion(lib: IntegrityCheckingUniffiLib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 30 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_maple_core_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { + if (lib.uniffi_maple_core_checksum_func_default_api_url() != 29239.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_maple_core_checksum_method_ffiapp_dispatch() != 27078.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_maple_core_checksum_method_ffiapp_listen_for_updates() != 63526.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_maple_core_checksum_method_ffiapp_state() != 13593.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_maple_core_checksum_constructor_ffiapp_new() != 52040.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_maple_core_checksum_method_appreconciler_reconcile() != 25035.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +/** + * @suppress + */ +public fun uniffiEnsureInitialized() { + IntegrityCheckingUniffiLib + // UniffiLib() initialized as objects are used, but we still need to explicitly + // reference it so initialization across crates works as expected. + UniffiLib +} + +// Async support + +// Public interface members begin here. + + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + for (arg in args) { + when (arg) { + is Disposable -> arg.destroy() + is ArrayList<*> -> { + for (idx in arg.indices) { + val element = arg[idx] + if (element is Disposable) { + element.destroy() + } + } + } + is Map<*, *> -> { + for (element in arg.values) { + if (element is Disposable) { + element.destroy() + } + } + } + is Iterable<*> -> { + for (element in arg) { + if (element is Disposable) { + element.destroy() + } + } + } + } + } + } + } +} + +/** + * @suppress + */ +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +/** + * Placeholder object used to signal that we're constructing an interface with a FFI handle. + * + * This is the first argument for interface constructors that input a raw handle. It exists is that + * so we can avoid signature conflicts when an interface has a regular constructor than inputs a + * Long. + * + * @suppress + * */ +object UniffiWithHandle + +/** + * Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. + * + * @suppress + * */ +object NoHandle// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +internal const val IDX_CALLBACK_FREE = 0 +// Callback return codes +internal const val UNIFFI_CALLBACK_SUCCESS = 0 +internal const val UNIFFI_CALLBACK_ERROR = 1 +internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 + +/** + * @suppress + */ +public abstract class FfiConverterCallbackInterface: FfiConverter { + internal val handleMap = UniffiHandleMap() + + internal fun drop(handle: Long) { + handleMap.remove(handle) + } + + override fun lift(value: Long): CallbackInterface { + return handleMap.get(value) + } + + override fun read(buf: ByteBuffer) = lift(buf.getLong()) + + override fun lower(value: CallbackInterface) = handleMap.insert(value) + + override fun allocationSize(value: CallbackInterface) = 8UL + + override fun write(value: CallbackInterface, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} +/** + * The cleaner interface for Object finalization code to run. + * This is the entry point to any implementation that we're using. + * + * The cleaner registers objects and returns cleanables, so now we are + * defining a `UniffiCleaner` with a `UniffiClenaer.Cleanable` to abstract the + * different implmentations available at compile time. + * + * @suppress + */ +interface UniffiCleaner { + interface Cleanable { + fun clean() + } + + fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable + + companion object +} + +// The fallback Jna cleaner, which is available for both Android, and the JVM. +private class UniffiJnaCleaner : UniffiCleaner { + private val cleaner = com.sun.jna.internal.Cleaner.getCleaner() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + UniffiJnaCleanable(cleaner.register(value, cleanUpTask)) +} + +private class UniffiJnaCleanable( + private val cleanable: com.sun.jna.internal.Cleaner.Cleanable, +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + + +// We decide at uniffi binding generation time whether we were +// using Android or not. +// There are further runtime checks to chose the correct implementation +// of the cleaner. +private fun UniffiCleaner.Companion.create(): UniffiCleaner = + try { + // For safety's sake: if the library hasn't been run in android_cleaner = true + // mode, but is being run on Android, then we still need to think about + // Android API versions. + // So we check if java.lang.ref.Cleaner is there, and use that… + java.lang.Class.forName("java.lang.ref.Cleaner") + JavaLangRefCleaner() + } catch (e: ClassNotFoundException) { + // … otherwise, fallback to the JNA cleaner. + UniffiJnaCleaner() + } + +private class JavaLangRefCleaner : UniffiCleaner { + val cleaner = java.lang.ref.Cleaner.create() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + JavaLangRefCleanable(cleaner.register(value, cleanUpTask)) +} + +private class JavaLangRefCleanable( + val cleanable: java.lang.ref.Cleaner.Cleanable +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + +/** + * @suppress + */ +public object FfiConverterULong: FfiConverter { + override fun lift(value: Long): ULong { + return value.toULong() + } + + override fun read(buf: ByteBuffer): ULong { + return lift(buf.getLong()) + } + + override fun lower(value: ULong): Long { + return value.toLong() + } + + override fun allocationSize(value: ULong) = 8UL + + override fun write(value: ULong, buf: ByteBuffer) { + buf.putLong(value.toLong()) + } +} + +/** + * @suppress + */ +public object FfiConverterLong: FfiConverter { + override fun lift(value: Long): Long { + return value + } + + override fun read(buf: ByteBuffer): Long { + return buf.getLong() + } + + override fun lower(value: Long): Long { + return value + } + + override fun allocationSize(value: Long) = 8UL + + override fun write(value: Long, buf: ByteBuffer) { + buf.putLong(value) + } +} + +/** + * @suppress + */ +public object FfiConverterBoolean: FfiConverter { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1UL + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} + +/** + * @suppress + */ +public object FfiConverterString: FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len.toInt()) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit().toULong()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): ULong { + val sizeForLength = 4UL + val sizeForString = value.length.toULong() * 3UL + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + + +// This template implements a class for working with a Rust struct via a handle +// to the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque handle to the underlying Rust struct. +// Method calls need to read this handle from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its handle should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the handle, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the handle, but is interrupted +// before it can pass the handle over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read handle value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +public interface FfiAppInterface { + + fun `dispatch`(`action`: AppAction) + + fun `listenForUpdates`(`reconciler`: AppReconciler) + + fun `state`(): AppState + + companion object +} + +open class FfiApp: Disposable, AutoCloseable, FfiAppInterface +{ + + @Suppress("UNUSED_PARAMETER") + /** + * @suppress + */ + constructor(withHandle: UniffiWithHandle, handle: Long) { + this.handle = handle + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(handle)) + } + + /** + * @suppress + * + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noHandle: NoHandle) { + this.handle = 0 + this.cleanable = null + } + constructor(`apiUrl`: kotlin.String, `clientId`: kotlin.String, `dataDir`: kotlin.String) : + this(UniffiWithHandle, + uniffiRustCall() { _status -> + UniffiLib.uniffi_maple_core_fn_constructor_ffiapp_new( + + FfiConverterString.lower(`apiUrl`),FfiConverterString.lower(`clientId`),FfiConverterString.lower(`dataDir`),_status) +} + ) + + protected val handle: Long + protected val cleanable: UniffiCleaner.Cleanable? + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable?.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithHandle(block: (handle: Long) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the handle being freed concurrently. + try { + return block(this.uniffiCloneHandle()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable?.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val handle: Long) : Runnable { + override fun run() { + if (handle == 0.toLong()) { + // Fake object created with `NoHandle`, don't try to free. + return; + } + uniffiRustCall { status -> + UniffiLib.uniffi_maple_core_fn_free_ffiapp(handle, status) + } + } + } + + /** + * @suppress + */ + fun uniffiCloneHandle(): Long { + if (handle == 0.toLong()) { + throw InternalException("uniffiCloneHandle() called on NoHandle object"); + } + return uniffiRustCall() { status -> + UniffiLib.uniffi_maple_core_fn_clone_ffiapp(handle, status) + } + } + + override fun `dispatch`(`action`: AppAction) + = + callWithHandle { + uniffiRustCall() { _status -> + UniffiLib.uniffi_maple_core_fn_method_ffiapp_dispatch( + it, + FfiConverterTypeAppAction.lower(`action`),_status) +} + } + + + + override fun `listenForUpdates`(`reconciler`: AppReconciler) + = + callWithHandle { + uniffiRustCall() { _status -> + UniffiLib.uniffi_maple_core_fn_method_ffiapp_listen_for_updates( + it, + FfiConverterTypeAppReconciler.lower(`reconciler`),_status) +} + } + + + + override fun `state`(): AppState { + return FfiConverterTypeAppState.lift( + callWithHandle { + uniffiRustCall() { _status -> + UniffiLib.uniffi_maple_core_fn_method_ffiapp_state( + it, + _status) +} + } + ) + } + + + + + + + + + + /** + * @suppress + */ + companion object + +} + + +/** + * @suppress + */ +public object FfiConverterTypeFfiApp: FfiConverter { + override fun lower(value: FfiApp): Long { + return value.uniffiCloneHandle() + } + + override fun lift(value: Long): FfiApp { + return FfiApp(UniffiWithHandle, value) + } + + override fun read(buf: ByteBuffer): FfiApp { + return lift(buf.getLong()) + } + + override fun allocationSize(value: FfiApp) = 8UL + + override fun write(value: FfiApp, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} + + + +data class AppState ( + var `rev`: kotlin.ULong + , + var `auth`: AuthState + , + var `pendingAuthUrl`: kotlin.String? + , + var `router`: Router + , + var `messages`: List + , + var `isAgentTyping`: kotlin.Boolean + , + var `isLoadingHistory`: kotlin.Boolean + , + var `hasOlderMessages`: kotlin.Boolean + , + var `composeText`: kotlin.String + , + var `toast`: kotlin.String? + , + var `showSettings`: kotlin.Boolean + , + var `confirmDeleteAgent`: kotlin.Boolean + , + var `isDeletingAgent`: kotlin.Boolean + +){ + + + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeAppState: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): AppState { + return AppState( + FfiConverterULong.read(buf), + FfiConverterTypeAuthState.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterTypeRouter.read(buf), + FfiConverterSequenceTypeChatMessage.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: AppState) = ( + FfiConverterULong.allocationSize(value.`rev`) + + FfiConverterTypeAuthState.allocationSize(value.`auth`) + + FfiConverterOptionalString.allocationSize(value.`pendingAuthUrl`) + + FfiConverterTypeRouter.allocationSize(value.`router`) + + FfiConverterSequenceTypeChatMessage.allocationSize(value.`messages`) + + FfiConverterBoolean.allocationSize(value.`isAgentTyping`) + + FfiConverterBoolean.allocationSize(value.`isLoadingHistory`) + + FfiConverterBoolean.allocationSize(value.`hasOlderMessages`) + + FfiConverterString.allocationSize(value.`composeText`) + + FfiConverterOptionalString.allocationSize(value.`toast`) + + FfiConverterBoolean.allocationSize(value.`showSettings`) + + FfiConverterBoolean.allocationSize(value.`confirmDeleteAgent`) + + FfiConverterBoolean.allocationSize(value.`isDeletingAgent`) + ) + + override fun write(value: AppState, buf: ByteBuffer) { + FfiConverterULong.write(value.`rev`, buf) + FfiConverterTypeAuthState.write(value.`auth`, buf) + FfiConverterOptionalString.write(value.`pendingAuthUrl`, buf) + FfiConverterTypeRouter.write(value.`router`, buf) + FfiConverterSequenceTypeChatMessage.write(value.`messages`, buf) + FfiConverterBoolean.write(value.`isAgentTyping`, buf) + FfiConverterBoolean.write(value.`isLoadingHistory`, buf) + FfiConverterBoolean.write(value.`hasOlderMessages`, buf) + FfiConverterString.write(value.`composeText`, buf) + FfiConverterOptionalString.write(value.`toast`, buf) + FfiConverterBoolean.write(value.`showSettings`, buf) + FfiConverterBoolean.write(value.`confirmDeleteAgent`, buf) + FfiConverterBoolean.write(value.`isDeletingAgent`, buf) + } +} + + + +data class ChatMessage ( + var `id`: kotlin.String + , + var `content`: kotlin.String + , + var `isUser`: kotlin.Boolean + , + var `isStreaming`: kotlin.Boolean + , + var `timestamp`: kotlin.Long + , + var `timestampDisplay`: kotlin.String + , + var `showSender`: kotlin.Boolean + , + var `showTimestamp`: kotlin.Boolean + +){ + + + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeChatMessage: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ChatMessage { + return ChatMessage( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterLong.read(buf), + FfiConverterString.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: ChatMessage) = ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterString.allocationSize(value.`content`) + + FfiConverterBoolean.allocationSize(value.`isUser`) + + FfiConverterBoolean.allocationSize(value.`isStreaming`) + + FfiConverterLong.allocationSize(value.`timestamp`) + + FfiConverterString.allocationSize(value.`timestampDisplay`) + + FfiConverterBoolean.allocationSize(value.`showSender`) + + FfiConverterBoolean.allocationSize(value.`showTimestamp`) + ) + + override fun write(value: ChatMessage, buf: ByteBuffer) { + FfiConverterString.write(value.`id`, buf) + FfiConverterString.write(value.`content`, buf) + FfiConverterBoolean.write(value.`isUser`, buf) + FfiConverterBoolean.write(value.`isStreaming`, buf) + FfiConverterLong.write(value.`timestamp`, buf) + FfiConverterString.write(value.`timestampDisplay`, buf) + FfiConverterBoolean.write(value.`showSender`, buf) + FfiConverterBoolean.write(value.`showTimestamp`, buf) + } +} + + + +data class Router ( + var `defaultScreen`: Screen + , + var `screenStack`: List + +){ + + + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeRouter: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): Router { + return Router( + FfiConverterTypeScreen.read(buf), + FfiConverterSequenceTypeScreen.read(buf), + ) + } + + override fun allocationSize(value: Router) = ( + FfiConverterTypeScreen.allocationSize(value.`defaultScreen`) + + FfiConverterSequenceTypeScreen.allocationSize(value.`screenStack`) + ) + + override fun write(value: Router, buf: ByteBuffer) { + FfiConverterTypeScreen.write(value.`defaultScreen`, buf) + FfiConverterSequenceTypeScreen.write(value.`screenStack`, buf) + } +} + + + +sealed class AppAction { + + data class LoginWithEmail( + val `email`: kotlin.String, + val `password`: kotlin.String) : AppAction() + + { + + + companion object + } + + data class SignUpWithEmail( + val `email`: kotlin.String, + val `password`: kotlin.String, + val `name`: kotlin.String) : AppAction() + + { + + + companion object + } + + data class InitiateOAuth( + val `provider`: cloud.opensecret.maple.rust.OAuthProvider, + val `inviteCode`: kotlin.String?) : AppAction() + + { + + + companion object + } + + data class HandleOAuthCallback( + val `provider`: cloud.opensecret.maple.rust.OAuthProvider, + val `code`: kotlin.String, + val `state`: kotlin.String, + val `inviteCode`: kotlin.String) : AppAction() + + { + + + companion object + } + + data class RestoreSession( + val `accessToken`: kotlin.String, + val `refreshToken`: kotlin.String) : AppAction() + + { + + + companion object + } + + object CompleteStartup : AppAction() + + + object ClearPendingAuthUrl : AppAction() + + + object Logout : AppAction() + + + data class SendMessage( + val `content`: kotlin.String) : AppAction() + + { + + + companion object + } + + object LoadOlderMessages : AppAction() + + + object RefreshTimestamps : AppAction() + + + object ClearToast : AppAction() + + + object ToggleSettings : AppAction() + + + object RequestDeleteAgent : AppAction() + + + object ConfirmDeleteAgent : AppAction() + + + object CancelDeleteAgent : AppAction() + + + + + + + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeAppAction : FfiConverterRustBuffer{ + override fun read(buf: ByteBuffer): AppAction { + return when(buf.getInt()) { + 1 -> AppAction.LoginWithEmail( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + 2 -> AppAction.SignUpWithEmail( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + 3 -> AppAction.InitiateOAuth( + FfiConverterTypeOAuthProvider.read(buf), + FfiConverterOptionalString.read(buf), + ) + 4 -> AppAction.HandleOAuthCallback( + FfiConverterTypeOAuthProvider.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + 5 -> AppAction.RestoreSession( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + 6 -> AppAction.CompleteStartup + 7 -> AppAction.ClearPendingAuthUrl + 8 -> AppAction.Logout + 9 -> AppAction.SendMessage( + FfiConverterString.read(buf), + ) + 10 -> AppAction.LoadOlderMessages + 11 -> AppAction.RefreshTimestamps + 12 -> AppAction.ClearToast + 13 -> AppAction.ToggleSettings + 14 -> AppAction.RequestDeleteAgent + 15 -> AppAction.ConfirmDeleteAgent + 16 -> AppAction.CancelDeleteAgent + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: AppAction) = when(value) { + is AppAction.LoginWithEmail -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`email`) + + FfiConverterString.allocationSize(value.`password`) + ) + } + is AppAction.SignUpWithEmail -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`email`) + + FfiConverterString.allocationSize(value.`password`) + + FfiConverterString.allocationSize(value.`name`) + ) + } + is AppAction.InitiateOAuth -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeOAuthProvider.allocationSize(value.`provider`) + + FfiConverterOptionalString.allocationSize(value.`inviteCode`) + ) + } + is AppAction.HandleOAuthCallback -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeOAuthProvider.allocationSize(value.`provider`) + + FfiConverterString.allocationSize(value.`code`) + + FfiConverterString.allocationSize(value.`state`) + + FfiConverterString.allocationSize(value.`inviteCode`) + ) + } + is AppAction.RestoreSession -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`accessToken`) + + FfiConverterString.allocationSize(value.`refreshToken`) + ) + } + is AppAction.CompleteStartup -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.ClearPendingAuthUrl -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.Logout -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.SendMessage -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`content`) + ) + } + is AppAction.LoadOlderMessages -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.RefreshTimestamps -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.ClearToast -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.ToggleSettings -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.RequestDeleteAgent -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.ConfirmDeleteAgent -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AppAction.CancelDeleteAgent -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + } + + override fun write(value: AppAction, buf: ByteBuffer) { + when(value) { + is AppAction.LoginWithEmail -> { + buf.putInt(1) + FfiConverterString.write(value.`email`, buf) + FfiConverterString.write(value.`password`, buf) + Unit + } + is AppAction.SignUpWithEmail -> { + buf.putInt(2) + FfiConverterString.write(value.`email`, buf) + FfiConverterString.write(value.`password`, buf) + FfiConverterString.write(value.`name`, buf) + Unit + } + is AppAction.InitiateOAuth -> { + buf.putInt(3) + FfiConverterTypeOAuthProvider.write(value.`provider`, buf) + FfiConverterOptionalString.write(value.`inviteCode`, buf) + Unit + } + is AppAction.HandleOAuthCallback -> { + buf.putInt(4) + FfiConverterTypeOAuthProvider.write(value.`provider`, buf) + FfiConverterString.write(value.`code`, buf) + FfiConverterString.write(value.`state`, buf) + FfiConverterString.write(value.`inviteCode`, buf) + Unit + } + is AppAction.RestoreSession -> { + buf.putInt(5) + FfiConverterString.write(value.`accessToken`, buf) + FfiConverterString.write(value.`refreshToken`, buf) + Unit + } + is AppAction.CompleteStartup -> { + buf.putInt(6) + Unit + } + is AppAction.ClearPendingAuthUrl -> { + buf.putInt(7) + Unit + } + is AppAction.Logout -> { + buf.putInt(8) + Unit + } + is AppAction.SendMessage -> { + buf.putInt(9) + FfiConverterString.write(value.`content`, buf) + Unit + } + is AppAction.LoadOlderMessages -> { + buf.putInt(10) + Unit + } + is AppAction.RefreshTimestamps -> { + buf.putInt(11) + Unit + } + is AppAction.ClearToast -> { + buf.putInt(12) + Unit + } + is AppAction.ToggleSettings -> { + buf.putInt(13) + Unit + } + is AppAction.RequestDeleteAgent -> { + buf.putInt(14) + Unit + } + is AppAction.ConfirmDeleteAgent -> { + buf.putInt(15) + Unit + } + is AppAction.CancelDeleteAgent -> { + buf.putInt(16) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + + + + + +sealed class AppUpdate { + + data class FullState( + val v1: cloud.opensecret.maple.rust.AppState) : AppUpdate() + + { + + + companion object + } + + data class SessionTokens( + val `rev`: kotlin.ULong, + val `accessToken`: kotlin.String, + val `refreshToken`: kotlin.String) : AppUpdate() + + { + + + companion object + } + + + + + + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeAppUpdate : FfiConverterRustBuffer{ + override fun read(buf: ByteBuffer): AppUpdate { + return when(buf.getInt()) { + 1 -> AppUpdate.FullState( + FfiConverterTypeAppState.read(buf), + ) + 2 -> AppUpdate.SessionTokens( + FfiConverterULong.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: AppUpdate) = when(value) { + is AppUpdate.FullState -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeAppState.allocationSize(value.v1) + ) + } + is AppUpdate.SessionTokens -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterULong.allocationSize(value.`rev`) + + FfiConverterString.allocationSize(value.`accessToken`) + + FfiConverterString.allocationSize(value.`refreshToken`) + ) + } + } + + override fun write(value: AppUpdate, buf: ByteBuffer) { + when(value) { + is AppUpdate.FullState -> { + buf.putInt(1) + FfiConverterTypeAppState.write(value.v1, buf) + Unit + } + is AppUpdate.SessionTokens -> { + buf.putInt(2) + FfiConverterULong.write(value.`rev`, buf) + FfiConverterString.write(value.`accessToken`, buf) + FfiConverterString.write(value.`refreshToken`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + + + + + +sealed class AuthState { + + object Initializing : AuthState() + + + object Ready : AuthState() + + + object LoggingIn : AuthState() + + + object SigningUp : AuthState() + + + data class LoggedIn( + val `userId`: kotlin.String, + val `email`: kotlin.String?, + val `name`: kotlin.String?) : AuthState() + + { + + + companion object + } + + + + + + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeAuthState : FfiConverterRustBuffer{ + override fun read(buf: ByteBuffer): AuthState { + return when(buf.getInt()) { + 1 -> AuthState.Initializing + 2 -> AuthState.Ready + 3 -> AuthState.LoggingIn + 4 -> AuthState.SigningUp + 5 -> AuthState.LoggedIn( + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: AuthState) = when(value) { + is AuthState.Initializing -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AuthState.Ready -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AuthState.LoggingIn -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AuthState.SigningUp -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is AuthState.LoggedIn -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`userId`) + + FfiConverterOptionalString.allocationSize(value.`email`) + + FfiConverterOptionalString.allocationSize(value.`name`) + ) + } + } + + override fun write(value: AuthState, buf: ByteBuffer) { + when(value) { + is AuthState.Initializing -> { + buf.putInt(1) + Unit + } + is AuthState.Ready -> { + buf.putInt(2) + Unit + } + is AuthState.LoggingIn -> { + buf.putInt(3) + Unit + } + is AuthState.SigningUp -> { + buf.putInt(4) + Unit + } + is AuthState.LoggedIn -> { + buf.putInt(5) + FfiConverterString.write(value.`userId`, buf) + FfiConverterOptionalString.write(value.`email`, buf) + FfiConverterOptionalString.write(value.`name`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + + + + + + +enum class OAuthProvider { + + GITHUB, + GOOGLE, + APPLE; + + + + + companion object +} + + +/** + * @suppress + */ +public object FfiConverterTypeOAuthProvider: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer) = try { + OAuthProvider.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: OAuthProvider) = 4UL + + override fun write(value: OAuthProvider, buf: ByteBuffer) { + buf.putInt(value.ordinal + 1) + } +} + + + + + + +enum class Screen { + + LOADING, + LOGIN, + CHAT; + + + + + companion object +} + + +/** + * @suppress + */ +public object FfiConverterTypeScreen: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer) = try { + Screen.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: Screen) = 4UL + + override fun write(value: Screen, buf: ByteBuffer) { + buf.putInt(value.ordinal + 1) + } +} + + + + + + + +public interface AppReconciler { + + fun `reconcile`(`update`: AppUpdate) + + companion object +} + + + +// Put the implementation in an object so we don't pollute the top-level namespace +internal object uniffiCallbackInterfaceAppReconciler { + internal object `reconcile`: UniffiCallbackInterfaceAppReconcilerMethod0 { + override fun callback(`uniffiHandle`: Long,`update`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeAppReconciler.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`reconcile`( + FfiConverterTypeAppUpdate.lift(`update`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) + } + } + + internal object uniffiFree: UniffiCallbackInterfaceFree { + override fun callback(handle: Long) { + FfiConverterTypeAppReconciler.handleMap.remove(handle) + } + } + + internal object uniffiClone: UniffiCallbackInterfaceClone { + override fun callback(handle: Long): Long { + return FfiConverterTypeAppReconciler.handleMap.clone(handle) + } + } + + internal var vtable = UniffiVTableCallbackInterfaceAppReconciler.UniffiByValue( + uniffiFree, + uniffiClone, + `reconcile`, + ) + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal fun register(lib: UniffiLib) { + lib.uniffi_maple_core_fn_init_callback_vtable_appreconciler(vtable) + } +} + +/** + * The ffiConverter which transforms the Callbacks in to handles to pass to Rust. + * + * @suppress + */ +public object FfiConverterTypeAppReconciler: FfiConverterCallbackInterface() + + + + +/** + * @suppress + */ +public object FfiConverterOptionalString: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.String? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterString.read(buf) + } + + override fun allocationSize(value: kotlin.String?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterString.allocationSize(value) + } + } + + override fun write(value: kotlin.String?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterString.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeChatMessage: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeChatMessage.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeChatMessage.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeChatMessage.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeScreen: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeScreen.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeScreen.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeScreen.write(it, buf) + } + } +} fun `defaultApiUrl`(): kotlin.String { + return FfiConverterString.lift( + uniffiRustCall() { _status -> + UniffiLib.uniffi_maple_core_fn_func_default_api_url( + + _status) +} + ) + } + + + diff --git a/native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt b/native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt new file mode 100644 index 00000000..3f5de919 --- /dev/null +++ b/native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt @@ -0,0 +1,1375 @@ +package cloud.opensecret.maple.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cloud.opensecret.maple.AppManager +import cloud.opensecret.maple.rust.AppAction +import cloud.opensecret.maple.rust.AuthState +import cloud.opensecret.maple.rust.ChatMessage +import cloud.opensecret.maple.rust.OAuthProvider +import cloud.opensecret.maple.rust.Screen +import cloud.opensecret.maple.ui.theme.* +import kotlin.math.roundToInt + +private const val HISTORY_PREFETCH_THRESHOLD = 3 +private const val BOTTOM_FOLLOW_THRESHOLD_PX = 96 +private const val TYPING_INDICATOR_KEY = "typing-indicator" + +private sealed interface HistoryLoadScrollStrategy { + data object PreserveBottom : HistoryLoadScrollStrategy + data class PreserveAnchor( + val messageId: String, + val scrollOffset: Int, + ) : HistoryLoadScrollStrategy +} + +private fun didPrependMessages( + previousIds: List, + currentIds: List, +): Boolean { + return previousIds.isNotEmpty() && + currentIds.size > previousIds.size && + currentIds.takeLast(previousIds.size) == previousIds +} + +private fun didAppendMessages( + previousIds: List, + currentIds: List, +): Boolean { + return previousIds.isNotEmpty() && + currentIds.size > previousIds.size && + currentIds.take(previousIds.size) == previousIds +} + +private fun LazyListState.isNearBottom(): Boolean { + val info = layoutInfo + if (info.totalItemsCount == 0) return true + + val lastVisible = info.visibleItemsInfo.lastOrNull() ?: return true + val viewportEnd = info.viewportEndOffset - info.afterContentPadding + val distanceFromBottom = (lastVisible.offset + lastVisible.size) - viewportEnd + + return lastVisible.index >= info.totalItemsCount - 2 && + distanceFromBottom <= BOTTOM_FOLLOW_THRESHOLD_PX +} + +private fun LazyListState.historyAnchor( + messageIds: List, +): HistoryLoadScrollStrategy.PreserveAnchor? { + val firstVisibleMessage = layoutInfo.visibleItemsInfo.firstOrNull { visibleItem -> + val key = visibleItem.key as? String + key != null && key != TYPING_INDICATOR_KEY + } ?: return null + + val messageId = firstVisibleMessage.key as? String ?: return null + if (messageId !in messageIds) return null + + return HistoryLoadScrollStrategy.PreserveAnchor( + messageId = messageId, + scrollOffset = firstVisibleItemScrollOffset, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainApp(manager: AppManager) { + val state = manager.state + var showSplash by remember { mutableStateOf(true) } + var minTimePassed by remember { mutableStateOf(false) } + val currentScreen = state.router.defaultScreen + val toastMessage = state.toast + + LaunchedEffect(Unit) { + kotlinx.coroutines.delay(1000) + minTimePassed = true + } + + LaunchedEffect(minTimePassed, currentScreen) { + if (minTimePassed && currentScreen != Screen.LOADING) { + showSplash = false + } + } + + LaunchedEffect(toastMessage) { + if (toastMessage == null) return@LaunchedEffect + kotlinx.coroutines.delay(4000) + if (manager.state.toast == toastMessage) { + manager.dispatch(AppAction.ClearToast) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + when (state.router.defaultScreen) { + Screen.LOADING -> SplashScreen() + Screen.LOGIN -> LoginScreen(manager) + Screen.CHAT -> AgentChatScreen(manager) + } + + androidx.compose.animation.AnimatedVisibility( + visible = showSplash, + exit = androidx.compose.animation.fadeOut( + animationSpec = androidx.compose.animation.core.tween(400), + ), + ) { + SplashScreen() + } + + MapleToastHost( + message = toastMessage, + isChatScreen = currentScreen == Screen.CHAT, + onDismiss = { manager.dispatch(AppAction.ClearToast) }, + ) + } +} + +@Composable +private fun MapleToastHost( + message: String?, + isChatScreen: Boolean, + onDismiss: () -> Unit, +) { + val isDarkTheme = isSystemInDarkTheme() + + AnimatedVisibility( + visible = message != null, + modifier = Modifier.fillMaxSize(), + enter = fadeIn(animationSpec = tween(durationMillis = 180)) + + slideInVertically(animationSpec = tween(durationMillis = 220)) { it / 2 }, + exit = fadeOut(animationSpec = tween(durationMillis = 160)) + + slideOutVertically(animationSpec = tween(durationMillis = 200)) { it / 2 }, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + Surface( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = if (isChatScreen) 112.dp else 24.dp) + .clickable(onClick = onDismiss), + shape = RoundedCornerShape(999.dp), + color = if (isDarkTheme) { + Color(0xFF271D1A).copy(alpha = 0.92f) + } else { + Color.White.copy(alpha = 0.94f) + }, + tonalElevation = 0.dp, + shadowElevation = if (isDarkTheme) 8.dp else 4.dp, + border = BorderStroke( + 1.dp, + MapleError.copy(alpha = if (isDarkTheme) 0.35f else 0.18f), + ), + ) { + Text( + text = message.orEmpty(), + color = if (isDarkTheme) Pebble50 else Neutral800, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) + } + } + } +} + +// -- Splash -- + +@Composable +fun SplashScreen() { + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + val w = constraints.maxWidth.toFloat() + val h = constraints.maxHeight.toFloat() + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.radialGradient( + colors = listOf( + androidx.compose.ui.graphics.Color(0xFFFF9771), + androidx.compose.ui.graphics.Color(0xFFCE9A8E), + androidx.compose.ui.graphics.Color(0xFF9C9DAB), + ), + center = androidx.compose.ui.geometry.Offset(w / 2f, h), + radius = h, + ), + ), + ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = cloud.opensecret.maple.R.drawable.maple_wordmark), + contentDescription = "Maple", + modifier = Modifier.height(40.dp), + tint = Pebble50, + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + "Privacy-first intelligence", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = cloud.opensecret.maple.ui.theme.ArrayFamily, + ), + color = Pebble100.copy(alpha = 0.8f), + ) + } + } +} + +// -- Login -- + +private data class LoginPalette( + val backgroundBase: Color, + val backgroundGlow: List, + val cardBackground: Color, + val cardHighlight: Color, + val cardBorder: Color, + val cardShadow: Color, + val wordmark: Color, + val supportingText: Color, + val tertiaryText: Color, + val divider: Color, + val fieldBackground: Color, + val fieldBorder: Color, + val fieldText: Color, + val fieldPlaceholder: Color, + val secondaryButtonBackground: Color, + val secondaryButtonBorder: Color, + val secondaryButtonForeground: Color, + val secondaryButtonShadow: Color, +) + +private fun loginPalette(isDarkTheme: Boolean): LoginPalette = + if (isDarkTheme) { + LoginPalette( + backgroundBase = Color(0xFF1A110E), + backgroundGlow = listOf( + Maple500.copy(alpha = 0.07f), + Color(0xFF5D4036).copy(alpha = 0.11f), + Pebble800.copy(alpha = 0.12f), + Color.Transparent, + ), + cardBackground = Color(0xFF271D1A).copy(alpha = 0.9f), + cardHighlight = Color.White.copy(alpha = 0.04f), + cardBorder = Color(0xFF53433E), + cardShadow = Color.Black.copy(alpha = 0.4f), + wordmark = Pebble50, + supportingText = Color(0xFFD8C2BB), + tertiaryText = Color(0xFFD8C2BB).copy(alpha = 0.8f), + divider = Color(0xFF53433E), + fieldBackground = Color(0xFF231A16).copy(alpha = 0.96f), + fieldBorder = Color(0xFFA08D86).copy(alpha = 0.45f), + fieldText = Color(0xFFF1DFD9), + fieldPlaceholder = Color(0xFFD8C2BB).copy(alpha = 0.75f), + secondaryButtonBackground = Color(0xFF322824).copy(alpha = 0.96f), + secondaryButtonBorder = Color(0xFF53433E), + secondaryButtonForeground = Color(0xFFF1DFD9), + secondaryButtonShadow = Color.Black.copy(alpha = 0.14f), + ) + } else { + LoginPalette( + backgroundBase = Color(0xFFFBF8F6), + backgroundGlow = listOf( + Maple500.copy(alpha = 0.18f), + Bark300.copy(alpha = 0.1f), + Pebble300.copy(alpha = 0.1f), + Color.Transparent, + ), + cardBackground = Color.White.copy(alpha = 0.74f), + cardHighlight = Color.White.copy(alpha = 0.42f), + cardBorder = Color.White.copy(alpha = 0.72f), + cardShadow = Pebble900.copy(alpha = 0.08f), + wordmark = Pebble800, + supportingText = Pebble600, + tertiaryText = Pebble400, + divider = Neutral200, + fieldBackground = Color.White.copy(alpha = 0.84f), + fieldBorder = Neutral200.copy(alpha = 0.95f), + fieldText = Pebble800, + fieldPlaceholder = Pebble400, + secondaryButtonBackground = Color.White.copy(alpha = 0.56f), + secondaryButtonBorder = Color.White.copy(alpha = 0.68f), + secondaryButtonForeground = Pebble700, + secondaryButtonShadow = Pebble900.copy(alpha = 0.05f), + ) + } + +private data class ChatPalette( + val backgroundBase: Color, + val backgroundGlow: List, + val chromeHighlight: Color, + val chromeBackground: Color, + val chromeBorder: Color, + val chromeShadow: Color, + val headerWordmark: Color, + val secondaryIcon: Color, + val composeBackground: Color, + val composeBorder: Color, + val composeShadow: Color, + val composeText: Color, + val composePlaceholder: Color, + val metadataText: Color, + val assistantText: Color, + val userBubbleColor: Color, + val userText: Color, + val surfaceText: Color, + val sheetBackground: Color, + val sheetDivider: Color, + val sheetSecondaryText: Color, + val disabledIcon: Color, +) + +private fun chatPalette(isDarkTheme: Boolean): ChatPalette = + if (isDarkTheme) { + ChatPalette( + backgroundBase = Color(0xFF1A110E), + backgroundGlow = listOf( + Maple500.copy(alpha = 0.07f), + Color(0xFF5D4036).copy(alpha = 0.1f), + Pebble800.copy(alpha = 0.12f), + Color.Transparent, + ), + chromeHighlight = Color(0xFF271D1A).copy(alpha = 0.78f), + chromeBackground = Color(0xFF271D1A).copy(alpha = 0.78f), + chromeBorder = Color(0xFF53433E), + chromeShadow = Color.Black.copy(alpha = 0.14f), + headerWordmark = Pebble50, + secondaryIcon = Color(0xFFD8C2BB), + composeBackground = Color(0xFF271D1A).copy(alpha = 0.78f), + composeBorder = Color(0xFF53433E), + composeShadow = Color.Black.copy(alpha = 0.14f), + composeText = Color(0xFFF1DFD9), + composePlaceholder = Color(0xFFD8C2BB).copy(alpha = 0.78f), + metadataText = Color(0xFFD8C2BB).copy(alpha = 0.82f), + assistantText = Color(0xFFF1DFD9), + userBubbleColor = Color(0xFF322824).copy(alpha = 0.96f), + userText = Color(0xFFF1DFD9), + surfaceText = Color(0xFFF1DFD9), + sheetBackground = Color(0xFF271D1A), + sheetDivider = Color(0xFF53433E), + sheetSecondaryText = Color(0xFFD8C2BB), + disabledIcon = Color(0xFFA08D86).copy(alpha = 0.45f), + ) + } else { + ChatPalette( + backgroundBase = Color.White, + backgroundGlow = listOf( + Color(0xFFFF9771).copy(alpha = 0.35f), + Color(0xFFECB8A5).copy(alpha = 0.2f), + Color(0xFFDADADA).copy(alpha = 0.1f), + Color.Transparent, + ), + chromeHighlight = Color.White.copy(alpha = 0.98f), + chromeBackground = Color(0xFFFFF1EC).copy(alpha = 0.84f), + chromeBorder = Color.White.copy(alpha = 0.92f), + chromeShadow = Pebble900.copy(alpha = 0.08f), + headerWordmark = Pebble800, + secondaryIcon = Pebble800, + composeBackground = Color.White.copy(alpha = 0.9f), + composeBorder = Color.White.copy(alpha = 0.94f), + composeShadow = Pebble900.copy(alpha = 0.12f), + composeText = Neutral800, + composePlaceholder = Color(0xFF878787), + metadataText = Pebble400, + assistantText = Pebble800, + userBubbleColor = Pebble100, + userText = Neutral800, + surfaceText = Neutral800, + sheetBackground = Color.White.copy(alpha = 0.96f), + sheetDivider = Neutral200, + sheetSecondaryText = Pebble500, + disabledIcon = Neutral300, + ) + } + +@Composable +private fun composeFieldColors(palette: ChatPalette) = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedTextColor = palette.composeText, + unfocusedTextColor = palette.composeText, + disabledTextColor = palette.composeText.copy(alpha = 0.5f), + cursorColor = Maple500, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + disabledBorderColor = Color.Transparent, + focusedPlaceholderColor = palette.composePlaceholder, + unfocusedPlaceholderColor = palette.composePlaceholder, + disabledPlaceholderColor = palette.composePlaceholder.copy(alpha = 0.5f), +) + +@Composable +fun LoginScreen(manager: AppManager) { + val state = manager.state + val isDarkTheme = isSystemInDarkTheme() + val palette = loginPalette(isDarkTheme) + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var name by remember { mutableStateOf("") } + var isSignUp by remember { mutableStateOf(false) } + + val isLoading = state.auth is AuthState.LoggingIn || state.auth is AuthState.SigningUp + + val nameFocus = remember { FocusRequester() } + val emailFocus = remember { FocusRequester() } + val passwordFocus = remember { FocusRequester() } + + fun submitForm() { + if (isSignUp) { + manager.dispatch(AppAction.SignUpWithEmail(email = email, password = password, name = name)) + } else { + manager.dispatch(AppAction.LoginWithEmail(email = email, password = password)) + } + } + + val canSubmit = email.isNotEmpty() && password.isNotEmpty() && !isLoading + + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + val w = constraints.maxWidth.toFloat() + val h = constraints.maxHeight.toFloat() + Box( + modifier = Modifier + .fillMaxSize() + .background(palette.backgroundBase), + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.radialGradient( + colors = palette.backgroundGlow, + center = androidx.compose.ui.geometry.Offset(w / 2f, h), + radius = h, + ), + ), + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column( + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 360.dp) + .shadow( + elevation = if (isDarkTheme) 28.dp else 20.dp, + shape = RoundedCornerShape(24.dp), + ambientColor = palette.cardShadow, + spotColor = palette.cardShadow, + ) + .clip(RoundedCornerShape(24.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf(palette.cardHighlight, palette.cardBackground), + ), + ) + .border(1.dp, palette.cardBorder, RoundedCornerShape(24.dp)) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = cloud.opensecret.maple.R.drawable.maple_wordmark), + contentDescription = "Maple", + modifier = Modifier.height(28.dp), + tint = palette.wordmark, + ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (isSignUp) { + MapleLoginTextField( + value = name, + onValueChange = { name = it }, + placeholder = "Name", + palette = palette, + modifier = Modifier.focusRequester(nameFocus), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { emailFocus.requestFocus() }), + ) + } + + MapleLoginTextField( + value = email, + onValueChange = { email = it }, + placeholder = "Email", + palette = palette, + modifier = Modifier.focusRequester(emailFocus), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { passwordFocus.requestFocus() }), + ) + + MapleLoginTextField( + value = password, + onValueChange = { password = it }, + placeholder = "Password", + palette = palette, + modifier = Modifier.focusRequester(passwordFocus), + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Go), + keyboardActions = KeyboardActions(onGo = { submitForm() }), + ) + } + + val primaryInteractionSource = remember { MutableInteractionSource() } + val primaryPressed by primaryInteractionSource.collectIsPressedAsState() + val primaryScale by animateFloatAsState( + targetValue = if (primaryPressed) 0.95f else 1f, + animationSpec = tween(durationMillis = 200), + ) + + Button( + onClick = { submitForm() }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .scale(primaryScale), + enabled = canSubmit, + interactionSource = primaryInteractionSource, + shape = RoundedCornerShape(999.dp), + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = Color.White, + disabledContainerColor = Color.Transparent, + disabledContentColor = Color.White, + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + disabledElevation = 0.dp, + ), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(999.dp)) + .background( + brush = Brush.verticalGradient( + colors = listOf(Maple400, Maple600), + ), + ) + .alpha(if (canSubmit || isLoading) 1f else 0.5f), + contentAlignment = Alignment.Center, + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = Color.White, + ) + } else { + Text( + if (isSignUp) "Sign Up" else "Sign In", + color = Color.White, + fontWeight = FontWeight.SemiBold, + ) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + HorizontalDivider(modifier = Modifier.weight(1f), color = palette.divider) + Text("or", style = MaterialTheme.typography.labelSmall, color = palette.tertiaryText) + HorizontalDivider(modifier = Modifier.weight(1f), color = palette.divider) + } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + GlassyOutlinedButton( + text = "Continue with GitHub", + onClick = { manager.dispatch(AppAction.InitiateOAuth(provider = OAuthProvider.GITHUB, inviteCode = null)) }, + palette = palette, + enabled = !isLoading, + ) + GlassyOutlinedButton( + text = "Continue with Google", + onClick = { manager.dispatch(AppAction.InitiateOAuth(provider = OAuthProvider.GOOGLE, inviteCode = null)) }, + palette = palette, + enabled = !isLoading, + ) + GlassyOutlinedButton( + text = "Continue with Apple", + onClick = { manager.dispatch(AppAction.InitiateOAuth(provider = OAuthProvider.APPLE, inviteCode = null)) }, + palette = palette, + enabled = !isLoading, + ) + } + + TextButton(onClick = { isSignUp = !isSignUp }) { + Text( + if (isSignUp) "Already have an account? Sign In" else "Don't have an account? Sign Up", + color = palette.supportingText, + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +@Composable +private fun MapleLoginTextField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + palette: LoginPalette, + modifier: Modifier = Modifier, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, +) { + val shape = RoundedCornerShape(12.dp) + + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .fillMaxWidth() + .clip(shape) + .background(palette.fieldBackground, shape) + .border(1.dp, palette.fieldBorder, shape) + .padding(horizontal = 16.dp, vertical = 14.dp), + textStyle = MaterialTheme.typography.bodyLarge.copy(color = palette.fieldText), + singleLine = true, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + cursorBrush = SolidColor(Maple500), + decorationBox = { innerTextField -> + Box(modifier = Modifier.fillMaxWidth()) { + if (value.isEmpty()) { + Text( + placeholder, + style = MaterialTheme.typography.bodyLarge, + color = palette.fieldPlaceholder, + ) + } + innerTextField() + } + }, + ) +} + +@Composable +private fun GlassyOutlinedButton( + text: String, + onClick: () -> Unit, + palette: LoginPalette, + enabled: Boolean = true, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1f, + animationSpec = tween(durationMillis = 200), + ) + + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .scale(scale) + .shadow( + elevation = 10.dp, + shape = RoundedCornerShape(999.dp), + ambientColor = palette.secondaryButtonShadow, + spotColor = palette.secondaryButtonShadow, + ), + enabled = enabled, + interactionSource = interactionSource, + shape = RoundedCornerShape(999.dp), + border = BorderStroke( + 1.dp, + if (enabled) palette.secondaryButtonBorder else palette.secondaryButtonBorder.copy(alpha = 0.5f), + ), + colors = ButtonDefaults.buttonColors( + containerColor = palette.secondaryButtonBackground, + contentColor = palette.secondaryButtonForeground, + disabledContainerColor = palette.secondaryButtonBackground.copy(alpha = 0.5f), + disabledContentColor = palette.secondaryButtonForeground.copy(alpha = 0.5f), + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + ), + ) { + Text(text, fontWeight = FontWeight.Medium) + } +} + +// -- Agent Chat -- + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AgentChatScreen(manager: AppManager) { + val state = manager.state + val isDarkTheme = isSystemInDarkTheme() + val palette = chatPalette(isDarkTheme) + val headerChromeShape = RoundedCornerShape(99.dp) + val composeChromeShape = RoundedCornerShape(24.dp) + val density = LocalDensity.current + var composeText by remember { mutableStateOf("") } + val messageIds = remember(state.messages) { state.messages.map(ChatMessage::id) } + val listState = rememberLazyListState() + var headerBottomPx by remember { mutableIntStateOf(0) } + var composeBarTopPx by remember { mutableIntStateOf(0) } + var hasSettledInitialScroll by remember { mutableStateOf(false) } + var pendingHistoryScrollStrategy by remember { + mutableStateOf(null) + } + var previousMessageIds by remember { mutableStateOf(messageIds) } + var previousLastMessageContent by remember { + mutableStateOf(state.messages.lastOrNull()?.content) + } + var previousIsAgentTyping by remember { mutableStateOf(state.isAgentTyping) } + var wasNearBottom by remember { mutableStateOf(true) } + + // Refresh relative timestamps every 30s + LaunchedEffect(Unit) { + while (true) { + kotlinx.coroutines.delay(30_000) + manager.dispatch(AppAction.RefreshTimestamps) + } + } + + suspend fun scrollToBottom(animated: Boolean) { + val bottomIndex = when { + state.isAgentTyping -> state.messages.size + state.messages.isNotEmpty() -> state.messages.lastIndex + else -> -1 + } + + if (bottomIndex < 0) return + + if (animated) { + listState.animateScrollToItem(bottomIndex) + } else { + listState.scrollToItem(bottomIndex) + } + } + + val canScrollBackward by remember { derivedStateOf { listState.canScrollBackward } } + val firstVisibleIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } + val isNearBottom by remember { derivedStateOf { listState.isNearBottom() } } + + fun requestOlderMessages() { + if ( + !hasSettledInitialScroll || + pendingHistoryScrollStrategy != null || + state.isLoadingHistory || + !state.hasOlderMessages + ) { + return + } + + pendingHistoryScrollStrategy = if (!canScrollBackward && isNearBottom) { + HistoryLoadScrollStrategy.PreserveBottom + } else { + listState.historyAnchor(messageIds) ?: HistoryLoadScrollStrategy.PreserveBottom + } + + manager.dispatch(AppAction.LoadOlderMessages) + } + + LaunchedEffect(isNearBottom) { + wasNearBottom = isNearBottom + } + + // Prefetch older messages when scrolled near top + LaunchedEffect( + firstVisibleIndex, + canScrollBackward, + state.hasOlderMessages, + state.isLoadingHistory, + hasSettledInitialScroll, + pendingHistoryScrollStrategy, + ) { + val shouldPrefetchOlder = + hasSettledInitialScroll && + state.hasOlderMessages && + !state.isLoadingHistory && + pendingHistoryScrollStrategy == null && + (firstVisibleIndex <= HISTORY_PREFETCH_THRESHOLD || !canScrollBackward) + + if (shouldPrefetchOlder) { + requestOlderMessages() + } + } + + LaunchedEffect(messageIds) { + when { + messageIds.isEmpty() -> { + hasSettledInitialScroll = false + pendingHistoryScrollStrategy = null + } + + !hasSettledInitialScroll -> { + scrollToBottom(animated = false) + hasSettledInitialScroll = true + } + + didPrependMessages(previousMessageIds, messageIds) -> { + when (val strategy = pendingHistoryScrollStrategy) { + HistoryLoadScrollStrategy.PreserveBottom -> scrollToBottom(animated = false) + is HistoryLoadScrollStrategy.PreserveAnchor -> { + val restoredIndex = messageIds.indexOf(strategy.messageId) + if (restoredIndex >= 0) { + listState.scrollToItem(restoredIndex, strategy.scrollOffset) + } + } + + null -> Unit + } + pendingHistoryScrollStrategy = null + } + + didAppendMessages(previousMessageIds, messageIds) && wasNearBottom -> { + scrollToBottom(animated = false) + } + + else -> { + pendingHistoryScrollStrategy = null + } + } + + previousMessageIds = messageIds + } + + LaunchedEffect(state.isLoadingHistory) { + if ( + !state.isLoadingHistory && + pendingHistoryScrollStrategy != null && + messageIds == previousMessageIds + ) { + pendingHistoryScrollStrategy = null + } + } + + LaunchedEffect(state.messages.lastOrNull()?.content, state.isAgentTyping) { + val contentChanged = state.messages.lastOrNull()?.content != previousLastMessageContent + val typingChanged = state.isAgentTyping != previousIsAgentTyping + + if ( + hasSettledInitialScroll && + pendingHistoryScrollStrategy == null && + wasNearBottom && + (contentChanged || typingChanged) + ) { + scrollToBottom(animated = false) + } + + previousLastMessageContent = state.messages.lastOrNull()?.content + previousIsAgentTyping = state.isAgentTyping + } + + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val w = constraints.maxWidth.toFloat() + val backgroundGlowRadius = with(density) { 500.dp.toPx() } + val topContentPadding = with(density) { + if (headerBottomPx == 0) 108.dp else headerBottomPx.toDp() + 16.dp + } + val bottomContentPadding = with(density) { + if (composeBarTopPx == 0) { + 116.dp + } else { + (constraints.maxHeight - composeBarTopPx).coerceAtLeast(0).toDp() + 16.dp + } + } + + // Background radial gradient from top center (matches Figma) + Box( + modifier = Modifier + .fillMaxSize() + .background(palette.backgroundBase), + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.radialGradient( + colors = palette.backgroundGlow, + center = androidx.compose.ui.geometry.Offset(w / 2f, 0f), + radius = backgroundGlowRadius, + ), + ), + ) + + // Messages list (full size, scrolls behind header and compose bar) + LazyColumn( + modifier = Modifier + .fillMaxSize() + .alpha(if (hasSettledInitialScroll || messageIds.isEmpty()) 1f else 0f), + state = listState, + contentPadding = PaddingValues( + start = 16.dp, end = 16.dp, + top = topContentPadding, + bottom = bottomContentPadding, + ), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + items(state.messages, key = { it.id }) { message -> + MessageBubble(message, palette) + } + + if (state.isAgentTyping) { + item(key = TYPING_INDICATOR_KEY) { + Text( + "Maple is typing...", + style = MaterialTheme.typography.bodySmall, + color = palette.metadataText, + modifier = Modifier.padding(start = 8.dp), + ) + } + } + } + + if (state.isLoadingHistory && state.messages.isNotEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = topContentPadding + 8.dp) + .align(Alignment.TopCenter), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp) + } + } + + if ((state.isLoadingHistory && state.messages.isEmpty()) || (!hasSettledInitialScroll && messageIds.isNotEmpty())) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + + // Floating header islands + Box( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(horizontal = 16.dp, vertical = 8.dp) + .onGloballyPositioned { coordinates -> + headerBottomPx = + (coordinates.positionInRoot().y + coordinates.size.height).roundToInt() + } + .align(Alignment.TopCenter), + ) { + // MPL wordmark pill with chevron (centered) + Box( + modifier = Modifier + .align(Alignment.Center) + .shadow( + elevation = if (isDarkTheme) 6.dp else 8.dp, + shape = headerChromeShape, + ambientColor = palette.chromeShadow, + spotColor = palette.chromeShadow, + ) + .background( + brush = Brush.linearGradient( + colors = listOf(palette.chromeHighlight, palette.chromeBackground), + ), + shape = headerChromeShape, + ) + .border( + width = if (isDarkTheme) 0.5.dp else 1.dp, + color = palette.chromeBorder, + shape = headerChromeShape, + ) + .padding(start = 16.dp, end = 12.dp, top = 12.dp, bottom = 12.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = cloud.opensecret.maple.R.drawable.maple_wordmark_abbr), + contentDescription = "Maple", + modifier = Modifier.height(16.dp), + tint = palette.headerWordmark, + ) + Text("⌄", fontSize = 12.sp, fontWeight = FontWeight.Black, color = Pebble400) + } + } + + // Hamburger menu pill (start aligned) + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .size(43.dp) + .shadow( + elevation = if (isDarkTheme) 6.dp else 8.dp, + shape = headerChromeShape, + ambientColor = palette.chromeShadow, + spotColor = palette.chromeShadow, + ) + .background( + brush = Brush.linearGradient( + colors = listOf(palette.chromeHighlight, palette.chromeBackground), + ), + shape = headerChromeShape, + ) + .border( + width = if (isDarkTheme) 0.5.dp else 1.dp, + color = palette.chromeBorder, + shape = headerChromeShape, + ), + contentAlignment = Alignment.Center, + ) { + IconButton( + onClick = { manager.dispatch(AppAction.ToggleSettings) }, + modifier = Modifier.size(43.dp), + ) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = "Menu", + tint = palette.secondaryIcon, + modifier = Modifier.size(18.dp), + ) + } + } + + // Search pill (end aligned) + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .size(43.dp) + .shadow( + elevation = if (isDarkTheme) 6.dp else 8.dp, + shape = headerChromeShape, + ambientColor = palette.chromeShadow, + spotColor = palette.chromeShadow, + ) + .background( + brush = Brush.linearGradient( + colors = listOf(palette.chromeHighlight, palette.chromeBackground), + ), + shape = headerChromeShape, + ) + .border( + width = if (isDarkTheme) 0.5.dp else 1.dp, + color = palette.chromeBorder, + shape = headerChromeShape, + ), + contentAlignment = Alignment.Center, + ) { + IconButton( + onClick = { }, + modifier = Modifier.size(43.dp), + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Search", + tint = palette.secondaryIcon, + modifier = Modifier.size(18.dp), + ) + } + } + } + + // Floating compose bar + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = 16.dp, vertical = 8.dp) + .onGloballyPositioned { coordinates -> + composeBarTopPx = coordinates.positionInRoot().y.roundToInt() + } + .shadow( + elevation = if (isDarkTheme) 0.dp else 12.dp, + shape = composeChromeShape, + ambientColor = palette.composeShadow, + spotColor = palette.composeShadow, + ) + .background( + color = palette.composeBackground, + shape = composeChromeShape, + ) + .border( + width = if (isDarkTheme) 0.5.dp else 1.dp, + color = palette.composeBorder, + shape = composeChromeShape, + ) + .padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + val composeFocus = remember { FocusRequester() } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + BasicTextField( + value = composeText, + onValueChange = { composeText = it }, + modifier = Modifier.fillMaxWidth().focusRequester(composeFocus), + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = palette.composeText, + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + ), + singleLine = false, + minLines = 1, + maxLines = 4, + cursorBrush = SolidColor(Maple500), + decorationBox = { innerTextField -> + Box(modifier = Modifier.fillMaxWidth()) { + if (composeText.isEmpty()) { + Text("Write...", color = palette.composePlaceholder, fontSize = 15.sp, fontWeight = FontWeight.Medium) + } + innerTextField() + } + }, + ) + + Box( + modifier = Modifier + .size(24.dp) + .background(Maple500.copy(alpha = 0.15f), CircleShape), + contentAlignment = Alignment.Center, + ) { + Text("+", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Maple500) + } + } + + val canSend = composeText.trim().isNotEmpty() && !state.isAgentTyping + + Box( + modifier = Modifier + .width(71.dp) + .clip(RoundedCornerShape(99.dp)) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Maple500.copy(alpha = if (canSend) 1f else 0.5f), + Maple700.copy(alpha = if (canSend) 1f else 0.5f), + ), + ), + ) + .clickable(enabled = canSend) { + val text = composeText.trim() + if (text.isNotEmpty()) { + manager.dispatch(AppAction.SendMessage(content = text)) + composeText = "" + } + } + .padding(horizontal = 24.dp, vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + Text( + "↑", + fontSize = 20.sp, + fontWeight = FontWeight.Black, + color = Color.White.copy(alpha = if (canSend) 1f else 0.1f), + ) + } + } + } + + if (state.showSettings) { + SettingsSheet(manager = manager, palette = palette) + } + + if (state.confirmDeleteAgent) { + AlertDialog( + onDismissRequest = { manager.dispatch(AppAction.CancelDeleteAgent) }, + containerColor = palette.sheetBackground, + titleContentColor = palette.surfaceText, + textContentColor = palette.metadataText, + title = { Text("Delete Agent?") }, + text = { Text("This will permanently delete your agent conversation history. This cannot be undone.") }, + confirmButton = { + TextButton(onClick = { manager.dispatch(AppAction.ConfirmDeleteAgent) }) { + Text("Delete", color = MapleError) + } + }, + dismissButton = { + TextButton(onClick = { manager.dispatch(AppAction.CancelDeleteAgent) }) { + Text("Cancel", color = palette.surfaceText) + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SettingsSheet(manager: AppManager, palette: ChatPalette) { + val sheetState = rememberModalBottomSheetState() + + ModalBottomSheet( + onDismissRequest = { manager.dispatch(AppAction.ToggleSettings) }, + sheetState = sheetState, + containerColor = palette.sheetBackground, + contentColor = palette.surfaceText, + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text( + "Settings", + style = MaterialTheme.typography.titleMedium, + color = palette.surfaceText, + modifier = Modifier.padding(bottom = 16.dp), + ) + TextButton( + onClick = { manager.dispatch(AppAction.RequestDeleteAgent) }, + enabled = !manager.state.isDeletingAgent, + shape = RoundedCornerShape(999.dp), + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = null, + tint = MapleError, + modifier = Modifier.padding(end = 8.dp), + ) + Text("Delete Agent", color = MapleError) + } + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp), color = palette.sheetDivider) + TextButton( + onClick = { + manager.dispatch(AppAction.ToggleSettings) + manager.dispatch(AppAction.Logout) + }, + shape = RoundedCornerShape(999.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ExitToApp, + contentDescription = null, + tint = palette.sheetSecondaryText, + modifier = Modifier.padding(end = 8.dp), + ) + Text("Sign Out", color = palette.surfaceText) + } + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Composable +private fun MessageBubble(message: ChatMessage, palette: ChatPalette) { + val isUser = message.isUser + val alignment = if (isUser) Alignment.End else Alignment.Start + val bubbleShape = RoundedCornerShape( + topStart = 24.dp, topEnd = 24.dp, + bottomStart = if (isUser) 24.dp else 4.dp, + bottomEnd = if (isUser) 4.dp else 24.dp, + ) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = alignment, + ) { + if (isUser) { + Box( + modifier = Modifier + .widthIn(max = 300.dp) + .background(palette.userBubbleColor, bubbleShape) + .padding(horizontal = 12.dp, vertical = 8.dp), + ) { + Text( + text = message.content, + color = palette.userText, + style = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 26.sp, + ), + ) + } + } else { + Text( + text = message.content, + color = palette.assistantText, + style = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 26.sp, + ), + modifier = Modifier.widthIn(max = 300.dp), + ) + } + if (message.showTimestamp) { + Text( + text = message.timestampDisplay, + style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp), + color = palette.metadataText, + modifier = Modifier.padding(start = 8.dp, end = 8.dp).padding(top = 2.dp), + ) + } + } +} diff --git a/native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Color.kt b/native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Color.kt new file mode 100644 index 00000000..51462f1f --- /dev/null +++ b/native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Color.kt @@ -0,0 +1,70 @@ +package cloud.opensecret.maple.ui.theme + +import androidx.compose.ui.graphics.Color + +// Maple (Primary) - "Primary brand energy" +val Maple50 = Color(0xFFFFF4F0) +val Maple100 = Color(0xFFFFE8E0) +val Maple200 = Color(0xFFFFD1C1) +val Maple300 = Color(0xFFFFBAA2) +val Maple400 = Color(0xFFFFA88A) +val Maple500 = Color(0xFFFF9771) +val Maple600 = Color(0xFFF67D57) +val Maple700 = Color(0xFFE8633D) +val Maple800 = Color(0xFFD04926) +val Maple900 = Color(0xFFA83515) + +// Pebble (Secondary) - "Ethereal balance" +val Pebble50 = Color(0xFFF7F7F9) +val Pebble100 = Color(0xFFE8E8ED) +val Pebble200 = Color(0xFFD1D2DC) +val Pebble300 = Color(0xFFBABCCB) +val Pebble400 = Color(0xFF9C9DAB) +val Pebble500 = Color(0xFF8A8B9A) +val Pebble600 = Color(0xFF757689) +val Pebble700 = Color(0xFF5E5F6E) +val Pebble800 = Color(0xFF474854) +val Pebble900 = Color(0xFF30313A) + +// Bark (Tertiary) - "Grounded structure" +val Bark50 = Color(0xFFF8F5F4) +val Bark100 = Color(0xFFEADED9) +val Bark200 = Color(0xFFD4BCAF) +val Bark300 = Color(0xFFC29A8D) +val Bark400 = Color(0xFFB0877C) +val Bark500 = Color(0xFF9E7469) +val Bark600 = Color(0xFF8A6055) +val Bark700 = Color(0xFF704D43) +val Bark800 = Color(0xFF583A32) +val Bark900 = Color(0xFF3D2821) + +// Grove (Tertiary) - "Organic calming" +val Grove50 = Color(0xFFF7F6F0) +val Grove100 = Color(0xFFE8E4D4) +val Grove200 = Color(0xFFD3CCB0) +val Grove300 = Color(0xFFBEB48C) +val Grove400 = Color(0xFFAEA375) +val Grove500 = Color(0xFF9E925E) +val Grove600 = Color(0xFF8A7F4C) +val Grove700 = Color(0xFF726B3C) +val Grove800 = Color(0xFF5A542D) +val Grove900 = Color(0xFF3F3B1F) + +// Neutral - "Focus and clarity" +val Neutral0 = Color(0xFFFAFAFA) +val Neutral50 = Color(0xFFF5F5F5) +val Neutral100 = Color(0xFFE5E5E5) +val Neutral200 = Color(0xFFD4D4D4) +val Neutral300 = Color(0xFFA3A3A3) +val Neutral400 = Color(0xFF737373) +val Neutral500 = Color(0xFF525252) +val Neutral600 = Color(0xFF404040) +val Neutral700 = Color(0xFF262626) +val Neutral800 = Color(0xFF171717) +val Neutral900 = Color(0xFF0A0A0A) + +// Semantic States +val MapleSuccess = Color(0xFF7B8F4A) +val MapleWarning = Color(0xFFD4A35A) +val MapleError = Color(0xFFD05E41) +val MapleInfo = Color(0xFF7E8DA1) diff --git a/native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Theme.kt b/native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Theme.kt new file mode 100644 index 00000000..715ddaad --- /dev/null +++ b/native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Theme.kt @@ -0,0 +1,154 @@ +package cloud.opensecret.maple.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cloud.opensecret.maple.R + +private val MapleLightColorScheme = lightColorScheme( + primary = Maple500, + onPrimary = Color.White, + primaryContainer = Maple100, + onPrimaryContainer = Maple900, + secondary = Pebble500, + onSecondary = Color.White, + secondaryContainer = Pebble100, + onSecondaryContainer = Pebble900, + tertiary = Bark500, + onTertiary = Color.White, + tertiaryContainer = Bark100, + onTertiaryContainer = Bark900, + background = Neutral50, + onBackground = Neutral900, + surface = Neutral0, + onSurface = Neutral900, + surfaceVariant = Neutral100, + onSurfaceVariant = Neutral600, + outline = Neutral300, + outlineVariant = Neutral200, + error = MapleError, + onError = Color.White, + errorContainer = Maple50, + onErrorContainer = Maple900, +) + +private val MapleDarkColorScheme = darkColorScheme( + primary = Color(0xFFFFB59B), + onPrimary = Color(0xFF55200A), + primaryContainer = Color(0xFF72351E), + onPrimaryContainer = Color(0xFFFFDBCF), + secondary = Color(0xFFE7BDB0), + onSecondary = Color(0xFF442A21), + secondaryContainer = Color(0xFF5D4036), + onSecondaryContainer = Color(0xFFFFDBCF), + tertiary = Color(0xFFD5C68E), + onTertiary = Color(0xFF393005), + tertiaryContainer = Color(0xFF50461A), + onTertiaryContainer = Color(0xFFF2E2A7), + background = Color(0xFF1A110E), + onBackground = Color(0xFFF1DFD9), + surface = Color(0xFF1A110E), + onSurface = Color(0xFFF1DFD9), + surfaceVariant = Color(0xFF53433E), + onSurfaceVariant = Color(0xFFD8C2BB), + outline = Color(0xFFA08D86), + outlineVariant = Color(0xFF53433E), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), +) + +private val MapleShapes = Shapes( + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(24.dp), +) + +val ManropeFamily = FontFamily( + Font(R.font.manrope_regular, FontWeight.Normal), + Font(R.font.manrope_medium, FontWeight.Medium), + Font(R.font.manrope_semibold, FontWeight.SemiBold), + Font(R.font.manrope_bold, FontWeight.Bold), +) + +val ArrayFamily = FontFamily( + Font(R.font.array_bold, FontWeight.Bold), +) + +private val MapleTypography = Typography( + headlineLarge = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + ), + headlineMedium = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + lineHeight = 28.sp, + ), + titleLarge = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + lineHeight = 28.sp, + ), + titleMedium = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + bodyLarge = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + bodyMedium = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + labelLarge = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + labelMedium = TextStyle( + fontFamily = ManropeFamily, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + ), +) + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + MaterialTheme( + colorScheme = if (darkTheme) MapleDarkColorScheme else MapleLightColorScheme, + shapes = MapleShapes, + typography = MapleTypography, + content = content, + ) +} diff --git a/native/android/app/src/main/res/drawable/maple_wordmark.xml b/native/android/app/src/main/res/drawable/maple_wordmark.xml new file mode 100644 index 00000000..5a1658be --- /dev/null +++ b/native/android/app/src/main/res/drawable/maple_wordmark.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/native/android/app/src/main/res/drawable/maple_wordmark_abbr.xml b/native/android/app/src/main/res/drawable/maple_wordmark_abbr.xml new file mode 100644 index 00000000..03042601 --- /dev/null +++ b/native/android/app/src/main/res/drawable/maple_wordmark_abbr.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/native/android/app/src/main/res/font/array_bold.ttf b/native/android/app/src/main/res/font/array_bold.ttf new file mode 100644 index 00000000..85af9a9d Binary files /dev/null and b/native/android/app/src/main/res/font/array_bold.ttf differ diff --git a/native/android/app/src/main/res/font/manrope_bold.ttf b/native/android/app/src/main/res/font/manrope_bold.ttf new file mode 100644 index 00000000..62a61839 Binary files /dev/null and b/native/android/app/src/main/res/font/manrope_bold.ttf differ diff --git a/native/android/app/src/main/res/font/manrope_medium.ttf b/native/android/app/src/main/res/font/manrope_medium.ttf new file mode 100644 index 00000000..c6d28def Binary files /dev/null and b/native/android/app/src/main/res/font/manrope_medium.ttf differ diff --git a/native/android/app/src/main/res/font/manrope_regular.ttf b/native/android/app/src/main/res/font/manrope_regular.ttf new file mode 100644 index 00000000..9a108f1c Binary files /dev/null and b/native/android/app/src/main/res/font/manrope_regular.ttf differ diff --git a/native/android/app/src/main/res/font/manrope_semibold.ttf b/native/android/app/src/main/res/font/manrope_semibold.ttf new file mode 100644 index 00000000..46a13d61 Binary files /dev/null and b/native/android/app/src/main/res/font/manrope_semibold.ttf differ diff --git a/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..f85d6125 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..f85d6125 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..4a3759f3 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..4a3759f3 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..8bfe2d86 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..8bfe2d86 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..223e00da Binary files /dev/null and b/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..223e00da Binary files /dev/null and b/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..5184cb11 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..5184cb11 Binary files /dev/null and b/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/native/android/app/src/main/res/values/strings.xml b/native/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..aee402c0 --- /dev/null +++ b/native/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Hello Chat + diff --git a/native/android/app/src/main/res/values/themes.xml b/native/android/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..b72bfb9a --- /dev/null +++ b/native/android/app/src/main/res/values/themes.xml @@ -0,0 +1,3 @@ + +