diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5f7d701..eba4981d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,100 +67,38 @@ jobs: - name: Run tests run: cargo test - # Mobile builds: Android APK - # iOS temporarily disabled: macos-latest Xcode now requires a Development Team / - # provisioning profile even with `flutter build ios --no-codesign`, which breaks - # the unsigned device build. Re-enable once codesigning is sorted. - build-mobile: - if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - platform: android - artifact: okena-android - - runs-on: ${{ matrix.os }} - + # React Native mobile client — TypeScript checks. + # + # The native app build (uniffi-bindgen-react-native cross-compiling + # crates/okena-mobile-ffi to an Android NDK .so / iOS xcframework, then + # `react-native run-*`) needs the mobile toolchain and is NOT yet wired into + # CI — see mobile/rn/README.md for the device-side steps. Until then CI + # type-checks and lints the RN/TS sources; the Rust FFI crate itself is + # already covered by the `check` job's workspace build + clippy + tests. + mobile-rn: + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: mobile/rn steps: - uses: actions/checkout@v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.93" - - - name: Add Android Rust targets - if: matrix.platform == 'android' - run: | - rustup target add armv7-linux-androideabi - rustup target add aarch64-linux-android - rustup target add x86_64-linux-android - rustup target add i686-linux-android - - - name: Add iOS Rust targets - if: matrix.platform == 'ios' - run: | - rustup target add aarch64-apple-ios - rustup target add x86_64-apple-ios - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + - name: Setup Node + uses: actions/setup-node@v4 with: - shared-key: mobile-${{ matrix.platform }} - workspaces: mobile/native - - - name: Setup Java (Android) - if: matrix.platform == 'android' - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Flutter pub get - working-directory: mobile - run: flutter pub get + node-version: 20 + cache: npm + cache-dependency-path: mobile/rn/package-lock.json - - name: Build Android APKs - if: matrix.platform == 'android' - working-directory: mobile - run: | - flutter build apk --release --split-per-abi - flutter build apk --release + - name: Install dependencies + run: npm ci - - name: Prepare Android artifact - if: matrix.platform == 'android' - run: | - mkdir -p dist - cp mobile/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk dist/okena-arm64-v8a.apk - cp mobile/build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/okena-armeabi-v7a.apk - cp mobile/build/app/outputs/flutter-apk/app-x86_64-release.apk dist/okena-x86_64.apk - cp mobile/build/app/outputs/flutter-apk/app-release.apk dist/okena-universal.apk - - - name: Build iOS (no codesign) - if: matrix.platform == 'ios' - working-directory: mobile - run: flutter build ios --release --no-codesign - - - name: Prepare iOS artifact - if: matrix.platform == 'ios' - run: | - mkdir -p dist - cd mobile/build/ios/iphoneos - zip -r ../../../../dist/okena-ios.zip Runner.app + - name: Typecheck + run: npm run typecheck - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact }} - path: dist/ - retention-days: 7 + - name: Lint + run: npm run lint # Full multi-platform build: tags + manual trigger build: @@ -260,7 +198,7 @@ jobs: # Create release when a tag is pushed release: - needs: [build, build-mobile] + needs: [build] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index 90039139..856894a3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,13 +18,18 @@ settings.local.json dist -# Mobile (Flutter) -mobile/.dart_tool/ -mobile/build/ -mobile/.flutter-plugins -mobile/.flutter-plugins-dependencies -mobile/pubspec.lock -mobile/native/target/ +# Mobile (React Native) +mobile/rn/node_modules/ +mobile/rn/ios/Pods/ +mobile/rn/ios/build/ +mobile/rn/android/.gradle/ +mobile/rn/android/build/ +mobile/rn/android/app/build/ +mobile/rn/.cxx/ +# uniffi-bindgen-react-native generated output (regenerated by `npm run ubrn:*`) +mobile/rn/src/generated/ +mobile/rn/cpp/generated/ +mobile/rn/rust_modules/ # Web client web/node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md index b49492bb..60e12a5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,8 @@ On Windows, build from **x64 Native Tools Command Prompt for VS 2022** to avoid ``` src/ # Desktop app — main binary, GPUI views, app coordinator -crates/ # Library crates (23 crates, see below) -mobile/ # Mobile app (Flutter + Rust FFI) +crates/ # Library crates (24 crates, see below) +mobile/ # Mobile app — React Native UI (mobile/rn) over the Rust core via uniffi (crates/okena-mobile-ffi) web/ # Web client (React + TypeScript + xterm.js) assets/ # Fonts, icons (assets/icons/*.svg referenced as icons/*.svg) scripts/ # Build & utility scripts @@ -56,6 +56,7 @@ Most logic lives in `crates/`. The `src/` modules are thin re-exports (`pub use | `okena-ext-github` | GitHub status extension | | `okena-ext-updater` | Self-update system | | `okena-core` | Shared types, API client, key handling | +| `okena-mobile-ffi` | uniffi FFI surface for the React Native mobile app (`mobile/rn`); self-contained ConnectionManager / TerminalHolder engine over `okena-core` | ## Module-Specific Context @@ -68,5 +69,5 @@ Read these when working in the corresponding areas: - `crates/okena-workspace/CLAUDE.md` — State management, LayoutNode tree, persistence - `crates/okena-terminal/CLAUDE.md` — PTY threading model, shell detection - `crates/okena-git/CLAUDE.md` — Diff parsing, worktree operations -- `mobile/CLAUDE.md` — Flutter + Rust FFI mobile app +- `mobile/rn/CLAUDE.md` — React Native mobile app (uniffi over `okena-mobile-ffi`) - `web/CLAUDE.md` — React web client diff --git a/Cargo.lock b/Cargo.lock index fbfcae3e..d64b6714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,40 +94,12 @@ dependencies = [ "equator", ] -[[package]] -name = "allo-isolate" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" -dependencies = [ - "anyhow", - "atomic", - "backtrace", -] - [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter 0.1.4", - "log", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -282,6 +254,48 @@ dependencies = [ "zbus", ] +[[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 2.1.1", + "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 0.7.15", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -317,6 +331,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-compression" version = "0.4.41" @@ -588,28 +615,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.8.8" @@ -692,6 +697,15 @@ 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 = "bincode" version = "1.3.3" @@ -838,12 +852,6 @@ dependencies = [ "serde", ] -[[package]] -name = "build-target" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" - [[package]] name = "built" version = "0.8.0" @@ -919,6 +927,38 @@ dependencies = [ "wayland-client", ] +[[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 = "cbc" version = "0.1.2" @@ -1045,15 +1085,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -1498,28 +1529,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "dart-sys" -version = "4.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" -dependencies = [ - "cc", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1552,17 +1561,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" -[[package]] -name = "delegate-attr" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "deranged" version = "0.5.8" @@ -2122,48 +2120,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "flutter_rust_bridge" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725" -dependencies = [ - "allo-isolate", - "android_logger", - "anyhow", - "build-target", - "bytemuck", - "byteorder", - "console_error_panic_hook", - "dart-sys", - "delegate-attr", - "flutter_rust_bridge_macros", - "futures", - "js-sys", - "lazy_static", - "log", - "oslog", - "portable-atomic", - "threadpool", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "flutter_rust_bridge_macros" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080" -dependencies = [ - "hex", - "md-5", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "fnv" version = "1.0.7" @@ -2271,10 +2227,13 @@ dependencies = [ ] [[package]] -name = "fs_extra" -version = "1.3.0" +name = "fs-err" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] [[package]] name = "fsevent-sys" @@ -3246,7 +3205,7 @@ version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb" dependencies = [ - "dashmap 6.1.0", + "dashmap", "gix-fs", "libc", "parking_lot", @@ -3452,6 +3411,17 @@ dependencies = [ "gl_generator", ] +[[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 = "gpu-allocator" version = "0.28.0" @@ -5803,6 +5773,23 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "okena-mobile-ffi" +version = "0.1.0" +dependencies = [ + "alacritty_terminal", + "anyhow", + "async-channel 2.5.0", + "log", + "okena-core", + "parking_lot", + "reqwest", + "serde_json", + "tokio", + "uniffi", + "uuid", +] + [[package]] name = "okena-remote-client" version = "0.1.0" @@ -6007,26 +5994,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "okena_mobile_native" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "async-channel 2.5.0", - "flutter_rust_bridge", - "futures", - "log", - "okena-core", - "parking_lot", - "reqwest", - "serde", - "serde_json", - "tokio", - "tokio-tungstenite 0.24.0", - "uuid", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -6138,17 +6105,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "oslog" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" -dependencies = [ - "cc", - "dashmap 5.5.3", - "log", -] - [[package]] name = "parking" version = "2.2.1" @@ -7334,7 +7290,6 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -7372,7 +7327,6 @@ version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -7514,6 +7468,26 @@ dependencies = [ "once_cell", ] +[[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 = "seahash" version = "4.1.0" @@ -7882,6 +7856,12 @@ 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 = "smol" version = "2.0.2" @@ -8343,6 +8323,15 @@ dependencies = [ "winapi-util", ] +[[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" @@ -8392,15 +8381,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "tiff" version = "0.11.3" @@ -9040,6 +9020,126 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "uniffi" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.9.12+spec-1.1.0", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml 0.9.12+spec-1.1.0", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -9554,6 +9654,15 @@ 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 7.1.3", +] + [[package]] name = "weezl" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 5ffbd9f3..e830ed73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "mobile/native", "crates/okena-core", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-views-remote", "crates/okena-theme"] +members = [".", "crates/okena-mobile-ffi", "crates/okena-core", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-views-remote", "crates/okena-theme"] resolver = "2" [package] @@ -98,9 +98,9 @@ semver = "1" # Remote server TLS (self-signed cert + dual-stack http/https on one port) rcgen = "0.13" -rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "logging", "tls12"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "tls12"] } rustls-pki-types = "1" -tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } hyper = { version = "1", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1", features = ["server-auto", "tokio"] } tower-service = "0.3" diff --git a/README.md b/README.md index a97dc1c7..e1cf8343 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ The install script includes built-in auto-update support. On macOS and Linux, Ok ### Remote Control & Companion Apps - **Remote API** - Local HTTP/WebSocket server for remote terminal control (see `docs/remote.md`) -- **Mobile app** - Flutter + Rust FFI companion app for Android/iOS (see `docs/mobile-status.md`) +- **Mobile app** - React Native companion app for Android/iOS over the Rust core via uniffi (see `docs/mobile-status.md`, code in `mobile/rn`) - **Web client** - Browser-based terminal access via built-in web UI - **Secure pairing** - HMAC-SHA256 token auth with rate-limited pairing codes @@ -194,7 +194,7 @@ Settings are stored in the platform's config directory (macOS: `~/Library/Applic | [Project Services](docs/services.md) | okena.yaml, Docker Compose integration, auto-restart | | [Git Worktrees](docs/worktrees.md) | Worktree management, sync watcher, path templates | | [Remote Control API](docs/remote.md) | HTTP/WebSocket API, pairing, authentication | -| [Mobile Client](docs/mobile-status.md) | Flutter + Rust FFI mobile companion app | +| [Mobile Client](docs/mobile-status.md) | React Native (uniffi) mobile companion app | ## Dependencies diff --git a/crates/okena-core/Cargo.toml b/crates/okena-core/Cargo.toml index b67f7859..ee2fcc8e 100644 --- a/crates/okena-core/Cargo.toml +++ b/crates/okena-core/Cargo.toml @@ -25,9 +25,12 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls" tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"], optional = true } async-channel = { version = "2.3", optional = true } futures = { version = "0.3", optional = true } -# Client TLS with certificate pinning (TOFU). aws_lc_rs matches reqwest's default -# rustls backend so a single CryptoProvider is shared across reqwest + tungstenite. -rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "tls12", "logging", "std"], optional = true } +# Client TLS with certificate pinning (TOFU). Uses the `ring` crypto backend: +# it's the portable provider that works on every target (notably Android/NDK, +# where `aws_lc_rs`'s jitter-entropy init segfaults), and reqwest 0.12's +# `rustls-tls` feature already pulls `ring` for hyper/tokio-rustls, so a single +# CryptoProvider is shared across reqwest + tungstenite. +rustls = { version = "0.23", default-features = false, features = ["ring", "tls12", "logging", "std"], optional = true } rustls-pki-types = { version = "1", optional = true } sha2 = { version = "0.10", optional = true } diff --git a/crates/okena-core/src/api.rs b/crates/okena-core/src/api.rs index 90eebfc2..21f21776 100644 --- a/crates/okena-core/src/api.rs +++ b/crates/okena-core/src/api.rs @@ -233,6 +233,10 @@ pub enum ApiLayoutNode { terminal_id: Option, minimized: bool, detached: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + cols: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + rows: Option, }, Split { direction: SplitDirection, @@ -602,6 +606,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -609,6 +615,8 @@ mod tests { terminal_id: Some("t2".into()), minimized: true, detached: true, + cols: None, + rows: None, }], }, ], @@ -946,6 +954,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -954,16 +964,22 @@ mod tests { terminal_id: Some("t2".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Terminal { terminal_id: None, minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Terminal { terminal_id: Some("t3".into()), minimized: false, detached: true, + cols: None, + rows: None, }, ], }, diff --git a/crates/okena-core/src/client/connection.rs b/crates/okena-core/src/client/connection.rs index 577da86c..a449ad2b 100644 --- a/crates/okena-core/src/client/connection.rs +++ b/crates/okena-core/src/client/connection.rs @@ -1,7 +1,7 @@ use crate::api::StateResponse; use crate::client::config::RemoteConnectionConfig; use crate::client::id::make_prefixed_id; -use crate::client::state::{collect_all_terminal_ids, collect_state_terminal_ids, diff_states}; +use crate::client::state::{collect_all_terminal_ids, collect_state_terminal_ids, collect_terminal_sizes, diff_states}; use crate::client::types::{ ConnectionEvent, ConnectionStatus, SessionError, WsClientMessage, TOKEN_REFRESH_AGE_SECS, }; @@ -13,16 +13,19 @@ use tokio_tungstenite::tungstenite; /// Platform-specific operations that the generic client delegates to. /// /// Desktop creates `Terminal` objects and inserts into `TerminalsRegistry`. -/// Mobile may create Flutter-side terminal state via FFI callbacks. +/// Mobile creates `TerminalHolder` state via the FFI binding crate. pub trait ConnectionHandler: Send + Sync + 'static { /// Terminal discovered — create platform terminal object. /// `ws_sender` is for constructing a transport that sends WS commands. + /// `cols`/`rows` are the server's current terminal dimensions (0 if unknown). fn create_terminal( &self, connection_id: &str, terminal_id: &str, prefixed_id: &str, ws_sender: async_channel::Sender, + cols: u16, + rows: u16, ); /// Binary PTY output arrived — route to the terminal's emulator. fn on_terminal_output(&self, prefixed_id: &str, data: &[u8]); @@ -673,9 +676,11 @@ impl RemoteClient { handler.remove_terminals_except(&config.id, ¤t_ids); let terminal_ids = collect_state_terminal_ids(&state); + let size_map = collect_terminal_sizes(&state); for tid in &terminal_ids { let prefixed = make_prefixed_id(&config.id, tid); - handler.create_terminal(&config.id, tid, &prefixed, ws_tx.clone()); + let (cols, rows) = size_map.get(tid).copied().unwrap_or((0, 0)); + handler.create_terminal(&config.id, tid, &prefixed, ws_tx.clone(), cols, rows); } // Notify state received @@ -892,16 +897,24 @@ impl RemoteClient { { let diff = diff_states(&cached_state, &new_state); + let new_size_map = + collect_terminal_sizes(&new_state); // Add new terminals via handler for tid in &diff.added_terminals { let prefixed = make_prefixed_id(&config_id, tid); + let (cols, rows) = new_size_map + .get(tid) + .copied() + .unwrap_or((0, 0)); handler_clone.create_terminal( &config_id, tid, &prefixed, ws_tx_clone.clone(), + cols, + rows, ); } diff --git a/crates/okena-core/src/client/mod.rs b/crates/okena-core/src/client/mod.rs index 6ab38d5b..24632f7d 100644 --- a/crates/okena-core/src/client/mod.rs +++ b/crates/okena-core/src/client/mod.rs @@ -8,5 +8,5 @@ pub mod types; pub use config::RemoteConnectionConfig; pub use connection::{ConnectionHandler, RemoteClient}; pub use id::{is_remote_terminal, make_prefixed_id, strip_prefix}; -pub use state::{collect_all_terminal_ids, collect_state_terminal_ids, diff_states, StateDiff}; +pub use state::{collect_all_terminal_ids, collect_state_terminal_ids, collect_terminal_sizes, diff_states, StateDiff}; pub use types::{ConnectionEvent, ConnectionStatus, WsClientMessage, TOKEN_REFRESH_AGE_SECS}; diff --git a/crates/okena-core/src/client/state.rs b/crates/okena-core/src/client/state.rs index 6df1a70b..fa184239 100644 --- a/crates/okena-core/src/client/state.rs +++ b/crates/okena-core/src/client/state.rs @@ -112,10 +112,48 @@ fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec) { } } +/// Collect terminal sizes from all projects in a StateResponse. +/// +/// Returns a map of terminal_id → (cols, rows) for terminals that have +/// size information in the layout tree. +pub fn collect_terminal_sizes(state: &StateResponse) -> std::collections::HashMap { + let mut sizes = std::collections::HashMap::new(); + for project in &state.projects { + if let Some(ref layout) = project.layout { + collect_layout_terminal_sizes(layout, &mut sizes); + } + } + sizes +} + +fn collect_layout_terminal_sizes( + node: &ApiLayoutNode, + sizes: &mut std::collections::HashMap, +) { + match node { + ApiLayoutNode::Terminal { + terminal_id, + cols, + rows, + .. + } => { + if let (Some(id), Some(c), Some(r)) = (terminal_id, cols, rows) { + sizes.insert(id.clone(), (*c, *r)); + } + } + ApiLayoutNode::Split { children, .. } | ApiLayoutNode::Tabs { children, .. } => { + for child in children { + collect_layout_terminal_sizes(child, sizes); + } + } + } +} + #[cfg(test)] mod tests { use super::*; use crate::api::{ApiLayoutNode, ApiProject, StateResponse}; + use crate::theme::FolderColor; use crate::types::SplitDirection; fn make_state(projects: Vec) -> StateResponse { @@ -137,6 +175,8 @@ mod tests { terminal_id: Some(terminal_ids[0].to_string()), minimized: false, detached: false, + cols: None, + rows: None, }) } else { Some(ApiLayoutNode::Split { @@ -148,6 +188,8 @@ mod tests { terminal_id: Some(tid.to_string()), minimized: false, detached: false, + cols: None, + rows: None, }) .collect(), }) @@ -202,4 +244,43 @@ mod tests { assert!(diff.removed_terminals.is_empty()); assert!(diff.changed_projects.is_empty()); } + + #[test] + fn collect_terminal_sizes_extracts_from_layout() { + let state = make_state(vec![ApiProject { + id: "p1".into(), + name: "p1".into(), + path: "/tmp".into(), + show_in_overview: true, + layout: Some(ApiLayoutNode::Split { + direction: SplitDirection::Horizontal, + sizes: vec![50.0, 50.0], + children: vec![ + ApiLayoutNode::Terminal { + terminal_id: Some("t1".into()), + minimized: false, + detached: false, + cols: Some(120), + rows: Some(40), + }, + ApiLayoutNode::Terminal { + terminal_id: Some("t2".into()), + minimized: false, + detached: false, + cols: None, + rows: None, + }, + ], + }), + terminal_names: Default::default(), + git_status: None, + folder_color: FolderColor::default(), + services: Vec::new(), + worktree_info: None, + worktree_ids: Vec::new(), + }]); + let sizes = collect_terminal_sizes(&state); + assert_eq!(sizes.get("t1"), Some(&(120, 40))); + assert_eq!(sizes.get("t2"), None); + } } diff --git a/crates/okena-core/src/client/tls.rs b/crates/okena-core/src/client/tls.rs index fc49997c..909353aa 100644 --- a/crates/okena-core/src/client/tls.rs +++ b/crates/okena-core/src/client/tls.rs @@ -153,7 +153,7 @@ impl ServerCertVerifier for PinnedCertVerifier { } fn provider() -> Arc { - Arc::new(rustls::crypto::aws_lc_rs::default_provider()) + Arc::new(rustls::crypto::ring::default_provider()) } /// Build a rustls `ClientConfig` that pins the server cert via [`PinnedCertVerifier`]. @@ -162,11 +162,11 @@ fn pinned_client_config(pinned: Option, observed: ObservedFingerprint) - let verifier = Arc::new(PinnedCertVerifier::new(pinned, observed, provider.clone())); #[allow( clippy::expect_used, - reason = "aws_lc_rs default provider always supports the default protocol versions" + reason = "ring default provider always supports the default protocol versions" )] let builder = rustls::ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("aws_lc_rs default provider supports default protocol versions"); + .expect("ring default provider supports default protocol versions"); builder .dangerous() .with_custom_certificate_verifier(verifier) diff --git a/crates/okena-core/src/keys.rs b/crates/okena-core/src/keys.rs index a9023841..19f3d4fd 100644 --- a/crates/okena-core/src/keys.rs +++ b/crates/okena-core/src/keys.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; pub enum SpecialKey { Enter, Escape, + Backspace, + Delete, CtrlC, CtrlD, CtrlZ, @@ -17,8 +19,6 @@ pub enum SpecialKey { End, PageUp, PageDown, - Backspace, - Delete, } impl SpecialKey { @@ -27,6 +27,8 @@ impl SpecialKey { match self { SpecialKey::Enter => b"\r", SpecialKey::Escape => b"\x1b", + SpecialKey::Backspace => b"\x7f", + SpecialKey::Delete => b"\x1b[3~", SpecialKey::CtrlC => b"\x03", SpecialKey::CtrlD => b"\x04", SpecialKey::CtrlZ => b"\x1a", @@ -39,8 +41,6 @@ impl SpecialKey { SpecialKey::End => b"\x1b[F", SpecialKey::PageUp => b"\x1b[5~", SpecialKey::PageDown => b"\x1b[6~", - SpecialKey::Backspace => b"\x7f", - SpecialKey::Delete => b"\x1b[3~", } } } @@ -54,6 +54,8 @@ mod tests { let keys = vec![ SpecialKey::Enter, SpecialKey::Escape, + SpecialKey::Backspace, + SpecialKey::Delete, SpecialKey::CtrlC, SpecialKey::CtrlD, SpecialKey::CtrlZ, @@ -66,8 +68,6 @@ mod tests { SpecialKey::End, SpecialKey::PageUp, SpecialKey::PageDown, - SpecialKey::Backspace, - SpecialKey::Delete, ]; for key in keys { let json = serde_json::to_string(&key).unwrap(); @@ -80,6 +80,8 @@ mod tests { fn special_key_to_bytes() { assert_eq!(SpecialKey::Enter.to_bytes(), b"\r"); assert_eq!(SpecialKey::Escape.to_bytes(), b"\x1b"); + assert_eq!(SpecialKey::Backspace.to_bytes(), b"\x7f"); + assert_eq!(SpecialKey::Delete.to_bytes(), b"\x1b[3~"); assert_eq!(SpecialKey::CtrlC.to_bytes(), b"\x03"); assert_eq!(SpecialKey::CtrlD.to_bytes(), b"\x04"); assert_eq!(SpecialKey::CtrlZ.to_bytes(), b"\x1a"); @@ -92,7 +94,5 @@ mod tests { assert_eq!(SpecialKey::End.to_bytes(), b"\x1b[F"); assert_eq!(SpecialKey::PageUp.to_bytes(), b"\x1b[5~"); assert_eq!(SpecialKey::PageDown.to_bytes(), b"\x1b[6~"); - assert_eq!(SpecialKey::Backspace.to_bytes(), b"\x7f"); - assert_eq!(SpecialKey::Delete.to_bytes(), b"\x1b[3~"); } } diff --git a/crates/okena-layout/src/lib.rs b/crates/okena-layout/src/lib.rs index a25060e2..b27db1ed 100644 --- a/crates/okena-layout/src/lib.rs +++ b/crates/okena-layout/src/lib.rs @@ -739,6 +739,7 @@ impl LayoutNode { terminal_id, minimized, detached, + .. } => LayoutNode::Terminal { terminal_id: terminal_id.clone(), minimized: *minimized, @@ -773,6 +774,7 @@ impl LayoutNode { terminal_id, minimized, detached, + .. } => LayoutNode::Terminal { terminal_id: terminal_id.as_ref().map(|id| format!("{}:{}", prefix, id)), minimized: *minimized, @@ -807,31 +809,48 @@ impl LayoutNode { /// Convert to API layout node. pub fn to_api(&self) -> okena_core::api::ApiLayoutNode { + self.to_api_with_sizes(&std::collections::HashMap::new()) + } + + /// Convert to API, populating terminal `cols`/`rows` from the given size map. + pub fn to_api_with_sizes( + &self, + sizes: &std::collections::HashMap, + ) -> okena_core::api::ApiLayoutNode { match self { LayoutNode::Terminal { terminal_id, minimized, detached, .. - } => okena_core::api::ApiLayoutNode::Terminal { - terminal_id: terminal_id.clone(), - minimized: *minimized, - detached: *detached, - }, + } => { + let (cols, rows) = terminal_id + .as_ref() + .and_then(|id| sizes.get(id)) + .map(|&(c, r)| (Some(c), Some(r))) + .unwrap_or((None, None)); + okena_core::api::ApiLayoutNode::Terminal { + terminal_id: terminal_id.clone(), + minimized: *minimized, + detached: *detached, + cols, + rows, + } + } LayoutNode::Split { direction, - sizes, + sizes: split_sizes, children, } => okena_core::api::ApiLayoutNode::Split { direction: *direction, - sizes: sizes.clone(), - children: children.iter().map(LayoutNode::to_api).collect(), + sizes: split_sizes.clone(), + children: children.iter().map(|c| c.to_api_with_sizes(sizes)).collect(), }, LayoutNode::Tabs { children, active_tab, } => okena_core::api::ApiLayoutNode::Tabs { - children: children.iter().map(LayoutNode::to_api).collect(), + children: children.iter().map(|c| c.to_api_with_sizes(sizes)).collect(), active_tab: *active_tab, }, } diff --git a/crates/okena-mobile-ffi/Cargo.toml b/crates/okena-mobile-ffi/Cargo.toml new file mode 100644 index 00000000..7f2e5d2a --- /dev/null +++ b/crates/okena-mobile-ffi/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "okena-mobile-ffi" +version = "0.1.0" +edition = "2024" + +[lib] +# cdylib + staticlib for the eventual ubrn-generated Android (NDK) / iOS +# (xcframework) artifacts; `lib` so it can also be linked as a normal Rust +# library (e.g. for `cargo test` and downstream tooling). +crate-type = ["cdylib", "staticlib", "lib"] + +[dependencies] +# Shared protocol / TLS / WS / terminal-emulation core. The `client` feature +# pulls reqwest + tokio-tungstenite with the rustls backends (NDK-friendly — no +# OpenSSL cross-compile), so we inherit that backend selection via unification. +okena-core = { path = "../okena-core", features = ["client"] } + +# uniffi in proc-macro mode (no UDL). `tokio` enables async export via +# `#[uniffi::export(async_runtime = "tokio")]`. Pinned to 0.31 to match the +# `uniffi-bindgen-react-native` (ubrn) version in mobile/rn (ubrn 0.31.0-3 pins +# `uniffi =0.31.0`, and the metadata contract version must match the bindgen). +uniffi = { version = "0.31", features = ["tokio"] } + +# The connection/terminal engine carried over from the retired `mobile/native` +# crate (ConnectionManager, MobileConnectionHandler, TerminalHolder). These are +# its direct dependencies — no flutter_rust_bridge. +alacritty_terminal = "0.25" +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "time"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +async-channel = "2.3" +parking_lot = "0.12" +uuid = { version = "1.10", features = ["v4"] } +log = "0.4" +serde_json = "1.0" +anyhow = "1.0" diff --git a/crates/okena-mobile-ffi/src/api/connection.rs b/crates/okena-mobile-ffi/src/api/connection.rs new file mode 100644 index 00000000..b414457f --- /dev/null +++ b/crates/okena-mobile-ffi/src/api/connection.rs @@ -0,0 +1,43 @@ +//! Connection status extraction. +//! +//! The lifecycle entry points (`init_app`, `connect`, `pair`, …) are exported +//! directly from `crate::lib` via uniffi; this module only holds the plain +//! `ConnectionStatus` enum (mirrored as a uniffi enum in `crate::types`) and the +//! one accessor `lib.rs` delegates to. + +use crate::client::manager::ConnectionManager; + +/// Connection status returned via FFI. +/// +/// Simplified version of okena_core's ConnectionStatus — collapses `Reconnecting { attempt }` +/// into `Connecting` since mobile UI doesn't need the attempt count. +#[derive(Debug, Clone)] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Pairing, + Error { message: String }, +} + +impl From for ConnectionStatus { + fn from(status: okena_core::client::ConnectionStatus) -> Self { + match status { + okena_core::client::ConnectionStatus::Disconnected => ConnectionStatus::Disconnected, + okena_core::client::ConnectionStatus::Connecting => ConnectionStatus::Connecting, + okena_core::client::ConnectionStatus::Connected => ConnectionStatus::Connected, + okena_core::client::ConnectionStatus::Pairing => ConnectionStatus::Pairing, + okena_core::client::ConnectionStatus::Reconnecting { .. } => { + ConnectionStatus::Connecting + } + okena_core::client::ConnectionStatus::Error(msg) => { + ConnectionStatus::Error { message: msg } + } + } + } +} + +/// Get current connection status. +pub fn connection_status(conn_id: String) -> ConnectionStatus { + ConnectionManager::get().get_status(&conn_id).into() +} diff --git a/mobile/native/src/api/mod.rs b/crates/okena-mobile-ffi/src/api/mod.rs similarity index 100% rename from mobile/native/src/api/mod.rs rename to crates/okena-mobile-ffi/src/api/mod.rs diff --git a/crates/okena-mobile-ffi/src/api/state.rs b/crates/okena-mobile-ffi/src/api/state.rs new file mode 100644 index 00000000..699593a1 --- /dev/null +++ b/crates/okena-mobile-ffi/src/api/state.rs @@ -0,0 +1,171 @@ +//! Cached-state extraction into plain, FFI-friendly structs. +//! +//! Only the read-side accessors that `lib.rs` delegates to live here. Every +//! mutating action (terminal / git / service / project / layout) is exported +//! directly from `lib.rs` via uniffi against `ConnectionManager`, so it is not +//! duplicated here. The uniffi mirrors of these structs (with +//! `#[derive(uniffi::Record)]`) live in `crate::types`. + +use std::collections::HashMap; + +use crate::client::manager::ConnectionManager; +use okena_core::api::ApiLayoutNode; + +/// Flat FFI-friendly project info. +#[derive(Debug, Clone)] +pub struct ProjectInfo { + pub id: String, + pub name: String, + pub path: String, + pub show_in_overview: bool, + pub terminal_ids: Vec, + pub terminal_names: HashMap, + pub git_branch: Option, + pub git_lines_added: u32, + pub git_lines_removed: u32, + pub services: Vec, + pub folder_color: String, +} + +/// FFI-friendly service info. +#[derive(Debug, Clone)] +pub struct ServiceInfo { + pub name: String, + pub status: String, + pub terminal_id: Option, + pub ports: Vec, + pub exit_code: Option, + pub kind: String, + pub is_extra: bool, +} + +/// FFI-friendly folder info. +#[derive(Debug, Clone)] +pub struct FolderInfo { + pub id: String, + pub name: String, + pub project_ids: Vec, + pub folder_color: String, +} + +/// FFI-friendly fullscreen info. +#[derive(Debug, Clone)] +pub struct FullscreenInfo { + pub project_id: String, + pub terminal_id: String, +} + +/// Get all projects from the cached remote state. +pub fn get_projects(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let state = match mgr.get_state(&conn_id) { + Some(s) => s, + None => return Vec::new(), + }; + + state + .projects + .iter() + .map(|p| { + let terminal_ids = if let Some(ref layout) = p.layout { + let mut ids = Vec::new(); + collect_layout_ids_vec(layout, &mut ids); + ids + } else { + Vec::new() + }; + let (git_branch, git_lines_added, git_lines_removed) = + if let Some(ref gs) = p.git_status { + (gs.branch.clone(), gs.lines_added as u32, gs.lines_removed as u32) + } else { + (None, 0, 0) + }; + let services = p + .services + .iter() + .map(|s| ServiceInfo { + name: s.name.clone(), + status: s.status.clone(), + terminal_id: s.terminal_id.clone(), + ports: s.ports.clone(), + exit_code: s.exit_code, + kind: s.kind.clone(), + is_extra: s.is_extra, + }) + .collect(); + ProjectInfo { + id: p.id.clone(), + name: p.name.clone(), + path: p.path.clone(), + show_in_overview: p.show_in_overview, + terminal_ids, + terminal_names: p.terminal_names.clone(), + git_branch, + git_lines_added, + git_lines_removed, + services, + folder_color: format!("{:?}", p.folder_color).to_lowercase(), + } + }) + .collect() +} + +/// Get the focused project ID from the cached remote state. +pub fn get_focused_project_id(conn_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id) + .and_then(|s| s.focused_project_id.clone()) +} + +/// Get folders from the cached remote state. +pub fn get_folders(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let state = match mgr.get_state(&conn_id) { + Some(s) => s, + None => return Vec::new(), + }; + state + .folders + .iter() + .map(|f| FolderInfo { + id: f.id.clone(), + name: f.name.clone(), + project_ids: f.project_ids.clone(), + folder_color: format!("{:?}", f.folder_color).to_lowercase(), + }) + .collect() +} + +/// Get the project order from the cached remote state. +pub fn get_project_order(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id) + .map(|s| s.project_order.clone()) + .unwrap_or_default() +} + +/// Get fullscreen terminal info. +pub fn get_fullscreen_terminal(conn_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id).and_then(|s| { + s.fullscreen_terminal.as_ref().map(|f| FullscreenInfo { + project_id: f.project_id.clone(), + terminal_id: f.terminal_id.clone(), + }) + }) +} + +fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec) { + match node { + ApiLayoutNode::Terminal { terminal_id, .. } => { + if let Some(id) = terminal_id { + ids.push(id.clone()); + } + } + ApiLayoutNode::Split { children, .. } | ApiLayoutNode::Tabs { children, .. } => { + for child in children { + collect_layout_ids_vec(child, ids); + } + } + } +} diff --git a/crates/okena-mobile-ffi/src/api/terminal.rs b/crates/okena-mobile-ffi/src/api/terminal.rs new file mode 100644 index 00000000..c4dd160a --- /dev/null +++ b/crates/okena-mobile-ffi/src/api/terminal.rs @@ -0,0 +1,52 @@ +//! Plain data structs extracted from the terminal grid. +//! +//! `TerminalHolder` (`crate::client::terminal_holder`) produces these; the +//! uniffi-facing equivalents (`#[derive(uniffi::Record)]`) live in +//! `crate::types` and convert from these via `From`. + +/// Cell data for FFI transfer (flat, no pointers). +#[derive(Debug, Clone)] +pub struct CellData { + /// The character in this cell. + pub character: String, + /// Foreground color as ARGB packed u32. + pub fg: u32, + /// Background color as ARGB packed u32. + pub bg: u32, + /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). + pub flags: u8, +} + +/// Cursor shape variants. +#[derive(Debug, Clone)] +pub enum CursorShape { + Block, + Underline, + Beam, +} + +/// Cursor state for FFI transfer. +#[derive(Debug, Clone)] +pub struct CursorState { + pub col: u16, + pub row: u16, + pub shape: CursorShape, + pub visible: bool, +} + +/// Scroll info for FFI transfer. +#[derive(Debug, Clone)] +pub struct ScrollInfo { + pub total_lines: u32, + pub visible_lines: u32, + pub display_offset: u32, +} + +/// Selection bounds for FFI transfer. +#[derive(Debug, Clone)] +pub struct SelectionBounds { + pub start_col: u16, + pub start_row: i32, + pub end_col: u16, + pub end_row: i32, +} diff --git a/mobile/native/src/client/handler.rs b/crates/okena-mobile-ffi/src/client/handler.rs similarity index 82% rename from mobile/native/src/client/handler.rs rename to crates/okena-mobile-ffi/src/client/handler.rs index b7427cad..ff1de1b4 100644 --- a/mobile/native/src/client/handler.rs +++ b/crates/okena-mobile-ffi/src/client/handler.rs @@ -40,13 +40,16 @@ impl ConnectionHandler for MobileConnectionHandler { _terminal_id: &str, prefixed_id: &str, _ws_sender: async_channel::Sender, + cols: u16, + rows: u16, ) { // Skip if terminal already exists — avoids leaking the old TerminalHolder // (and its alacritty grid) on reconnect when the server re-sends creates. if self.terminals.read().contains_key(prefixed_id) { return; } - let holder = TerminalHolder::new(80, 24); + let (c, r) = if cols > 0 && rows > 0 { (cols, rows) } else { (80, 24) }; + let holder = TerminalHolder::new(c, r); self.terminals .write() .insert(prefixed_id.to_string(), holder); @@ -115,7 +118,7 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); assert!(handler.terminals().read().contains_key("remote:conn1:t1")); handler.remove_terminal("remote:conn1:t1"); @@ -127,14 +130,14 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone()); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone(), 0, 0); let ptr1 = { let terminals = handler.terminals().read(); terminals.get("remote:conn1:t1").unwrap() as *const TerminalHolder }; // Second create with same prefixed_id should be a no-op - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); let ptr2 = { let terminals = handler.terminals().read(); terminals.get("remote:conn1:t1").unwrap() as *const TerminalHolder @@ -149,9 +152,9 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone()); - handler.create_terminal("conn1", "t2", "remote:conn1:t2", tx.clone()); - handler.create_terminal("conn2", "t3", "remote:conn2:t3", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone(), 0, 0); + handler.create_terminal("conn1", "t2", "remote:conn1:t2", tx.clone(), 0, 0); + handler.create_terminal("conn2", "t3", "remote:conn2:t3", tx, 0, 0); handler.remove_all_terminals("conn1"); @@ -161,12 +164,36 @@ mod tests { assert!(terminals.contains_key("remote:conn2:t3")); } + #[test] + fn create_terminal_uses_server_size() { + let handler = make_handler(); + let (tx, _rx) = async_channel::bounded(1); + + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 160, 48); + let terminals = handler.terminals().read(); + let holder = terminals.get("remote:conn1:t1").unwrap(); + let cells = holder.get_visible_cells(&okena_core::theme::DARK_THEME); + assert_eq!(cells.len(), 160 * 48); + } + + #[test] + fn create_terminal_falls_back_to_default_on_zero_size() { + let handler = make_handler(); + let (tx, _rx) = async_channel::bounded(1); + + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); + let terminals = handler.terminals().read(); + let holder = terminals.get("remote:conn1:t1").unwrap(); + let cells = holder.get_visible_cells(&okena_core::theme::DARK_THEME); + assert_eq!(cells.len(), 80 * 24); + } + #[test] fn on_terminal_output_routes_data() { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); handler.on_terminal_output("remote:conn1:t1", b"hello"); let terminals = handler.terminals().read(); diff --git a/mobile/native/src/client/manager.rs b/crates/okena-mobile-ffi/src/client/manager.rs similarity index 97% rename from mobile/native/src/client/manager.rs rename to crates/okena-mobile-ffi/src/client/manager.rs index a1a26c11..9544eff8 100644 --- a/mobile/native/src/client/manager.rs +++ b/crates/okena-mobile-ffi/src/client/manager.rs @@ -288,6 +288,16 @@ impl ConnectionManager { conn_id: &str, action: ActionRequest, ) -> anyhow::Result<()> { + self.send_action_with_response(conn_id, action).await?; + Ok(()) + } + + /// Send an action to the remote server and return the response body. + pub async fn send_action_with_response( + &self, + conn_id: &str, + action: ActionRequest, + ) -> anyhow::Result { let (host, port, token) = { let connections = self.connections.read(); let conn = connections @@ -316,7 +326,8 @@ impl ConnectionManager { anyhow::bail!("Action failed ({}): {}", status, body); } - Ok(()) + let body = resp.text().await.unwrap_or_default(); + Ok(body) } /// Background task that drains the event channel and updates connection state. diff --git a/mobile/native/src/client/mod.rs b/crates/okena-mobile-ffi/src/client/mod.rs similarity index 100% rename from mobile/native/src/client/mod.rs rename to crates/okena-mobile-ffi/src/client/mod.rs diff --git a/mobile/native/src/client/terminal_holder.rs b/crates/okena-mobile-ffi/src/client/terminal_holder.rs similarity index 97% rename from mobile/native/src/client/terminal_holder.rs rename to crates/okena-mobile-ffi/src/client/terminal_holder.rs index 5eca7281..03bd5497 100644 --- a/mobile/native/src/client/terminal_holder.rs +++ b/crates/okena-mobile-ffi/src/client/terminal_holder.rs @@ -247,6 +247,11 @@ impl TerminalHolder { } /// Take the dirty flag (returns true if it was dirty, resets to false). + /// + /// Reserved for the RN render loop's `is_dirty()`-gated repaint (migration + /// plan Decision C): the canvas will call this once per frame to consume the + /// flag. Not yet wired into the uniffi surface, hence `allow(dead_code)`. + #[allow(dead_code)] pub fn take_dirty(&self) -> bool { self.dirty.swap(false, Ordering::Relaxed) } diff --git a/crates/okena-mobile-ffi/src/lib.rs b/crates/okena-mobile-ffi/src/lib.rs new file mode 100644 index 00000000..aa3922a5 --- /dev/null +++ b/crates/okena-mobile-ffi/src/lib.rs @@ -0,0 +1,1001 @@ +//! uniffi FFI surface for the React Native mobile app. +//! +//! Exposes ~60 functions via uniffi proc-macros (`#[uniffi::export]`, +//! `#[derive(uniffi::Record)]`, `#[derive(uniffi::Enum)]`) so +//! `uniffi-bindgen-react-native` (ubrn) can emit a JSI TurboModule. This +//! replaces the `flutter_rust_bridge` `api/` layer of the retired `mobile/native` +//! crate, whose plain-Rust engine now lives here directly (see below). +//! +//! The networking/emulation engine lives in `crate::client` +//! (`ConnectionManager`, `MobileConnectionHandler`, `TerminalHolder`) and the +//! plain state-extraction structs in `crate::api` — both carried over from the +//! retired `mobile/native` Flutter crate (frb attributes stripped). This crate +//! is self-contained: it does not depend on any Flutter tooling. +//! +//! ## Async strategy +//! The frb api split sync vs. async based on whether the body actually awaits: +//! - Functions whose bodies are synchronous (the `with_terminal` / `get_state` +//! accessors, `send_text` / `send_special_key`, which only enqueue a WS +//! message via `send_ws_message`) are exported as plain sync uniffi fns — +//! important for the render hot path. +//! - Functions that genuinely `.await` reqwest (`*_terminal` actions, git, +//! services, project/layout mutations, `read_content`) are exported as +//! `async fn` with `#[uniffi::export(async_runtime = "tokio")]`, which ubrn +//! maps to JS Promises. + +#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))] + +mod api; +mod client; +mod types; + +use okena_core::api::ActionRequest; +use okena_core::client::{collect_state_terminal_ids, WsClientMessage}; +use okena_core::keys::SpecialKey; +use okena_core::theme::DARK_THEME; +use okena_core::types::{DiffMode, SplitDirection}; + +use crate::client::manager::ConnectionManager; + +pub use types::{ + CellData, ConnectionStatus, CursorShape, CursorState, FolderInfo, FullscreenInfo, ProjectInfo, + ScrollInfo, SelectionBounds, ServiceInfo, +}; + +uniffi::setup_scaffolding!(); + +// ── Connection lifecycle ──────────────────────────────────────────── + +/// Initialize the app. Must be called once at startup before any other fn. +#[uniffi::export] +pub fn init_app() { + ConnectionManager::init(); +} + +/// Connect to an Okena remote server. Returns a connection ID. +/// +/// `tls` and `pinned_cert_fingerprint` describe the desired transport security +/// for this connection (RN ships TLS-capable from day one). They are accepted +/// at the binding boundary so the RN UI and persisted server config can carry +/// the TLS flag. +/// +/// `tls` is forwarded to `ConnectionManager::add_connection`. The pinned cert, +/// however, is established via TOFU during the handshake (the manager has no +/// param to pre-seed a fingerprint — it records it from the `TlsUpgraded` / +/// pairing events), so `pinned_cert_fingerprint` is not forwarded yet. +#[uniffi::export] +pub fn connect( + host: String, + port: u16, + saved_token: Option, + tls: bool, + pinned_cert_fingerprint: Option, +) -> String { + // `pinned_cert_fingerprint` is intentionally not forwarded yet — the + // manager pins via TOFU events rather than an up-front fingerprint. Touch it + // so the unused-var lint stays quiet and the intent is explicit. + let _ = pinned_cert_fingerprint; + let mgr = ConnectionManager::get(); + let conn_id = mgr.add_connection(&host, port, saved_token, tls); + mgr.connect(&conn_id); + conn_id +} + +/// Get the current auth token for a connection (if paired). +#[uniffi::export] +pub fn get_token(conn_id: String) -> Option { + ConnectionManager::get().get_token(&conn_id) +} + +/// Pair with the server using a pairing code. (Body is synchronous: it only +/// kicks off the manager's pairing task.) +#[uniffi::export] +pub fn pair(conn_id: String, code: String) { + ConnectionManager::get().pair(&conn_id, &code); +} + +/// Disconnect from a server. +#[uniffi::export] +pub fn disconnect(conn_id: String) { + ConnectionManager::get().disconnect(&conn_id); +} + +/// Get current connection status. +#[uniffi::export] +pub fn connection_status(conn_id: String) -> ConnectionStatus { + // Delegate to the native api fn, which already maps okena-core's status + // (collapsing `Reconnecting` into `Connecting`) into its own enum; we then + // convert that into our uniffi enum. + crate::api::connection::connection_status(conn_id).into() +} + +/// Seconds since last WS activity (terminal output). Large value if missing. +#[uniffi::export] +pub fn seconds_since_activity(conn_id: String) -> f64 { + ConnectionManager::get().seconds_since_activity(&conn_id) +} + +// ── Terminal rendering / input ────────────────────────────────────── + +/// Get the visible terminal cells for rendering (records form). +/// +/// Kept for non-hot-path callers; the render loop should prefer +/// [`get_visible_cells_packed`]. +#[uniffi::export] +pub fn get_visible_cells(conn_id: String, terminal_id: String) -> Vec { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder + .get_visible_cells(&DARK_THEME) + .into_iter() + .map(Into::into) + .collect() + }) + .unwrap_or_default() +} + +/// Get the visible terminal grid as a packed little-endian byte buffer. +/// +/// This is the hot-path render bridge (migration plan Decision C): instead of +/// marshaling thousands of `CellData` records per frame across JSI, the RN +/// Skia canvas reads this compact buffer directly as an `ArrayBuffer`. +/// +/// ## Format (all multi-byte values little-endian) +/// ```text +/// Header (4 bytes): +/// cols : u16 LE +/// rows : u16 LE +/// Then cols*rows cells, row-major, 13 bytes each: +/// codepoint : u32 LE Unicode scalar of the cell's primary char +/// (0x20 / space for empty or wide-char-spacer cells) +/// fg : u32 LE ARGB +/// bg : u32 LE ARGB +/// flags : u8 bold(1)|italic(2)|underline(4)|strikethrough(8)| +/// inverse(16)|dim(32) +/// ``` +/// Total length = 4 + cols*rows*13 bytes. Built from the same `CellData` list +/// `get_visible_cells` returns. If the terminal is missing, returns a 0x0 +/// header (`[0, 0, 0, 0]`). +#[uniffi::export] +pub fn get_visible_cells_packed(conn_id: String, terminal_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let cells = mgr + .with_terminal(&conn_id, &terminal_id, |holder| { + holder.get_visible_cells(&DARK_THEME) + }) + .unwrap_or_default(); + + // Determine grid dimensions from the live cursor/grid via scroll_info would + // require another lock; instead derive cols from the row width recorded by + // the holder. The cell list is exactly cols*rows row-major, so we recover + // dimensions from the holder's reported scroll info (visible_lines = rows) + // and divide. To avoid a second terminal access we read both in one borrow. + let (cols, rows) = mgr + .with_terminal(&conn_id, &terminal_id, |holder| { + let (_total, visible, _offset) = holder.scroll_info(); + let rows = visible as u16; + let cols = if visible > 0 { + (cells.len() / visible) as u16 + } else { + 0 + }; + (cols, rows) + }) + .unwrap_or((0, 0)); + + let mut buf = Vec::with_capacity(4 + cells.len() * 13); + buf.extend_from_slice(&cols.to_le_bytes()); + buf.extend_from_slice(&rows.to_le_bytes()); + for cell in &cells { + // Primary scalar; empty (wide-char spacer) or space → 0x20. + let codepoint: u32 = cell.character.chars().next().map(|c| c as u32).unwrap_or(0x20); + let codepoint = if codepoint == 0 { 0x20 } else { codepoint }; + buf.extend_from_slice(&codepoint.to_le_bytes()); + buf.extend_from_slice(&cell.fg.to_le_bytes()); + buf.extend_from_slice(&cell.bg.to_le_bytes()); + buf.push(cell.flags); + } + buf +} + +/// Get the current cursor state. +#[uniffi::export] +pub fn get_cursor(conn_id: String, terminal_id: String) -> CursorState { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.get_cursor().into()) + .unwrap_or(CursorState { + col: 0, + row: 0, + shape: CursorShape::Block, + visible: true, + }) +} + +/// Send text input to a terminal. Synchronous: only enqueues a WS message. +#[uniffi::export] +pub fn send_text(conn_id: String, terminal_id: String, text: String) { + ConnectionManager::get().send_ws_message(&conn_id, WsClientMessage::SendText { terminal_id, text }); +} + +/// Resize a terminal (local grid + WS resize message). +#[uniffi::export] +pub fn resize_terminal(conn_id: String, terminal_id: String, cols: u16, rows: u16) { + ConnectionManager::get().resize_terminal(&conn_id, &terminal_id, cols, rows); +} + +/// Resize only the local alacritty grid (no WS message). Used when adapting to +/// the server's terminal size. +#[uniffi::export] +pub fn resize_local(conn_id: String, terminal_id: String, cols: u16, rows: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.resize(cols, rows); + }); +} + +// ── Scrolling ─────────────────────────────────────────────────────── + +/// Scroll the terminal display (positive = up, negative = down). +#[uniffi::export] +pub fn scroll(conn_id: String, terminal_id: String, delta: i32) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.scroll(delta); + }); +} + +/// Get scroll info: total lines, visible lines, display offset. +#[uniffi::export] +pub fn get_scroll_info(conn_id: String, terminal_id: String) -> ScrollInfo { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + let (total, visible, offset) = holder.scroll_info(); + ScrollInfo { + total_lines: total as u32, + visible_lines: visible as u32, + display_offset: offset as u32, + } + }) + .unwrap_or(ScrollInfo { + total_lines: 0, + visible_lines: 0, + display_offset: 0, + }) +} + +// ── Selection ─────────────────────────────────────────────────────── + +/// Start a character-level selection at col/row. +#[uniffi::export] +pub fn start_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.start_selection(col as usize, row as usize); + }); +} + +/// Start a word (semantic) selection at col/row. +#[uniffi::export] +pub fn start_word_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.start_word_selection(col as usize, row as usize); + }); +} + +/// Extend the current selection to col/row. +#[uniffi::export] +pub fn update_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.update_selection(col as usize, row as usize); + }); +} + +/// Clear the current selection. +#[uniffi::export] +pub fn clear_selection(conn_id: String, terminal_id: String) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.clear_selection(); + }); +} + +/// Get the selected text, if any. +#[uniffi::export] +pub fn get_selected_text(conn_id: String, terminal_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.get_selected_text()) + .flatten() +} + +/// Get selection bounds for rendering. +#[uniffi::export] +pub fn get_selection_bounds(conn_id: String, terminal_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder + .selection_bounds() + .map(|((sc, sr), (ec, er))| SelectionBounds { + start_col: sc as u16, + start_row: sr, + end_col: ec as u16, + end_row: er, + }) + }) + .flatten() +} + +// ── State queries ─────────────────────────────────────────────────── + +/// Get all projects from the cached remote state. +#[uniffi::export] +pub fn get_projects(conn_id: String) -> Vec { + crate::api::state::get_projects(conn_id) + .into_iter() + .map(Into::into) + .collect() +} + +/// Get the focused project ID from the cached remote state. +#[uniffi::export] +pub fn get_focused_project_id(conn_id: String) -> Option { + crate::api::state::get_focused_project_id(conn_id) +} + +/// Get folders from the cached remote state. +#[uniffi::export] +pub fn get_folders(conn_id: String) -> Vec { + crate::api::state::get_folders(conn_id) + .into_iter() + .map(Into::into) + .collect() +} + +/// Get the project order from the cached remote state. +#[uniffi::export] +pub fn get_project_order(conn_id: String) -> Vec { + crate::api::state::get_project_order(conn_id) +} + +/// Get fullscreen terminal info. +#[uniffi::export] +pub fn get_fullscreen_terminal(conn_id: String) -> Option { + crate::api::state::get_fullscreen_terminal(conn_id).map(Into::into) +} + +/// Get layout JSON for a project. +#[uniffi::export] +pub fn get_project_layout_json(conn_id: String, project_id: String) -> Option { + let mgr = ConnectionManager::get(); + let state = mgr.get_state(&conn_id)?; + let project = state.projects.iter().find(|p| p.id == project_id)?; + let layout = project.layout.as_ref()?; + serde_json::to_string(layout).ok() +} + +/// Check if a terminal has unprocessed output (dirty flag). +#[uniffi::export] +pub fn is_dirty(conn_id: String, terminal_id: String) -> bool { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.is_dirty()) + .unwrap_or(false) +} + +/// Get all terminal IDs from the cached remote state (flat list). +#[uniffi::export] +pub fn get_all_terminal_ids(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + match mgr.get_state(&conn_id) { + Some(state) => collect_state_terminal_ids(&state), + None => Vec::new(), + } +} + +/// Send a special key (e.g. `"Enter"`, `"CtrlC"`, `"ArrowUp"`) to a terminal. +/// +/// The body is synchronous (it only enqueues a WS message), so this is a sync +/// uniffi fn. The error path mirrors the frb version: an unknown key name +/// yields an error. +#[uniffi::export] +pub fn send_special_key( + conn_id: String, + terminal_id: String, + key: String, +) -> Result<(), MobileFfiError> { + let special_key: SpecialKey = serde_json::from_value(serde_json::Value::String(key.clone())) + .map_err(|_| MobileFfiError::Action { + message: format!("Unknown special key: {key}"), + })?; + let text = String::from_utf8_lossy(special_key.to_bytes()).to_string(); + ConnectionManager::get().send_ws_message(&conn_id, WsClientMessage::SendText { terminal_id, text }); + Ok(()) +} + +// ── Error type for async/fallible exports ─────────────────────────── + +/// Error returned by fallible FFI functions. uniffi maps this to a thrown +/// error / rejected Promise on the RN side. +#[derive(Debug, uniffi::Error)] +pub enum MobileFfiError { + /// A remote action (HTTP POST /v1/actions) or input validation failed. + Action { message: String }, +} + +impl std::fmt::Display for MobileFfiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MobileFfiError::Action { message } => write!(f, "{message}"), + } + } +} + +impl std::error::Error for MobileFfiError {} + +impl From for MobileFfiError { + fn from(e: anyhow::Error) -> Self { + MobileFfiError::Action { + message: e.to_string(), + } + } +} + +// ── Terminal actions (async — await reqwest) ──────────────────────── + +/// Create a new terminal in the given project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn create_terminal(conn_id: String, project_id: String) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::CreateTerminal { project_id }) + .await?; + Ok(()) +} + +/// Close a terminal in the given project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn close_terminal( + conn_id: String, + project_id: String, + terminal_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::CloseTerminal { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Close multiple terminals in a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn close_terminals( + conn_id: String, + project_id: String, + terminal_ids: Vec, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::CloseTerminals { + project_id, + terminal_ids, + }, + ) + .await?; + Ok(()) +} + +/// Rename a terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn rename_terminal( + conn_id: String, + project_id: String, + terminal_id: String, + name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::RenameTerminal { + project_id, + terminal_id, + name, + }, + ) + .await?; + Ok(()) +} + +/// Focus a terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn focus_terminal( + conn_id: String, + project_id: String, + terminal_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::FocusTerminal { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Toggle minimized state of a terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn toggle_minimized( + conn_id: String, + project_id: String, + terminal_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::ToggleMinimized { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Set/clear fullscreen terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_fullscreen( + conn_id: String, + project_id: String, + terminal_id: Option, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetFullscreen { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Split a terminal pane. `direction` is "vertical" or "horizontal". +#[uniffi::export(async_runtime = "tokio")] +pub async fn split_terminal( + conn_id: String, + project_id: String, + path: Vec, + direction: String, +) -> Result<(), MobileFfiError> { + let dir = match direction.as_str() { + "vertical" => SplitDirection::Vertical, + _ => SplitDirection::Horizontal, + }; + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SplitTerminal { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + direction: dir, + }, + ) + .await?; + Ok(()) +} + +/// Run a command in a terminal (presses Enter automatically). +#[uniffi::export(async_runtime = "tokio")] +pub async fn run_command( + conn_id: String, + terminal_id: String, + command: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::RunCommand { + terminal_id, + command, + }, + ) + .await?; + Ok(()) +} + +/// Read terminal content as text. +#[uniffi::export(async_runtime = "tokio")] +pub async fn read_content(conn_id: String, terminal_id: String) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::ReadContent { terminal_id }) + .await?) +} + +// ── Git actions (async) ───────────────────────────────────────────── + +/// Get detailed git status for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_status(conn_id: String, project_id: String) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::GitStatus { project_id }) + .await?) +} + +/// Get git diff summary for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_diff_summary( + conn_id: String, + project_id: String, +) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::GitDiffSummary { project_id }) + .await?) +} + +/// Get git diff for a project. `mode` is "working_tree" or "staged". +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_diff( + conn_id: String, + project_id: String, + mode: String, +) -> Result { + let diff_mode = match mode.as_str() { + "staged" => DiffMode::Staged, + _ => DiffMode::WorkingTree, + }; + Ok(ConnectionManager::get() + .send_action_with_response( + &conn_id, + ActionRequest::GitDiff { + project_id, + mode: diff_mode, + ignore_whitespace: false, + }, + ) + .await?) +} + +/// Get git branches for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_branches(conn_id: String, project_id: String) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::GitBranches { project_id }) + .await?) +} + +/// Get file contents from git (working tree or staged). +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_file_contents( + conn_id: String, + project_id: String, + file_path: String, + mode: String, +) -> Result { + let diff_mode = match mode.as_str() { + "staged" => DiffMode::Staged, + _ => DiffMode::WorkingTree, + }; + Ok(ConnectionManager::get() + .send_action_with_response( + &conn_id, + ActionRequest::GitFileContents { + project_id, + file_path, + mode: diff_mode, + }, + ) + .await?) +} + +// ── Service actions (async) ───────────────────────────────────────── + +/// Start a service. +#[uniffi::export(async_runtime = "tokio")] +pub async fn start_service( + conn_id: String, + project_id: String, + service_name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::StartService { + project_id, + service_name, + }, + ) + .await?; + Ok(()) +} + +/// Stop a service. +#[uniffi::export(async_runtime = "tokio")] +pub async fn stop_service( + conn_id: String, + project_id: String, + service_name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::StopService { + project_id, + service_name, + }, + ) + .await?; + Ok(()) +} + +/// Restart a service. +#[uniffi::export(async_runtime = "tokio")] +pub async fn restart_service( + conn_id: String, + project_id: String, + service_name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::RestartService { + project_id, + service_name, + }, + ) + .await?; + Ok(()) +} + +/// Start all services in a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn start_all_services( + conn_id: String, + project_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::StartAllServices { project_id }) + .await?; + Ok(()) +} + +/// Stop all services in a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn stop_all_services(conn_id: String, project_id: String) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::StopAllServices { project_id }) + .await?; + Ok(()) +} + +/// Reload services config for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn reload_services(conn_id: String, project_id: String) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::ReloadServices { project_id }) + .await?; + Ok(()) +} + +// ── Project management (async) ────────────────────────────────────── + +/// Add a new project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn add_project( + conn_id: String, + name: String, + path: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::AddProject { name, path }) + .await?; + Ok(()) +} + +/// Set project color (named color, e.g. "blue"; unknown → default). +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_project_color( + conn_id: String, + project_id: String, + color: String, +) -> Result<(), MobileFfiError> { + let folder_color: okena_core::theme::FolderColor = + serde_json::from_value(serde_json::Value::String(color)).unwrap_or_default(); + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetProjectColor { + project_id, + color: folder_color, + }, + ) + .await?; + Ok(()) +} + +/// Set folder color (named color; unknown → default). +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_folder_color( + conn_id: String, + folder_id: String, + color: String, +) -> Result<(), MobileFfiError> { + let folder_color: okena_core::theme::FolderColor = + serde_json::from_value(serde_json::Value::String(color)).unwrap_or_default(); + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetFolderColor { + folder_id, + color: folder_color, + }, + ) + .await?; + Ok(()) +} + +/// Reorder a project within a folder. +#[uniffi::export(async_runtime = "tokio")] +pub async fn reorder_project_in_folder( + conn_id: String, + folder_id: String, + project_id: String, + new_index: u32, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::ReorderProjectInFolder { + folder_id, + project_id, + new_index: new_index as usize, + }, + ) + .await?; + Ok(()) +} + +// ── Layout actions (async) ────────────────────────────────────────── + +/// Update split sizes for a split pane. +#[uniffi::export(async_runtime = "tokio")] +pub async fn update_split_sizes( + conn_id: String, + project_id: String, + path: Vec, + sizes: Vec, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::UpdateSplitSizes { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + sizes, + }, + ) + .await?; + Ok(()) +} + +/// Add a new tab to a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn add_tab( + conn_id: String, + project_id: String, + path: Vec, + in_group: bool, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::AddTab { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + in_group, + }, + ) + .await?; + Ok(()) +} + +/// Set the active tab in a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_active_tab( + conn_id: String, + project_id: String, + path: Vec, + index: u32, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetActiveTab { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + index: index as usize, + }, + ) + .await?; + Ok(()) +} + +/// Move a tab within a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn move_tab( + conn_id: String, + project_id: String, + path: Vec, + from_index: u32, + to_index: u32, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::MoveTab { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + from_index: from_index as usize, + to_index: to_index as usize, + }, + ) + .await?; + Ok(()) +} + +/// Move a terminal into a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn move_terminal_to_tab_group( + conn_id: String, + project_id: String, + terminal_id: String, + target_path: Vec, + position: Option, + target_project_id: Option, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::MoveTerminalToTabGroup { + project_id, + terminal_id, + target_path: target_path.into_iter().map(|v| v as usize).collect(), + position: position.map(|p| p as usize), + target_project_id, + }, + ) + .await?; + Ok(()) +} + +/// Move a pane to a drop zone relative to another terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn move_pane_to( + conn_id: String, + project_id: String, + terminal_id: String, + target_project_id: String, + target_terminal_id: String, + zone: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::MovePaneTo { + project_id, + terminal_id, + target_project_id, + target_terminal_id, + zone, + }, + ) + .await?; + Ok(()) +} diff --git a/crates/okena-mobile-ffi/src/types.rs b/crates/okena-mobile-ffi/src/types.rs new file mode 100644 index 00000000..89cb9e78 --- /dev/null +++ b/crates/okena-mobile-ffi/src/types.rs @@ -0,0 +1,242 @@ +//! uniffi `Record` / `Enum` types mirroring the data the FFI returns. +//! +//! uniffi derive macros are kept off the internal `crate::api` data structs +//! (which `TerminalHolder` and the state accessors produce), so we define the +//! uniffi-facing mirrors here and convert from the internal ones via `From`. +//! The shapes match 1:1 so the conversions are mechanical. + +use std::collections::HashMap; + +use crate::api::{ + connection::ConnectionStatus as NativeConnectionStatus, + state::{ + FolderInfo as NativeFolderInfo, FullscreenInfo as NativeFullscreenInfo, + ProjectInfo as NativeProjectInfo, ServiceInfo as NativeServiceInfo, + }, + terminal::{ + CellData as NativeCellData, CursorShape as NativeCursorShape, + CursorState as NativeCursorState, ScrollInfo as NativeScrollInfo, + SelectionBounds as NativeSelectionBounds, + }, +}; + +/// Connection status surfaced to the RN layer. +/// +/// Mirrors `crate::api::connection::ConnectionStatus` (which itself collapses +/// core's `Reconnecting { attempt }` into `Connecting`). +#[derive(Debug, Clone, uniffi::Enum)] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Pairing, + Error { message: String }, +} + +impl From for ConnectionStatus { + fn from(s: NativeConnectionStatus) -> Self { + match s { + NativeConnectionStatus::Disconnected => ConnectionStatus::Disconnected, + NativeConnectionStatus::Connecting => ConnectionStatus::Connecting, + NativeConnectionStatus::Connected => ConnectionStatus::Connected, + NativeConnectionStatus::Pairing => ConnectionStatus::Pairing, + NativeConnectionStatus::Error { message } => ConnectionStatus::Error { message }, + } + } +} + +/// A single terminal grid cell (flat, FFI-friendly). +#[derive(Debug, Clone, uniffi::Record)] +pub struct CellData { + /// The character in this cell (empty string for wide-char spacers). + pub character: String, + /// Foreground color as ARGB packed u32. + pub fg: u32, + /// Background color as ARGB packed u32. + pub bg: u32, + /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). + pub flags: u8, +} + +impl From for CellData { + fn from(c: NativeCellData) -> Self { + CellData { + character: c.character, + fg: c.fg, + bg: c.bg, + flags: c.flags, + } + } +} + +/// Cursor shape variants. +#[derive(Debug, Clone, uniffi::Enum)] +pub enum CursorShape { + Block, + Underline, + Beam, +} + +impl From for CursorShape { + fn from(s: NativeCursorShape) -> Self { + match s { + NativeCursorShape::Block => CursorShape::Block, + NativeCursorShape::Underline => CursorShape::Underline, + NativeCursorShape::Beam => CursorShape::Beam, + } + } +} + +/// Cursor state for rendering. +#[derive(Debug, Clone, uniffi::Record)] +pub struct CursorState { + pub col: u16, + pub row: u16, + pub shape: CursorShape, + pub visible: bool, +} + +impl From for CursorState { + fn from(c: NativeCursorState) -> Self { + CursorState { + col: c.col, + row: c.row, + shape: c.shape.into(), + visible: c.visible, + } + } +} + +/// Scroll info: total/visible line counts and the current display offset. +#[derive(Debug, Clone, uniffi::Record)] +pub struct ScrollInfo { + pub total_lines: u32, + pub visible_lines: u32, + pub display_offset: u32, +} + +impl From for ScrollInfo { + fn from(s: NativeScrollInfo) -> Self { + ScrollInfo { + total_lines: s.total_lines, + visible_lines: s.visible_lines, + display_offset: s.display_offset, + } + } +} + +/// Selection bounds (rows are buffer-relative, adjusted for display offset). +#[derive(Debug, Clone, uniffi::Record)] +pub struct SelectionBounds { + pub start_col: u16, + pub start_row: i32, + pub end_col: u16, + pub end_row: i32, +} + +impl From for SelectionBounds { + fn from(s: NativeSelectionBounds) -> Self { + SelectionBounds { + start_col: s.start_col, + start_row: s.start_row, + end_col: s.end_col, + end_row: s.end_row, + } + } +} + +/// Service entry inside a project. +#[derive(Debug, Clone, uniffi::Record)] +pub struct ServiceInfo { + pub name: String, + pub status: String, + pub terminal_id: Option, + pub ports: Vec, + pub exit_code: Option, + pub kind: String, + pub is_extra: bool, +} + +impl From for ServiceInfo { + fn from(s: NativeServiceInfo) -> Self { + ServiceInfo { + name: s.name, + status: s.status, + terminal_id: s.terminal_id, + ports: s.ports, + exit_code: s.exit_code, + kind: s.kind, + is_extra: s.is_extra, + } + } +} + +/// Flat, FFI-friendly project info. +#[derive(Debug, Clone, uniffi::Record)] +pub struct ProjectInfo { + pub id: String, + pub name: String, + pub path: String, + pub show_in_overview: bool, + pub terminal_ids: Vec, + pub terminal_names: HashMap, + pub git_branch: Option, + pub git_lines_added: u32, + pub git_lines_removed: u32, + pub services: Vec, + pub folder_color: String, +} + +impl From for ProjectInfo { + fn from(p: NativeProjectInfo) -> Self { + ProjectInfo { + id: p.id, + name: p.name, + path: p.path, + show_in_overview: p.show_in_overview, + terminal_ids: p.terminal_ids, + terminal_names: p.terminal_names, + git_branch: p.git_branch, + git_lines_added: p.git_lines_added, + git_lines_removed: p.git_lines_removed, + services: p.services.into_iter().map(Into::into).collect(), + folder_color: p.folder_color, + } + } +} + +/// Folder grouping projects. +#[derive(Debug, Clone, uniffi::Record)] +pub struct FolderInfo { + pub id: String, + pub name: String, + pub project_ids: Vec, + pub folder_color: String, +} + +impl From for FolderInfo { + fn from(f: NativeFolderInfo) -> Self { + FolderInfo { + id: f.id, + name: f.name, + project_ids: f.project_ids, + folder_color: f.folder_color, + } + } +} + +/// The currently fullscreened terminal, if any. +#[derive(Debug, Clone, uniffi::Record)] +pub struct FullscreenInfo { + pub project_id: String, + pub terminal_id: String, +} + +impl From for FullscreenInfo { + fn from(f: NativeFullscreenInfo) -> Self { + FullscreenInfo { + project_id: f.project_id, + terminal_id: f.terminal_id, + } + } +} diff --git a/crates/okena-remote-client/src/connection.rs b/crates/okena-remote-client/src/connection.rs index fbae8475..42d0c98c 100644 --- a/crates/okena-remote-client/src/connection.rs +++ b/crates/okena-remote-client/src/connection.rs @@ -37,6 +37,8 @@ impl ConnectionHandler for DesktopConnectionHandler { _terminal_id: &str, prefixed_id: &str, ws_sender: async_channel::Sender, + cols: u16, + rows: u16, ) { let mut terminals = self.terminals.lock(); // Skip if terminal already exists — on reconnect the server re-sends @@ -50,9 +52,14 @@ impl ConnectionHandler for DesktopConnectionHandler { ws_tx: ws_sender, connection_id: connection_id.to_string(), }); + let size = if cols > 0 && rows > 0 { + TerminalSize { cols, rows, ..TerminalSize::default() } + } else { + TerminalSize::default() + }; let terminal = Arc::new(Terminal::new( prefixed_id.to_string(), - TerminalSize::default(), + size, transport, String::new(), )); diff --git a/crates/okena-terminal/src/terminal/resize.rs b/crates/okena-terminal/src/terminal/resize.rs index 6205c59e..69628e62 100644 --- a/crates/okena-terminal/src/terminal/resize.rs +++ b/crates/okena-terminal/src/terminal/resize.rs @@ -20,6 +20,11 @@ impl Terminal { pub fn resize(&self, new_size: TerminalSize) { let debounce_ms = self.transport.resize_debounce_ms(); + // Clamp to at least 1 col/row - alacritty_terminal panics on zero dimensions + let cols = new_size.cols.max(1); + let rows = new_size.rows.max(1); + let new_size = TerminalSize { cols, rows, ..new_size }; + // Always update local size immediately (optimistic UI) { let mut rs = self.resize_state.lock(); diff --git a/crates/okena-ui/src/text_utils.rs b/crates/okena-ui/src/text_utils.rs index 4e0061b3..db1ec8a5 100644 --- a/crates/okena-ui/src/text_utils.rs +++ b/crates/okena-ui/src/text_utils.rs @@ -5,28 +5,59 @@ pub fn is_word_char(c: char) -> bool { c.is_alphanumeric() || c == '_' } -/// Find the word boundaries (start, end) around a given column position. -pub fn find_word_boundaries(text: &str, col: usize) -> (usize, usize) { - let chars: Vec = text.chars().collect(); - if chars.is_empty() { +/// Find the word boundaries (start, end) around a given byte offset. +/// +/// Both `byte_col` and the returned `(start, end)` are **byte offsets** into `text`, +/// guaranteed to land on UTF-8 char boundaries. +pub fn find_word_boundaries(text: &str, byte_col: usize) -> (usize, usize) { + if text.is_empty() { return (0, 0); } - let col = col.min(chars.len().saturating_sub(1)); - // Scan backwards for start + // Clamp to a valid char boundary at or before byte_col + let byte_col = byte_col.min(text.len()); + let col = if text.is_char_boundary(byte_col) { + byte_col + } else { + // Walk backwards to find a valid char boundary + let mut b = byte_col; + while b > 0 && !text.is_char_boundary(b) { + b -= 1; + } + b + }; + + // Get the char at `col` (if col == text.len(), there is no char) + let cur_char = text[col..].chars().next(); + let on_word = cur_char.is_some_and(is_word_char); + + // Scan backwards for start (byte offset) let mut start = col; - while start > 0 && is_word_char(chars[start - 1]) { - start -= 1; - } - // If cursor is on a non-word char, don't extend backwards - if !is_word_char(chars[col]) { - start = col; + if on_word { + while start > 0 { + // Find the previous char boundary + let mut prev = start - 1; + while prev > 0 && !text.is_char_boundary(prev) { + prev -= 1; + } + match text[prev..].chars().next() { + Some(prev_char) if is_word_char(prev_char) => start = prev, + _ => break, + } + } } - // Scan forwards for end + // Scan forwards for end (byte offset) let mut end = col; - while end < chars.len() && is_word_char(chars[end]) { - end += 1; + while end < text.len() { + let Some(next_char) = text[end..].chars().next() else { + break; + }; + if is_word_char(next_char) { + end += next_char.len_utf8(); + } else { + break; + } } (start, end) diff --git a/crates/okena-views-terminal/src/layout/tabs/mod.rs b/crates/okena-views-terminal/src/layout/tabs/mod.rs index 1f48884d..b7bc0cf6 100644 --- a/crates/okena-views-terminal/src/layout/tabs/mod.rs +++ b/crates/okena-views-terminal/src/layout/tabs/mod.rs @@ -261,6 +261,18 @@ impl LayoutContainer { self.deregister_child_resize_viewers_except(&visible_paths, cx); self.child_containers.retain(|path, _| valid_paths.contains(path)); + // Deregister pane map entries for inactive tabs so stale entries + // don't interfere with spatial navigation + let mut path = self.layout_path.clone(); + let base_len = path.len(); + for i in 0..num_children { + if i != active_tab { + path.truncate(base_len); + path.push(i); + crate::layout::navigation::deregister_pane_bounds(self.window_id, &self.project_id, &path); + } + } + let container_bounds_ref = self.container_bounds_ref.clone(); v_flex() diff --git a/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs b/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs index 8d3e6691..ae0d5b86 100644 --- a/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs +++ b/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs @@ -3,17 +3,79 @@ use crate::ActionDispatch; use okena_terminal::input::{KeyEvent, KeyModifiers, key_to_bytes}; use crate::layout::navigation::{get_pane_map, PaneBounds, NavigationDirection}; +use okena_workspace::state::LayoutNode; use gpui::*; use super::TerminalPane; impl TerminalPane { + /// Try to switch to an adjacent tab within a Tabs node. + /// Returns true if a tab switch happened, false if at edge or not in a tab group. + fn try_switch_tab(&mut self, next: bool, cx: &mut Context) -> bool { + if self.layout_path.is_empty() { + return false; + } + + let parent_path = &self.layout_path[..self.layout_path.len() - 1]; + let current_tab_index = self.layout_path[self.layout_path.len() - 1]; + + let tab_count = { + let ws = self.workspace.read(cx); + ws.project(&self.project_id).and_then(|p| { + p.layout.as_ref().and_then(|layout| { + layout.get_at_path(parent_path).and_then(|node| match node { + LayoutNode::Tabs { children, .. } => Some(children.len()), + _ => None, + }) + }) + }) + }; + + let num_tabs = match tab_count.filter(|&n| n > 1) { + Some(n) => n, + None => return false, + }; + + let at_edge = if next { + current_tab_index == num_tabs - 1 + } else { + current_tab_index == 0 + }; + + if at_edge { + return false; + } + + let new_tab = if next { current_tab_index + 1 } else { current_tab_index - 1 }; + let project_id = self.project_id.clone(); + let mut new_layout_path = parent_path.to_vec(); + new_layout_path.push(new_tab); + + let workspace = self.workspace.clone(); + self.focus_manager.update(cx, |fm, cx| { + workspace.update(cx, |ws, cx| { + ws.set_active_tab(&project_id, &new_layout_path[..new_layout_path.len() - 1], new_tab, cx); + ws.set_focused_terminal(fm, project_id, new_layout_path, cx); + }); + cx.notify(); + }); + true + } + pub(super) fn handle_navigation( &mut self, direction: NavigationDirection, window: &mut Window, cx: &mut Context, ) { + // Left/Right: try switching tabs first, fall through to spatial nav at edges + if matches!(direction, NavigationDirection::Left | NavigationDirection::Right) { + let next = matches!(direction, NavigationDirection::Right); + if self.try_switch_tab(next, cx) { + return; + } + } + let pane_map = get_pane_map(self.window_id); let source = match pane_map.find_pane(&self.project_id, &self.layout_path) { @@ -32,6 +94,10 @@ impl TerminalPane { window: &mut Window, cx: &mut Context, ) { + if self.try_switch_tab(next, cx) { + return; + } + let pane_map = get_pane_map(self.window_id); let source = match pane_map.find_pane(&self.project_id, &self.layout_path) { diff --git a/crates/okena-workspace/src/remote_apply.rs b/crates/okena-workspace/src/remote_apply.rs index aa2b6c37..5216204d 100644 --- a/crates/okena-workspace/src/remote_apply.rs +++ b/crates/okena-workspace/src/remote_apply.rs @@ -398,6 +398,8 @@ mod tests { terminal_id: Some(id.to_string()), minimized: false, detached: false, + cols: None, + rows: None, } } diff --git a/crates/okena-workspace/src/state.rs b/crates/okena-workspace/src/state.rs index 660f1781..e3134f04 100644 --- a/crates/okena-workspace/src/state.rs +++ b/crates/okena-workspace/src/state.rs @@ -871,6 +871,7 @@ impl Workspace { } } + #[cfg(test)] mod workspace_tests { use crate::state::{ diff --git a/docs/mobile-status.md b/docs/mobile-status.md index a00d2768..285468e8 100644 --- a/docs/mobile-status.md +++ b/docs/mobile-status.md @@ -2,83 +2,74 @@ ## Overview -Flutter + Rust FFI mobile app (Android/iOS) for controlling a remote Okena desktop instance. Uses `alacritty_terminal` in Rust for ANSI processing — identical terminal emulation as the desktop app. Communicates with the desktop's remote server via REST + WebSocket. +React Native mobile app (Android/iOS) for controlling a remote Okena desktop instance. The Rust +core (`alacritty_terminal` for ANSI processing — identical terminal emulation as the desktop) is +reused below an FFI-agnostic seam and exposed to TypeScript via **uniffi** (a JSI TurboModule +generated by `uniffi-bindgen-react-native`). The terminal grid is painted natively with +`react-native-skia` — **no `xterm.js`, no WebView**. Communicates with the desktop's remote server +over REST + WebSocket. + +> Migration note: this app replaced an earlier Flutter + `flutter_rust_bridge` client. The binding +> generator and UI were swapped (`flutter_rust_bridge` → uniffi, Dart widgets → RN components); the +> protocol, TLS, reconnect and ANSI emulation in `okena-core` were reused unchanged. See +> [`mobile/RN_MIGRATION.md`](../mobile/RN_MIGRATION.md) for the full plan and rationale. ## Architecture ``` -┌──────────────────────────────────────────────────────────────┐ -│ Flutter Mobile App │ -│ │ -│ ┌──────────────────┐ ┌────────────────────────────────┐ │ -│ │ Dart UI │ │ Rust (via flutter_rust_bridge) │ │ -│ │ │ │ │ │ -│ │ Screens │ │ ConnectionManager (OnceLock) │ │ -│ │ Providers │ │ ├─ RemoteClient │ │ -│ │ Widgets │ │ ├─ MobileConnectionHandler │ │ -│ │ │ │ └─ TerminalHolder per terminal │ │ -│ │ │ │ └─ alacritty_terminal::Term │ │ -│ └──────────────────┘ └────────────────────────────────┘ │ -│ │ │ │ -│ flutter_rust_bridge (FFI) │ │ -└──────────┼──────────────────────────┼─────────────────────────┘ - │ HTTP + WebSocket - ▼ ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Okena Desktop (server) │ -│ │ -│ Remote Server (src/remote/) │ +┌─────────────────────────────────────────────────────────────┐ +│ React Native app (TypeScript) mobile/rn/ │ +│ │ +│ Screens / navigation / zustand stores │ +│ KeyToolbar, ProjectDrawer, LayoutRenderer (RN components) │ +│ TerminalView → react-native-skia (native GPU) │ +│ │ reads packed cell buffer, 3-pass paint │ +│ ▼ │ +│ TS bindings (generated by uniffi-bindgen-react-native, JSI) │ +└────────┼────────────────────────────────────────────────────┘ + │ JSI (in-process: sync host fns + Promises) +┌────────▼────────────────────────────────────────────────────┐ +│ crates/okena-mobile-ffi (uniffi-annotated, self-contained) │ +│ ConnectionManager (OnceLock + tokio runtime) │ +│ ├─ RemoteClient (okena-core) │ +│ ├─ MobileConnectionHandler │ +│ └─ TerminalHolder per terminal → alacritty_terminal::Term │ +└────────┼─────────────────────────────────────────────────────┘ + │ HTTP + WebSocket + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Okena Desktop — remote server (src/remote/) │ │ ├── POST /v1/pair → code → bearer token │ │ ├── GET /v1/state → workspace snapshot (JSON) │ -│ ├── POST /v1/actions → send_text, split, close, resize │ -│ └── WS /v1/stream → binary PTY frames + state events │ -└──────────────────────────────────────────────────────────────┘ +│ ├── POST /v1/actions → send_text, split, close, resize… │ +│ └── WS /v1/stream → binary PTY frames + state events │ +└─────────────────────────────────────────────────────────────┘ ``` ## Repository Structure ``` -Cargo.toml ← workspace: members = [".", "mobile/native", "crates/okena-core"] +Cargo.toml ← workspace: members include "crates/okena-mobile-ffi" src/ ← desktop app crates/okena-core/ ← shared crate (API types, client state machine, theme colors) +crates/okena-mobile-ffi/ ← uniffi FFI crate (self-contained — no flutter tooling) + Cargo.toml ← crate-type = ["cdylib","staticlib","lib"]; uniffi 0.29 + src/ + lib.rs ← #[uniffi::export] surface (~60 fns) + packed cell buffer + types.rs ← uniffi Record/Enum mirrors (+ From conversions) + api/ ← plain state-extraction structs + accessors + client/ ← ConnectionManager / MobileConnectionHandler / TerminalHolder mobile/ - android/, ios/ ← platform shells - lib/ - main.dart ← App entry, MultiProvider setup, AppRouter - src/ - models/ - saved_server.dart ← SavedServer data class, JSON persistence - layout_node.dart ← Sealed classes: TerminalNode, SplitNode, TabsNode - providers/ - connection_provider.dart ← Saved servers CRUD, connection lifecycle, status polling - workspace_provider.dart ← Project list polling, focused project tracking - screens/ - server_list_screen.dart ← Server list + add bottom sheet - pairing_screen.dart ← Connect → pair flow, code input - workspace_screen.dart ← AppBar + drawer + layout + key toolbar - theme/ - app_theme.dart ← JetBrainsMono font, Catppuccin dark colors - widgets/ - key_toolbar.dart ← ESC, TAB, CTRL/ALT (sticky), arrows - layout_renderer.dart ← Recursive layout tree → TerminalView/Flex/Tabs - project_drawer.dart ← Project list drawer with disconnect button - status_indicator.dart ← Colored dot + label - terminal_painter.dart ← CustomPainter: bg rects → text → cursor - terminal_view.dart ← Terminal widget: resize, input, 30fps polling - rust/api/ ← generated Dart bindings (do not edit) + RN_MIGRATION.md ← migration plan (uniffi + native Skia rendering) fonts/ ← JetBrainsMono (Regular, Bold, Italic, BoldItalic) - native/ ← Rust FFI crate - Cargo.toml + rn/ ← React Native app (see mobile/rn/README.md) src/ - lib.rs ← pub mod api; pub mod client; - api/ - connection.rs ← connect, pair, disconnect, connection_status - terminal.rs ← get_visible_cells, get_cursor, send_text, resize_terminal - state.rs ← get_projects, is_dirty, send_special_key, get_project_layout_json - client/ - manager.rs ← ConnectionManager singleton (OnceLock + tokio runtime) - handler.rs ← MobileConnectionHandler (impl ConnectionHandler) - terminal_holder.rs ← TerminalHolder (alacritty_terminal::Term wrapper) + native/okena.ts ← OkenaNative binding contract + getOkenaNative() + native/cells.ts ← packed cell-buffer decoder (matches the Rust encoder) + state/ ← zustand stores (connection, workspace) — DI for tests + screens/ ← ServerList, Pairing, Workspace + components/ ← TerminalView (Skia), KeyToolbar, LayoutRenderer, ProjectDrawer… + models/ ← SavedServer, LayoutNode AST ``` ## Data Flow @@ -86,43 +77,36 @@ mobile/ ### PTY output (server → mobile screen) ``` -Remote PTY process - → PtyBroadcaster (server) - → WebSocket binary frame [proto=1][type=1][stream_id:u32][data...] - → RemoteClient WS reader task (okena-core) +Remote PTY → WS binary frame → RemoteClient WS reader (okena-core) → MobileConnectionHandler.on_terminal_output() - → TerminalHolder.process_output(data) ← alacritty ANSI processing - → dirty flag set - → Flutter polls is_dirty() every 33ms → get_visible_cells() via FFI - → TerminalPainter (CustomPainter) renders cell grid + → TerminalHolder.process_output(data) ← alacritty ANSI processing → dirty flag + → RN render loop (requestAnimationFrame, gated on isDirty()) + → getVisibleCellsPacked() → ArrayBuffer → decodeCells() → Skia 3-pass paint ``` +The hot path uses `get_visible_cells_packed` (a compact `cols,rows` header + 13-byte cells) instead +of marshalling thousands of records per frame across JSI — the key perf lever (Decision C in the +migration plan). The record form `get_visible_cells` is kept for non-hot callers. + ### Keyboard input (mobile → server) ``` -Soft keyboard / key toolbar tap - → Dart calls FFI send_text() or send_special_key() - → ConnectionManager.send_ws_message() - → WsClientMessage::SendText via WebSocket - → Server bridge → PtyManager.send_input() - → Remote PTY stdin +Soft keyboard / KeyToolbar tap → sendText() / sendSpecialKey() (JSI) + → ConnectionManager.send_ws_message() → WsClientMessage::SendText → WS → server PTY stdin ``` ### State sync (project list, layouts) ``` -WS "state_changed" event or initial connect - → RemoteClient fetches GET /v1/state - → Parses StateResponse, diffs against cached state - → Creates/removes TerminalHolders for added/removed terminals - → Auto-subscribes to new terminal streams - → state_cache updated in MobileConnection - → Flutter reads via FFI get_projects() +WS "state_changed" / initial connect → RemoteClient fetches GET /v1/state + → diff against cached state → create/remove TerminalHolders → state_cache updated + → RN stores poll getProjects()/getFolders()/getProjectLayoutJson() (1s) ``` ## Shared Core: okena-core -The `crates/okena-core/` crate contains all code shared between desktop and mobile: +`crates/okena-core/` contains everything shared between desktop and mobile (reused **unchanged** by +the RN client): | Module | Contents | |--------|----------| @@ -132,127 +116,64 @@ The `crates/okena-core/` crate contains all code shared between desktop and mobi | `client::state` | `diff_states()`, `collect_state_terminal_ids()` | | `theme::colors` | `ThemeColors`, `DARK_THEME`, `ansi_to_argb()` | | `keys` | `SpecialKey` enum with `to_bytes()` | -| `ws` | Binary PTY frame format helpers | -| `types` | `SplitDirection` | +| `types` | `SplitDirection`, `DiffMode`, `FolderColor` | -The `client` module is behind a `client` feature flag (adds tokio, reqwest, tokio-tungstenite, async-channel, futures). +The `client` feature pulls tokio, reqwest, tokio-tungstenite, async-channel, futures — all with the +**rustls** backends, so the NDK / iOS builds never cross-compile OpenSSL. -Desktop uses the same `RemoteClient` with `DesktopConnectionHandler` (creates `Terminal` objects in the GPUI `TerminalsRegistry`). Mobile uses `MobileConnectionHandler` (creates `TerminalHolder` objects in a shared `HashMap`). +Desktop uses the same `RemoteClient` with `DesktopConnectionHandler`; mobile uses +`MobileConnectionHandler` (creates `TerminalHolder` objects in a shared `HashMap`). ## Key Decisions -### Flutter + Rust FFI (not React Native + xterm.js) - -- Same terminal parser as desktop (alacritty_terminal) — no rendering divergence -- Shared Rust code via okena-core — real code reuse, not just type duplication -- CustomPainter for grid rendering — full control, no WebView overhead -- Higher build complexity (NDK cross-compilation) — acceptable tradeoff - -### NoopEventListener on mobile - -The server's `Term` already handles PtyWrite responses (cursor reports, DA sequences). If the mobile `Term` also forwarded these back via WebSocket, they'd be written to the PTY twice. So mobile uses a no-op listener. - -### Terminal ID namespacing - -Remote terminal IDs use the prefix `remote:{connection_id}:{terminal_id}` to avoid collisions. The ConnectionHandler receives both the raw `terminal_id` (for WS messages to the server) and the `prefixed_id` (for local storage keys). - -### ConnectionManager as OnceLock singleton - -Mobile doesn't have GPUI's entity system. A `static OnceLock` with a 2-thread tokio runtime provides the async backbone. All FFI functions access it via `ConnectionManager::get()`. - -### FFI ConnectionStatus simplification - -The FFI `ConnectionStatus` enum collapses `Reconnecting { attempt }` into `Connecting` — mobile UI doesn't need the attempt counter. - -### DARK_THEME as default palette - -Cell colors use `ThemeColors::DARK_THEME` for ANSI → ARGB conversion. Theme switching can be added later by passing a `ThemeColors` reference from the Flutter side. +- **React Native + Rust via uniffi (not RN + xterm.js).** Keep the desktop's exact terminal parser + (`alacritty_terminal`) and real code reuse via `okena-core`; render natively with + react-native-skia. The documented fallback if the ubrn build proves too painful is to drop Rust on + mobile and reuse the web client's TS protocol with a TS ANSI parser feeding the *same* Skia + renderer — `xterm.js` stays rejected. See `mobile/RN_MIGRATION.md` §5. +- **Self-contained FFI crate.** `okena-mobile-ffi` owns the `ConnectionManager` / `TerminalHolder` + engine outright (carried over from the retired `mobile/native`, frb attributes stripped). It + depends only on `okena-core` + uniffi — no Flutter tooling. +- **NoopEventListener on mobile.** The server's `Term` already handles PtyWrite responses; the mobile + `Term` uses a no-op listener so they aren't written back to the PTY twice. +- **Terminal ID namespacing.** Remote IDs are prefixed `remote:{connection_id}:{terminal_id}`. +- **ConnectionManager as `OnceLock` singleton** with a 2-thread tokio runtime — no GPUI entity system + on mobile. All FFI functions access it via `ConnectionManager::get()`. +- **FFI `ConnectionStatus`** collapses `Reconnecting { attempt }` into `Connecting`. +- **DARK_THEME** as the default ANSI → ARGB palette; server-driven theme sync is a later addition. + +## FFI Surface (`crates/okena-mobile-ffi/src/lib.rs`) + +~60 `#[uniffi::export]` functions. Cheap getters on the render hot path are **sync** JSI host +functions; anything that crosses the wire / awaits a server response is **async** (JS Promise, via +`#[uniffi::export(async_runtime = "tokio")]`). The full contract — and the TypeScript shape ubrn +emits — is mirrored in [`mobile/rn/src/native/okena.ts`](../mobile/rn/src/native/okena.ts). Highlights: + +| Function | Sync/async | Description | +|----------|------------|-------------| +| `init_app()` / `connect()` / `pair()` / `disconnect()` / `connection_status()` | mixed | Connection lifecycle | +| `get_visible_cells_packed(conn, term) → Vec` | sync | Packed grid buffer — the render hot path | +| `get_visible_cells` / `get_cursor` / `get_scroll_info` / selection getters | sync | Render/selection reads | +| `is_dirty(conn, term) → bool` | sync | Unread output since last fetch | +| `send_text` / `resize_terminal` / `resize_local` / `scroll` / `send_special_key` | sync | Input / viewport | +| `get_projects` / `get_folders` / `get_project_order` / `get_project_layout_json` / `get_fullscreen_terminal` | sync | Cached-state reads | +| terminal / git / service / project / layout actions (`create_terminal`, `git_diff`, `start_service`, `add_project`, `update_split_sizes`, `move_pane_to`, …) | async | POST `/v1/actions`, return `()`/JSON | ## Current State -### Done - -| Layer | What | Status | -|-------|------|--------| -| **Shared core** | okena-core with API types, RemoteClient state machine, ThemeColors | Complete | -| **Desktop client** | `src/remote_client/` — DesktopConnectionHandler, RemoteBackend, sidebar integration | Complete | -| **Desktop server** | All endpoints: health, pair, state, actions (including resize, create_terminal), WS stream | Complete | -| **Web client** | React SPA at `/v1/web/` — connect, pair, browse projects, render terminals (xterm.js) | Complete | -| **Mobile Rust core** | ConnectionManager, MobileConnectionHandler, TerminalHolder, all FFI functions wired to real networking | Complete | -| **Mobile Flutter UI** | Full UI: ServerListScreen, PairingScreen, WorkspaceScreen, project drawer, terminal rendering, key toolbar, layout rendering | Complete | -| **Mobile state management** | ConnectionProvider (saved servers, polling), WorkspaceProvider (project list, focus tracking) | Complete | -| **Terminal rendering** | CustomPainter (3-pass: bg, text, cursor), 30fps dirty polling, auto-resize with debounce | Complete | -| **Key toolbar** | ESC, TAB, CTRL/ALT sticky toggles, arrow keys | Complete | -| **Layout rendering** | Recursive split/tab layout from JSON, portrait-mode auto-vertical, tab switching | Complete | -| **Saved servers** | SharedPreferences persistence with JSON serialization | Complete | -| **Rust tests** | 8 mobile native + 23 okena-core unit tests | Passing | -| **Dart tests** | 22 unit tests (7 SavedServer, 7 LayoutNode, 8 terminal flags/colors) | Passing | - -### Not yet done (polish) - -| What | Description | -|------|-------------| -| **Gestures** | Text selection, pinch-to-zoom font size, scrollback (two-finger scroll) | -| **Auto-reconnect UI** | Visual feedback banner for reconnection attempts | -| **Long-press arrows** | Key repeat on long-press for arrow keys | -| **Theme sync** | Receive theme colors from server instead of hardcoded DARK_THEME | -| **F-keys** | F1–F12 in toolbar (swipe-up row) | -| **App icon & splash** | Custom launcher icon, branded splash screen | -| **On-device testing** | End-to-end test on physical Android device with real Okena server | - -## FFI Surface - -### connection.rs - -| Function | Sync | Description | -|----------|------|-------------| -| `init_app()` | init | Setup FRB + ConnectionManager | -| `connect(host, port) → String` | sync | Create connection, start health check, return conn_id | -| `pair(conn_id, code)` | async | Pair with code, start WS | -| `disconnect(conn_id)` | sync | Close WS, cleanup terminals | -| `connection_status(conn_id) → ConnectionStatus` | sync | Current status | - -### terminal.rs - -| Function | Sync | Description | -|----------|------|-------------| -| `get_visible_cells(conn_id, terminal_id) → Vec` | sync | Grid cells with ARGB colors + flags | -| `get_cursor(conn_id, terminal_id) → CursorState` | sync | Cursor position, shape, visibility | -| `send_text(conn_id, terminal_id, text)` | async | Send text input via WS | -| `resize_terminal(conn_id, terminal_id, cols, rows)` | async | Resize local grid + send WS resize | - -### state.rs - -| Function | Sync | Description | -|----------|------|-------------| -| `get_projects(conn_id) → Vec` | sync | Project list from cached state | -| `get_focused_project_id(conn_id) → Option` | sync | Server's focused project | -| `is_dirty(conn_id, terminal_id) → bool` | sync | Terminal has new output | -| `send_special_key(conn_id, terminal_id, key)` | async | Send named key (Enter, CtrlC, ArrowUp, ...) | -| `get_project_layout_json(conn_id, project_id) → Option` | sync | Layout tree as JSON | -| `get_all_terminal_ids(conn_id) → Vec` | sync | Flat list of all terminal IDs | - -## Next Steps (Polish) - -### 1. Gestures & interaction - -- Pinch-to-zoom font size (adjust `_fontSize` → recompute grid → `resize_terminal()`) -- Long-press arrow keys for key repeat -- Text selection + copy (long-press → drag to select → clipboard) -- Two-finger scroll for scrollback - -### 2. Visual polish - -- Auto-reconnect banner (visual feedback when connection drops and reconnects) -- App icon and splash screen -- Theme sync from server (receive `ThemeColors` via state, pass to `TerminalPainter`) -- F1–F12 keys in toolbar (swipe-up secondary row) - -### 3. On-device testing - -- End-to-end test: connect to real Okena server, pair, browse projects, type in terminal -- Verify performance (30fps rendering, resize latency) -- Test with large terminal output (build logs, `htop`) +| Layer | Status | +|-------|--------| +| **Shared core** (`okena-core`) | Complete — reused unchanged | +| **Desktop client + server** | Complete (all endpoints) | +| **Web client** | Complete (React SPA, xterm.js) | +| **Mobile FFI crate** (`okena-mobile-ffi`) | Complete — self-contained, ~60 uniffi fns + packed buffer; `cargo test -p okena-mobile-ffi` passing (11 tests) | +| **RN TS app** (`mobile/rn`) | Scaffold complete — screens, zustand stores (DI-testable), Skia `TerminalView`, packed-cell decoder; type-checks + lints | +| **ubrn binding generation** | Config present (`mobile/rn/ubrn.config.yaml`); cross-compile not yet run (needs NDK/Xcode) | +| **Native host projects** (`android/`, `ios/`) | Not generated yet — `react-native init` + merge, see `mobile/rn/README.md` | +| **Phase-0 spikes** (toolchain S1, Skia throughput S2) | Not yet run on-device | + +See `mobile/rn/README.md` for the exact device-side steps and `mobile/RN_MIGRATION.md` for the +phased plan. ## Networking @@ -268,19 +189,15 @@ The remote server binds to a configurable IP (default localhost). Mobile clients ## Build ```bash -# Rust only -cargo build -p okena_mobile_native -cargo test -p okena_mobile_native - -# Regenerate Dart bindings -cd mobile && flutter_rust_bridge_codegen generate +# Rust FFI crate only (runs anywhere — no mobile toolchain needed) +cargo build -p okena-mobile-ffi +cargo test -p okena-mobile-ffi -# Build APK -export ANDROID_HOME=~/android-sdk -export PATH="$HOME/flutter/bin:$HOME/.cargo/bin:$PATH" -cd mobile && flutter build apk --debug +# RN TypeScript checks +cd mobile/rn && npm ci && npm run typecheck && npm run lint ``` -**Critical notes:** -- `run_build_tool.sh` needs `$HOME/.cargo/bin` in PATH (Gradle daemon doesn't inherit it) -- Use `rustls-tls` (not `native-tls`) for all deps to avoid cross-compiling OpenSSL +The native app build (ubrn cross-compile → Android `.so` / iOS xcframework, `pod install`, +`react-native run-*`) requires the mobile toolchain and is documented step-by-step in +`mobile/rn/README.md`. Use `rustls-tls` (already selected by `okena-core`'s `client` feature) to +avoid cross-compiling OpenSSL for the NDK. diff --git a/mobile/.gitignore b/mobile/.gitignore deleted file mode 100644 index 3820a95c..00000000 --- a/mobile/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/mobile/.metadata b/mobile/.metadata deleted file mode 100644 index 6b591ae4..00000000 --- a/mobile/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "67323de285b00232883f53b84095eb72be97d35c" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - platform: linux - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/mobile/CLAUDE.md b/mobile/CLAUDE.md deleted file mode 100644 index 845edb1f..00000000 --- a/mobile/CLAUDE.md +++ /dev/null @@ -1,187 +0,0 @@ -# Mobile App — Flutter + Rust FFI - -Remote terminal client for Android/iOS. Connects to the Okena desktop server via REST + WebSocket. Uses `alacritty_terminal` in Rust for ANSI processing — identical terminal emulation as the desktop app. - -## Build Commands - -```bash -# Rust crate only -cargo build -p okena_mobile_native -cargo test -p okena_mobile_native - -# Regenerate Dart FFI bindings (after changing Rust api/ signatures) -cd mobile && flutter_rust_bridge_codegen generate - -# Build APK -export ANDROID_HOME=~/android-sdk -export PATH="$HOME/flutter/bin:$HOME/.cargo/bin:$PATH" -cd mobile && flutter build apk --debug -``` - -**Critical:** -- `run_build_tool.sh` needs `$HOME/.cargo/bin` in PATH (Gradle daemon doesn't inherit it) -- Use `rustls-tls` (not `native-tls`) for all deps — avoids cross-compiling OpenSSL for NDK - -## Architecture - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Flutter Mobile App │ -│ │ -│ ┌──────────────────┐ ┌────────────────────────────────┐ │ -│ │ Dart UI │ │ Rust (via flutter_rust_bridge) │ │ -│ │ │ │ │ │ -│ │ Screens │ │ ConnectionManager (OnceLock) │ │ -│ │ Providers │ │ ├─ RemoteClient │ │ -│ │ Widgets │ │ ├─ MobileConnectionHandler │ │ -│ │ │ │ └─ TerminalHolder per terminal │ │ -│ │ │ │ └─ alacritty_terminal::Term │ │ -│ └──────────────────┘ └────────────────────────────────┘ │ -│ │ │ │ -│ flutter_rust_bridge (FFI) │ │ -└──────────┼──────────────────────────┼─────────────────────────┘ - │ HTTP + WebSocket - ▼ ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Okena Desktop (remote server on src/remote/) │ -│ POST /v1/pair, GET /v1/state, POST /v1/actions, WS /v1/stream │ -└──────────────────────────────────────────────────────────────┘ -``` - -## Directory Structure - -``` -mobile/ -├── lib/ -│ ├── main.dart # App entry, MultiProvider setup, AppRouter -│ └── src/ -│ ├── models/ -│ │ ├── layout_node.dart # Sealed classes: TerminalNode, SplitNode, TabsNode -│ │ └── saved_server.dart # Server config (host, port, label), JSON persistence -│ ├── providers/ -│ │ ├── connection_provider.dart # Saved servers, connection lifecycle, status polling -│ │ └── workspace_provider.dart # Project list, focused project, 1s polling -│ ├── screens/ -│ │ ├── server_list_screen.dart # Server list + add bottom sheet -│ │ ├── pairing_screen.dart # Connect → pair flow, code input -│ │ └── workspace_screen.dart # App bar + drawer + layout + key toolbar -│ ├── theme/ -│ │ └── app_theme.dart # JetBrainsMono font, Catppuccin dark colors -│ ├── widgets/ -│ │ ├── key_toolbar.dart # ESC, TAB, CTRL/ALT (sticky), arrows -│ │ ├── layout_renderer.dart # Recursive layout tree → TerminalView/Flex/Tabs -│ │ ├── project_drawer.dart # Project list drawer with disconnect button -│ │ ├── status_indicator.dart # Colored dot + label -│ │ ├── terminal_painter.dart # CustomPainter: bg rects → text → cursor -│ │ └── terminal_view.dart # Terminal widget: resize, input, 30fps polling -│ └── rust/ # GENERATED by flutter_rust_bridge — do not edit -│ └── api/ # Dart bindings for native/src/api/ -├── native/ # Rust FFI crate (okena_mobile_native) -│ └── src/ -│ ├── lib.rs -│ ├── api/ -│ │ ├── connection.rs # init_app, connect, pair, disconnect, connection_status -│ │ ├── terminal.rs # get_visible_cells, get_cursor, send_text, resize_terminal -│ │ └── state.rs # get_projects, is_dirty, send_special_key, get_project_layout_json -│ ├── client/ -│ │ ├── manager.rs # ConnectionManager singleton (OnceLock + 2-thread tokio runtime) -│ │ ├── handler.rs # MobileConnectionHandler (impl ConnectionHandler) -│ │ └── terminal_holder.rs # alacritty_terminal::Term wrapper + dirty flag -│ └── frb_generated.rs # GENERATED — do not edit -├── fonts/ # JetBrainsMono (Regular, Bold, Italic, BoldItalic) -├── rust_builder/ # Cargokit — NDK cross-compilation tooling -├── flutter_rust_bridge.yaml # FRB config: rust_root=native/, dart_output=lib/src/rust -└── pubspec.yaml # Flutter deps: provider, shared_preferences, flutter_rust_bridge -``` - -## Data Flow - -### Terminal output (server → screen) - -``` -Remote PTY → WS binary frame → RemoteClient → MobileConnectionHandler.on_terminal_output() - → TerminalHolder.process_output() (alacritty ANSI parse) → dirty flag - → Flutter polls is_dirty() every 33ms → get_visible_cells() → CustomPainter repaint -``` - -### Keyboard input (user → server) - -``` -Soft keyboard / KeyToolbar tap → FFI send_text() or send_special_key() - → ConnectionManager.send_ws_message() → WS → Server → PTY stdin -``` - -### State sync - -``` -WS "state_changed" event → RemoteClient fetches GET /v1/state - → diff_states() → create/remove TerminalHolders → state_cache updated - → Flutter polls get_projects() every 1s → UI update -``` - -## Key Patterns - -### State management -- `ChangeNotifier` providers with polling (no push from Rust → Dart) -- `ConnectionProvider`: 500ms fast poll during connect/pair, 2s when connected -- `WorkspaceProvider`: 1s poll for project list, auto-selects focused project - -### Terminal rendering -- `TerminalPainter` (CustomPainter): 3-pass — background rects, text, cursor -- `TerminalView`: `LayoutBuilder` measures size → computes cols/rows → `resize_terminal()` FFI -- 33ms refresh timer (~30fps) via `is_dirty()` check -- 200ms debounce on resize - -### Layout rendering -- `LayoutRenderer` parses `get_project_layout_json()` into sealed `LayoutNode` classes -- Recursive rendering: `TerminalNode` → `TerminalView`, `SplitNode` → `Flex`, `TabsNode` → tab bar -- Auto-converts horizontal splits to vertical in portrait orientation - -### Key toolbar -- CTRL/ALT are sticky toggles (tap to activate, next key applies modifier, auto-resets) -- CTRL+key sends control character directly (e.g., CTRL+C → `\x03`) - -### Rust singleton -- `ConnectionManager` is `OnceLock` — no GPUI entity system on mobile -- 2-thread tokio runtime for async networking -- All FFI functions access via `ConnectionManager::get()` -- `NoopEventListener` on `Term` — prevents duplicate PtyWrite responses (server already handles them) - -## FFI Surface (native/src/api/) - -### connection.rs -| Function | Description | -|----------|-------------| -| `init_app()` | Setup FRB + ConnectionManager | -| `connect(host, port) → String` | Create connection, return conn_id | -| `pair(conn_id, code)` | Async pair + start WS | -| `disconnect(conn_id)` | Close WS, cleanup | -| `connection_status(conn_id) → ConnectionStatus` | Current status | - -### terminal.rs -| Function | Description | -|----------|-------------| -| `get_visible_cells(conn_id, terminal_id) → Vec` | Grid cells with ARGB + flags | -| `get_cursor(conn_id, terminal_id) → CursorState` | Position, shape, visibility | -| `send_text(conn_id, terminal_id, text)` | Send text via WS (async) | -| `resize_terminal(conn_id, terminal_id, cols, rows)` | Resize local + send WS (async) | - -### state.rs -| Function | Description | -|----------|-------------| -| `get_projects(conn_id) → Vec` | Project list from cache | -| `get_focused_project_id(conn_id) → Option` | Server's focused project | -| `is_dirty(conn_id, terminal_id) → bool` | Has new output since last read | -| `send_special_key(conn_id, terminal_id, key)` | Named key (async) | -| `get_project_layout_json(conn_id, project_id) → Option` | Layout tree as JSON | -| `get_all_terminal_ids(conn_id) → Vec` | All terminal IDs | - -## Dependencies - -- **flutter_rust_bridge 2.11.1** — Rust↔Dart FFI bridge + codegen -- **provider** — State management (ChangeNotifier) -- **shared_preferences** — Persist saved server list -- **okena-core** (workspace crate) — Shared types, RemoteClient, ThemeColors -- **alacritty_terminal** — Terminal emulation (same as desktop) -- **tokio** + **tokio-tungstenite** (rustls-tls) — Async runtime + WebSocket -- **reqwest** (rustls-tls) — HTTP client for REST API diff --git a/mobile/README.md b/mobile/README.md deleted file mode 100644 index 7b012856..00000000 --- a/mobile/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# mobile - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/mobile/RN_MIGRATION.md b/mobile/RN_MIGRATION.md new file mode 100644 index 00000000..fc1b6703 --- /dev/null +++ b/mobile/RN_MIGRATION.md @@ -0,0 +1,184 @@ +# Mobile app → React Native migration plan + +Status: **in progress — Flutter removed, RN in place, device build pending.** The Flutter app +(`mobile/lib` + `mobile/native`) has been deleted; the Rust engine now lives self-contained in +`crates/okena-mobile-ffi` (uniffi), and `mobile/rn` is a complete RN 0.76 project + tooling + +`ubrn.config.yaml`. What remains is toolchain/device-bound: generating the native host +projects, running the ubrn cross-compile, and the Phase-0 spikes (§3) — see +[`rn/README.md`](rn/README.md). This document is kept as the design/rationale record. + +Primary strategy: **RN + Rust core via uniffi**, with **native GPU rendering** of the +terminal (no `xterm.js`). Dropping Rust entirely is a documented *fallback*, not the goal. + +--- + +## 1. Why this is architecturally clean + +All real logic already lives below an FFI-agnostic seam. The mobile app is a *thin* terminal +client over the desktop remote server (`/v1/pair`, `/v1/state`, `/v1/actions`, WS `/v1/stream`). + +``` +crates/okena-core (feature = "client") ← REUSE AS-IS. GPUI-free, framework-agnostic. + RemoteClient protocol, TLS + cert pinning (TOFU), WS, + ConnectionHandler trait reconnect, state diffing. ~1900 LOC. + ▲ +mobile/native (okena_mobile_native) ← REWRITE the binding layer only. + ConnectionManager / MobileConnectionHandler The *logic* here is plain Rust and is reusable; + TerminalHolder (alacritty_terminal::Term) only the `flutter_rust_bridge` attributes are + api/{connection,terminal,state}.rs Flutter-specific. ~60 exported FFI fns. + ▲ +Flutter UI (mobile/lib, ~2700+ LOC Dart) ← REPLACE with React Native. +``` + +The seam is `RemoteClient` (`crates/okena-core/src/client/connection.rs`). +`MobileConnectionHandler` is just one `impl ConnectionHandler`; the desktop has its own. RN +gets a third consumer of the *same* core — identical terminal emulation as desktop, because +both run `alacritty_terminal`. + +**Consequence:** ~80% of the hard work (wire protocol, TLS, reconnect, ANSI parsing) is not +touched. We swap the binding generator (`flutter_rust_bridge` → `uniffi`) and rewrite the UI. + +--- + +## 2. Target architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ React Native app (TypeScript) │ +│ │ +│ Screens / navigation / state (zustand or context) │ +│ KeyToolbar, ProjectDrawer, LayoutRenderer (RN components) │ +│ TerminalView → react-native-skia (native GPU) │ +│ │ reads packed cell buffer, paints 3-pass │ +│ ▼ │ +│ TS bindings (generated by uniffi-bindgen-react-native, JSI) │ +└────────┼─────────────────────────────────────────────────────┘ + │ JSI (in-process, sync + async) +┌────────▼─────────────────────────────────────────────────────┐ +│ crates/okena-mobile-ffi (uniffi-annotated, was mobile/native)│ +│ reuses ConnectionManager + TerminalHolder logic verbatim │ +│ ▲ │ +│ crates/okena-core (client) ← unchanged │ +└───────────────────────────────────────────────────────────────┘ +``` + +Three decisions define the project: + +### Decision A — binding tool: `uniffi-bindgen-react-native` +Mozilla `uniffi` describes the Rust API (via `#[uniffi::export]` proc-macros — no UDL needed), +and **`uniffi-bindgen-react-native` (ubrn)** generates a JSI-based TurboModule package consumable +from RN (new architecture). It also scaffolds the cargo cross-compile + codegen pipeline for both +Android (NDK) and iOS (xcframework), which is the equivalent of what `cargokit` does for Flutter today. + +- Async Rust fns (`pair`, `send_text`, `git_diff`, the `*_action` calls — ~40 of the 60) map to + JS Promises. Sync fns (`get_visible_cells`, `get_cursor`, `is_dirty`, `connection_status`) map + to synchronous JSI host functions — important for the render hot path. +- Verify the current `ubrn` version and RN new-architecture requirement during the Phase-0 spike. + +### Decision B — rendering: `react-native-skia` (native, not xterm.js) +The user requirement is **native rendering, no `xterm.js`**. `react-native-skia` exposes the same +Skia GPU canvas that Flutter's `CustomPainter` already uses — so we port `terminal_painter.dart`'s +3-pass algorithm (background rects → glyph run → cursor) almost 1:1, cross-platform, no DOM/WebView. +- Monospace metrics + glyph runs via Skia `Paragraph`/`Font`; ship the existing JetBrainsMono. +- Selection + cursor as overlay paints, identical to the Flutter widget. +- Escalation path if Skia text throughput is insufficient: a true **Fabric native component** + (Android `View`/Canvas or Skia; iOS `UIView` + CoreText/Metal) fed the same cell buffer. More + per-platform code, kept in reserve. + +### Decision C — the hot-path data bridge (the only real perf risk) +Today Flutter polls `get_visible_cells()` at ~30fps and gets a `Vec` (one struct per +cell). Marshaling thousands of records per frame across JSI as objects is the thing that can be +slow. Mitigation, in order: +1. **Packed binary buffer.** Add an FFI fn returning the visible grid as a compact `Vec` + (per cell: codepoint `u32`, fg `u32`, bg `u32`, flags `u8`), exposed as an `ArrayBuffer`. + Skia reads the typed array directly. This is the single most important perf lever. +2. **Drive repaint from `requestAnimationFrame` gated on `is_dirty()`**, not a fixed 33ms timer. +3. If still hot, move the paint loop fully native (Decision B escalation) and pass only the buffer + pointer. +Keep `get_visible_cells` (records) too for non-hot callers; add the packed variant for the canvas. + +--- + +## 3. Phased plan (de-risk first) + +### Phase 0 — Spikes (validate the two unknowns before committing) — ~1 week +- **S1 (toolchain):** `ubrn` hello-world — call `init_app()` + `connect()` + `connection_status()` + from a bare RN app on a real Android device *and* iOS sim. Proves NDK + xcframework build wiring. +- **S2 (rendering):** render a static 80×40 colored cell grid in `react-native-skia` and drive it + at 60fps from a JS timer. Proves Skia text throughput on-device. +- **Gate:** S1 fails → reconsider the *fallback* (§5). S2 fails → commit to the native-component + rendering path (Decision B escalation) up front. + +### Phase 1 — `okena-mobile-ffi` crate — ~1–1.5 weeks +- New crate (or in-place rework of `mobile/native`): strip `flutter_rust_bridge` attrs, add + `#[uniffi::export]`. The `ConnectionManager`, `MobileConnectionHandler`, `TerminalHolder` bodies + carry over unchanged — they are plain Rust. +- Port all ~60 fns (`api/{connection,terminal,state}.rs`). Add the **packed cell buffer** fn. +- Resolve the open `TODO(mobile-tls)`: expose `tls: bool` + fingerprint to the binding (the core + already supports it; Flutter just hardcodes `false`). RN should ship TLS-on from day one. +- Generate TS bindings; integrate cargo build into the RN Android Gradle + iOS pods. + +### Phase 2 — RN app shell — ~1–1.5 weeks +- Bare RN (or Expo + prebuild/dev-client — needs native modules either way). New-architecture on. +- Port `server_list`, `pairing`, `workspace` screens; theme (Catppuccin/iOS dark); navigation. +- State: mirror today's polling providers initially (`ConnectionProvider`, `WorkspaceProvider`), + then optionally move state push from Rust via a uniffi callback interface to drop polling. + +### Phase 3 — terminal rendering (the core deliverable) — ~1.5–2 weeks +- `TerminalView` on `react-native-skia`: 3-pass paint from the packed buffer; cursor + selection. +- Input: hidden `TextInput` for the soft keyboard + a `KeyToolbar` component (ESC/TAB/sticky + CTRL+ALT/arrows) mapping to `send_text` / `send_special_key`. +- Sizing: `onLayout` → cols/rows → `resize_terminal` (200ms debounce, as today). +- Scroll + character/word selection wired to the existing FFI. + +### Phase 4 — feature parity with PR #17 — ~2–3 weeks +`LayoutRenderer` (splits/tabs, portrait auto-vertical), `ProjectDrawer` (add/reorder/color), +git diff + file viewer, services panel, fullscreen, tab/pane management. All FFI already exists. + +### Phase 5 — cutover — ~1 week +CI for both platforms, parity test pass against a live desktop server, then retire `mobile/lib` +(Flutter) and `mobile/native`'s frb layer. + +**Rough total:** ~8–11 focused weeks for full parity; a *usable* connect-pair-render-input demo by +end of Phase 3 (~4–5 weeks). + +--- + +## 4. Reuse vs. rewrite + +| Layer | Today | RN target | Action | +|---|---|---|---| +| Protocol / TLS / WS / state diff | `okena-core` (Rust) | same | **reuse as-is** | +| ANSI emulation | `alacritty_terminal` in `TerminalHolder` | same | **reuse as-is** | +| FFI binding | `flutter_rust_bridge` | `uniffi` + `ubrn` (JSI) | **rewrite (mechanical)** | +| Binding *logic* (ConnectionManager etc.) | `mobile/native` Rust | same Rust | **reuse (strip frb attrs)** | +| Terminal paint | `terminal_painter.dart` (Skia) | `react-native-skia` | **port (~1:1)** | +| Screens / drawer / toolbar / layout | Dart widgets | RN components | **rewrite** | +| State management | `provider` + polling | zustand/context (+ optional push) | **rewrite** | + +--- + +## 5. Risks & the "drop Rust" fallback + +- **`ubrn` maturity / build integration.** The JSI binding generator and dual-platform + cross-compile are the least-proven pieces — hence Phase-0 S1. If it proves too painful to + maintain, the **fallback is to drop Rust on mobile**, *not* to adopt `xterm.js`: + - The web client (`web/src/api/client.ts`, `websocket.ts`) already speaks the identical wire + protocol in pure `fetch`/`WebSocket` TS — **directly reusable in RN**, no DOM. + - Replace `alacritty` with a TS ANSI parser feeding the **same `react-native-skia`** renderer. + Native rendering is preserved; only the emulation moves to JS. (`xterm.js` stays rejected.) + - Cost: a second emulation implementation that can drift from desktop — which is the main + argument *for* keeping Rust. +- **Hot-path perf:** mitigated by the packed buffer (Decision C); escalate to native component. +- **Two FFI generators during transition:** keep `flutter_rust_bridge` building until Phase 5; + `uniffi` lives in a parallel crate so the Flutter app is never broken mid-migration. +- **iOS signing / CocoaPods + Rust xcframework:** PR #17 already set up the iOS Flutter project; + reuse its signing config, but the `ubrn` xcframework wiring is new — covered by S1. + +--- + +## 6. Concrete next step + +Run **Phase 0 spikes (S1 + S2)** on this branch. They are small, independent, and decide between +the primary path (RN + uniffi + Skia) and the fallback (RN + TS protocol + Skia). Everything below +the seam is already in place and building on current `main`. diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml deleted file mode 100644 index 0d290213..00000000 --- a/mobile/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore deleted file mode 100644 index be3943c9..00000000 --- a/mobile/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts deleted file mode 100644 index 18a310af..00000000 --- a/mobile/android/app/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -android { - namespace = "com.example.mobile" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.mobile" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") - } - } -} - -flutter { - source = "../.." -} diff --git a/mobile/android/app/src/debug/AndroidManifest.xml b/mobile/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/mobile/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 771480b6..00000000 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt deleted file mode 100644 index b5dc9d07..00000000 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.mobile - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/mobile/android/app/src/main/res/drawable-v21/launch_background.xml b/mobile/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3..00000000 --- a/mobile/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/mobile/android/app/src/main/res/drawable/launch_background.xml b/mobile/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/mobile/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 183a983d..00000000 Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index bc8e0a00..00000000 Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index fa26a7a5..00000000 Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 0235ae46..00000000 Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 3e5db9a6..00000000 Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/values-night/styles.xml b/mobile/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be7..00000000 --- a/mobile/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef880..00000000 --- a/mobile/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/mobile/android/app/src/profile/AndroidManifest.xml b/mobile/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/mobile/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/mobile/android/build.gradle.kts b/mobile/android/build.gradle.kts deleted file mode 100644 index dbee657b..00000000 --- a/mobile/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties deleted file mode 100644 index fbee1d8c..00000000 --- a/mobile/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e4ef43fb..00000000 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/mobile/android/settings.gradle.kts b/mobile/android/settings.gradle.kts deleted file mode 100644 index ca7fe065..00000000 --- a/mobile/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false -} - -include(":app") diff --git a/mobile/flutter_rust_bridge.yaml b/mobile/flutter_rust_bridge.yaml deleted file mode 100644 index f945cbbf..00000000 --- a/mobile/flutter_rust_bridge.yaml +++ /dev/null @@ -1,3 +0,0 @@ -rust_input: crate::api -rust_root: native/ -dart_output: lib/src/rust \ No newline at end of file diff --git a/mobile/integration_test/simple_test.dart b/mobile/integration_test/simple_test.dart deleted file mode 100644 index 2582c066..00000000 --- a/mobile/integration_test/simple_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/main.dart'; -import 'package:mobile/src/rust/frb_generated.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - setUpAll(() async => await RustLib.init()); - testWidgets('App launches and shows server list', (WidgetTester tester) async { - await tester.pumpWidget(const OkenaApp()); - await tester.pumpAndSettle(); - expect(find.text('Okena'), findsWidgets); - }); -} diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore deleted file mode 100644 index 7a7f9873..00000000 --- a/mobile/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 1dc6cf76..00000000 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 13.0 - - diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee8..00000000 --- a/mobile/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee8..00000000 --- a/mobile/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 1f1b1aae..00000000 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,616 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e3773d42..00000000 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16..00000000 --- a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift deleted file mode 100644 index 62666446..00000000 --- a/mobile/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab..00000000 --- a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index f17c65b8..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index d7c58831..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2aed6485..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 85ebd374..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index f8736086..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index 95d9ded3..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 51348c93..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index b8266e0f..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index a2e8f335..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 975e2dea..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 975e2dea..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e2b6dbae..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 3b525e44..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 7e91b7c8..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d2487fe0..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2f..00000000 --- a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b..00000000 --- a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7..00000000 --- a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/ios/Runner/Base.lproj/Main.storyboard b/mobile/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516..00000000 --- a/mobile/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist deleted file mode 100644 index 43427ad3..00000000 --- a/mobile/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Mobile - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - mobile - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/mobile/ios/Runner/Runner-Bridging-Header.h b/mobile/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a56..00000000 --- a/mobile/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/mobile/ios/RunnerTests/RunnerTests.swift b/mobile/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1..00000000 --- a/mobile/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart deleted file mode 100644 index 1d1cfafa..00000000 --- a/mobile/lib/main.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'src/providers/connection_provider.dart'; -import 'src/providers/workspace_provider.dart'; -import 'src/screens/server_list_screen.dart'; -import 'src/screens/pairing_screen.dart'; -import 'src/screens/workspace_screen.dart'; -import 'src/rust/frb_generated.dart'; - -Future main() async { - await RustLib.init(); - runApp(const OkenaApp()); -} - -class OkenaApp extends StatelessWidget { - const OkenaApp({super.key}); - - @override - Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ConnectionProvider()), - ChangeNotifierProxyProvider( - create: (ctx) => - WorkspaceProvider(ctx.read()), - update: (_, connection, previous) => - previous ?? WorkspaceProvider(connection), - ), - ], - child: MaterialApp( - title: 'Okena', - theme: ThemeData.dark(useMaterial3: true).copyWith( - colorScheme: const ColorScheme.dark( - primary: Color(0xFF007ACC), - surface: Color(0xFF1E1E1E), - ), - scaffoldBackgroundColor: const Color(0xFF1E1E1E), - appBarTheme: const AppBarTheme( - backgroundColor: Color(0xFF323233), - ), - ), - home: const AppRouter(), - ), - ); - } -} - -class AppRouter extends StatelessWidget { - const AppRouter({super.key}); - - @override - Widget build(BuildContext context) { - final connection = context.watch(); - - if (connection.isConnected) { - return const WorkspaceScreen(); - } - if (connection.activeServer != null) { - return const PairingScreen(); - } - return const ServerListScreen(); - } -} diff --git a/mobile/lib/src/models/saved_server.dart b/mobile/lib/src/models/saved_server.dart deleted file mode 100644 index aaf2e968..00000000 --- a/mobile/lib/src/models/saved_server.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:convert'; - -class SavedServer { - final String host; - final int port; - final String? label; - final String? token; - - const SavedServer({ - required this.host, - required this.port, - this.label, - this.token, - }); - - SavedServer copyWith({String? token}) => SavedServer( - host: host, - port: port, - label: label, - token: token ?? this.token, - ); - - String get displayName => label ?? '$host:$port'; - - Map toJson() => { - 'host': host, - 'port': port, - if (label != null) 'label': label, - if (token != null) 'token': token, - }; - - factory SavedServer.fromJson(Map json) => SavedServer( - host: json['host'] as String, - port: json['port'] as int, - label: json['label'] as String?, - token: json['token'] as String?, - ); - - static List listFromJson(String jsonString) { - final list = jsonDecode(jsonString) as List; - return list - .map((e) => SavedServer.fromJson(e as Map)) - .toList(); - } - - static String listToJson(List servers) => - jsonEncode(servers.map((s) => s.toJson()).toList()); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SavedServer && host == other.host && port == other.port; - - @override - int get hashCode => Object.hash(host, port); -} diff --git a/mobile/lib/src/providers/connection_provider.dart b/mobile/lib/src/providers/connection_provider.dart deleted file mode 100644 index 8420a5b7..00000000 --- a/mobile/lib/src/providers/connection_provider.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../models/saved_server.dart'; -import '../../src/rust/api/connection.dart' as ffi; - -const _kSavedServersKey = 'saved_servers'; - -class ConnectionProvider extends ChangeNotifier { - List _servers = []; - SavedServer? _activeServer; - String? _connId; - ffi.ConnectionStatus _status = const ffi.ConnectionStatus.disconnected(); - Timer? _pollTimer; - - List get servers => _servers; - SavedServer? get activeServer => _activeServer; - String? get connId => _connId; - ffi.ConnectionStatus get status => _status; - - bool get isConnected => _status is ffi.ConnectionStatus_Connected; - bool get isPairing => _status is ffi.ConnectionStatus_Pairing; - bool get isConnecting => _status is ffi.ConnectionStatus_Connecting; - bool get isDisconnected => - _status is ffi.ConnectionStatus_Disconnected && _activeServer == null; - - ConnectionProvider() { - _loadServers(); - } - - Future _loadServers() async { - final prefs = await SharedPreferences.getInstance(); - final json = prefs.getString(_kSavedServersKey); - if (json != null) { - try { - _servers = SavedServer.listFromJson(json); - notifyListeners(); - } catch (_) { - // Corrupted data — start fresh - } - } - } - - Future _saveServers() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_kSavedServersKey, SavedServer.listToJson(_servers)); - } - - void addServer(SavedServer server) { - if (!_servers.contains(server)) { - _servers.add(server); - _saveServers(); - notifyListeners(); - } - } - - void removeServer(SavedServer server) { - _servers.remove(server); - _saveServers(); - notifyListeners(); - } - - void connectTo(SavedServer server) { - // Disconnect existing connection first - if (_connId != null) { - ffi.disconnect(connId: _connId!); - _stopPolling(); - } - - _activeServer = server; - _connId = ffi.connect( - host: server.host, port: server.port, savedToken: server.token); - _status = const ffi.ConnectionStatus.connecting(); - notifyListeners(); - _startPolling(fast: true); - } - - Future pair(String code) async { - if (_connId == null) return; - try { - await ffi.pair(connId: _connId!, code: code); - } catch (e) { - _status = ffi.ConnectionStatus.error(message: e.toString()); - notifyListeners(); - } - } - - void disconnect() { - if (_connId != null) { - ffi.disconnect(connId: _connId!); - } - _stopPolling(); - _connId = null; - _activeServer = null; - _status = const ffi.ConnectionStatus.disconnected(); - notifyListeners(); - } - - void _persistToken() { - if (_connId == null || _activeServer == null) return; - final token = ffi.getToken(connId: _connId!); - if (token != null && token != _activeServer!.token) { - final updated = _activeServer!.copyWith(token: token); - final idx = _servers.indexOf(_activeServer!); - if (idx >= 0) { - _servers[idx] = updated; - } - _activeServer = updated; - _saveServers(); - } - } - - void _startPolling({bool fast = false}) { - _pollTimer?.cancel(); - _pollTimer = Timer.periodic( - Duration(milliseconds: fast ? 500 : 2000), - (_) => _pollStatus(), - ); - } - - void _stopPolling() { - _pollTimer?.cancel(); - _pollTimer = null; - } - - void _pollStatus() { - if (_connId == null) return; - final oldStatus = _status; - _status = ffi.connectionStatus(connId: _connId!); - - // Switch to slow polling once connected, and persist the token - if (_status is ffi.ConnectionStatus_Connected && - oldStatus is! ffi.ConnectionStatus_Connected) { - _stopPolling(); - _startPolling(fast: false); - _persistToken(); - } - - // Stop polling on disconnect or error - if (_status is ffi.ConnectionStatus_Disconnected || - _status is ffi.ConnectionStatus_Error) { - _stopPolling(); - } - - if (_status != oldStatus) { - notifyListeners(); - } - } - - @override - void dispose() { - _stopPolling(); - if (_connId != null) { - ffi.disconnect(connId: _connId!); - } - super.dispose(); - } -} diff --git a/mobile/lib/src/providers/workspace_provider.dart b/mobile/lib/src/providers/workspace_provider.dart deleted file mode 100644 index aa51d962..00000000 --- a/mobile/lib/src/providers/workspace_provider.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import 'connection_provider.dart'; -import '../../src/rust/api/state.dart' as ffi; -import '../../src/rust/api/connection.dart' as conn_ffi; - -class WorkspaceProvider extends ChangeNotifier { - final ConnectionProvider _connection; - List _projects = []; - String? _selectedProjectId; - String? _selectedTerminalId; - Set? _previousTerminalIds; - Timer? _pollTimer; - double _secondsSinceActivity = 0; - - List get projects => _projects; - String? get selectedProjectId => _selectedProjectId; - String? get selectedTerminalId => _selectedTerminalId; - double get secondsSinceActivity => _secondsSinceActivity; - - ffi.ProjectInfo? get selectedProject { - if (_selectedProjectId == null) return _projects.firstOrNull; - return _projects - .where((p) => p.id == _selectedProjectId) - .firstOrNull ?? _projects.firstOrNull; - } - - WorkspaceProvider(this._connection) { - _connection.addListener(_onConnectionChanged); - if (_connection.isConnected) { - _startPolling(); - } - } - - void selectProject(String projectId) { - _selectedProjectId = projectId; - _selectedTerminalId = null; - _previousTerminalIds = null; - notifyListeners(); - } - - void selectTerminal(String terminalId) { - _selectedTerminalId = terminalId; - notifyListeners(); - } - - void _onConnectionChanged() { - if (_connection.isConnected) { - _startPolling(); - } else { - _stopPolling(); - _projects = []; - _selectedProjectId = null; - _selectedTerminalId = null; - notifyListeners(); - } - } - - void _startPolling() { - _pollTimer?.cancel(); - _pollTimer = Timer.periodic( - const Duration(seconds: 1), - (_) => _pollState(), - ); - // Immediate first poll - _pollState(); - } - - void _stopPolling() { - _pollTimer?.cancel(); - _pollTimer = null; - } - - void _pollState() { - final connId = _connection.connId; - if (connId == null) return; - - final newProjects = ffi.getProjects(connId: connId); - final focusedId = ffi.getFocusedProjectId(connId: connId); - - bool changed = false; - - if (!_projectListEquals(newProjects, _projects)) { - _projects = newProjects; - changed = true; - } - - // Auto-select the focused project if we don't have a selection - if (_selectedProjectId == null && focusedId != null) { - _selectedProjectId = focusedId; - changed = true; - } - - // Auto-select terminal: pick newly added terminal, or first if current gone - final project = selectedProject; - if (project != null && project.terminalIds.isNotEmpty) { - if (_selectedTerminalId == null || - !project.terminalIds.contains(_selectedTerminalId)) { - _selectedTerminalId = project.terminalIds.first; - changed = true; - } else if (_previousTerminalIds != null) { - // Find newly added terminals - final newIds = project.terminalIds - .where((id) => !_previousTerminalIds!.contains(id)) - .toList(); - if (newIds.isNotEmpty) { - _selectedTerminalId = newIds.last; - changed = true; - } - } - _previousTerminalIds = Set.of(project.terminalIds); - } else { - _previousTerminalIds = null; - if (_selectedTerminalId != null) { - _selectedTerminalId = null; - changed = true; - } - } - - // Poll connection health - final newActivity = conn_ffi.secondsSinceActivity(connId: connId); - if ((_secondsSinceActivity < 3) != (newActivity < 3) || - (_secondsSinceActivity < 10) != (newActivity < 10)) { - changed = true; - } - _secondsSinceActivity = newActivity; - - if (changed) { - notifyListeners(); - } - } - - bool _projectListEquals( - List a, List b) { - if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i].id != b[i].id || a[i].name != b[i].name) return false; - if (!listEquals(a[i].terminalIds, b[i].terminalIds)) return false; - } - return true; - } - - @override - void dispose() { - _connection.removeListener(_onConnectionChanged); - _stopPolling(); - super.dispose(); - } -} diff --git a/mobile/lib/src/rust/api/connection.dart b/mobile/lib/src/rust/api/connection.dart deleted file mode 100644 index ad0cd424..00000000 --- a/mobile/lib/src/rust/api/connection.dart +++ /dev/null @@ -1,53 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -import 'package:freezed_annotation/freezed_annotation.dart' hide protected; -part 'connection.freezed.dart'; - -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`, `from` - -/// Connect to an Okena remote server. Returns a connection ID. -/// If a saved token is provided, it will be used to skip pairing. -String connect({required String host, required int port, String? savedToken}) => - RustLib.instance.api.crateApiConnectionConnect( - host: host, - port: port, - savedToken: savedToken, - ); - -/// Get the current auth token for a connection (if paired). -String? getToken({required String connId}) => - RustLib.instance.api.crateApiConnectionGetToken(connId: connId); - -/// Pair with the server using a pairing code. -Future pair({required String connId, required String code}) => - RustLib.instance.api.crateApiConnectionPair(connId: connId, code: code); - -/// Disconnect from a server. -void disconnect({required String connId}) => - RustLib.instance.api.crateApiConnectionDisconnect(connId: connId); - -/// Get current connection status. -ConnectionStatus connectionStatus({required String connId}) => - RustLib.instance.api.crateApiConnectionConnectionStatus(connId: connId); - -/// Get seconds since last WS activity (terminal output). -/// Returns a large value if the connection doesn't exist. -double secondsSinceActivity({required String connId}) => - RustLib.instance.api.crateApiConnectionSecondsSinceActivity(connId: connId); - -@freezed -sealed class ConnectionStatus with _$ConnectionStatus { - const ConnectionStatus._(); - - const factory ConnectionStatus.disconnected() = ConnectionStatus_Disconnected; - const factory ConnectionStatus.connecting() = ConnectionStatus_Connecting; - const factory ConnectionStatus.connected() = ConnectionStatus_Connected; - const factory ConnectionStatus.pairing() = ConnectionStatus_Pairing; - const factory ConnectionStatus.error({required String message}) = - ConnectionStatus_Error; -} diff --git a/mobile/lib/src/rust/api/connection.freezed.dart b/mobile/lib/src/rust/api/connection.freezed.dart deleted file mode 100644 index 512cf6e9..00000000 --- a/mobile/lib/src/rust/api/connection.freezed.dart +++ /dev/null @@ -1,386 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'connection.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$ConnectionStatus { - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus()'; -} - - -} - -/// @nodoc -class $ConnectionStatusCopyWith<$Res> { -$ConnectionStatusCopyWith(ConnectionStatus _, $Res Function(ConnectionStatus) __); -} - - -/// Adds pattern-matching-related methods to [ConnectionStatus]. -extension ConnectionStatusPatterns on ConnectionStatus { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap({TResult Function( ConnectionStatus_Disconnected value)? disconnected,TResult Function( ConnectionStatus_Connecting value)? connecting,TResult Function( ConnectionStatus_Connected value)? connected,TResult Function( ConnectionStatus_Pairing value)? pairing,TResult Function( ConnectionStatus_Error value)? error,required TResult orElse(),}){ -final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected(_that);case ConnectionStatus_Connecting() when connecting != null: -return connecting(_that);case ConnectionStatus_Connected() when connected != null: -return connected(_that);case ConnectionStatus_Pairing() when pairing != null: -return pairing(_that);case ConnectionStatus_Error() when error != null: -return error(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map({required TResult Function( ConnectionStatus_Disconnected value) disconnected,required TResult Function( ConnectionStatus_Connecting value) connecting,required TResult Function( ConnectionStatus_Connected value) connected,required TResult Function( ConnectionStatus_Pairing value) pairing,required TResult Function( ConnectionStatus_Error value) error,}){ -final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected(): -return disconnected(_that);case ConnectionStatus_Connecting(): -return connecting(_that);case ConnectionStatus_Connected(): -return connected(_that);case ConnectionStatus_Pairing(): -return pairing(_that);case ConnectionStatus_Error(): -return error(_that);} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull({TResult? Function( ConnectionStatus_Disconnected value)? disconnected,TResult? Function( ConnectionStatus_Connecting value)? connecting,TResult? Function( ConnectionStatus_Connected value)? connected,TResult? Function( ConnectionStatus_Pairing value)? pairing,TResult? Function( ConnectionStatus_Error value)? error,}){ -final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected(_that);case ConnectionStatus_Connecting() when connecting != null: -return connecting(_that);case ConnectionStatus_Connected() when connected != null: -return connected(_that);case ConnectionStatus_Pairing() when pairing != null: -return pairing(_that);case ConnectionStatus_Error() when error != null: -return error(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen({TResult Function()? disconnected,TResult Function()? connecting,TResult Function()? connected,TResult Function()? pairing,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected();case ConnectionStatus_Connecting() when connecting != null: -return connecting();case ConnectionStatus_Connected() when connected != null: -return connected();case ConnectionStatus_Pairing() when pairing != null: -return pairing();case ConnectionStatus_Error() when error != null: -return error(_that.message);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when({required TResult Function() disconnected,required TResult Function() connecting,required TResult Function() connected,required TResult Function() pairing,required TResult Function( String message) error,}) {final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected(): -return disconnected();case ConnectionStatus_Connecting(): -return connecting();case ConnectionStatus_Connected(): -return connected();case ConnectionStatus_Pairing(): -return pairing();case ConnectionStatus_Error(): -return error(_that.message);} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull({TResult? Function()? disconnected,TResult? Function()? connecting,TResult? Function()? connected,TResult? Function()? pairing,TResult? Function( String message)? error,}) {final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected();case ConnectionStatus_Connecting() when connecting != null: -return connecting();case ConnectionStatus_Connected() when connected != null: -return connected();case ConnectionStatus_Pairing() when pairing != null: -return pairing();case ConnectionStatus_Error() when error != null: -return error(_that.message);case _: - return null; - -} -} - -} - -/// @nodoc - - -class ConnectionStatus_Disconnected extends ConnectionStatus { - const ConnectionStatus_Disconnected(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Disconnected); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.disconnected()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Connecting extends ConnectionStatus { - const ConnectionStatus_Connecting(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Connecting); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.connecting()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Connected extends ConnectionStatus { - const ConnectionStatus_Connected(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Connected); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.connected()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Pairing extends ConnectionStatus { - const ConnectionStatus_Pairing(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Pairing); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.pairing()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Error extends ConnectionStatus { - const ConnectionStatus_Error({required this.message}): super._(); - - - final String message; - -/// Create a copy of ConnectionStatus -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$ConnectionStatus_ErrorCopyWith get copyWith => _$ConnectionStatus_ErrorCopyWithImpl(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Error&&(identical(other.message, message) || other.message == message)); -} - - -@override -int get hashCode => Object.hash(runtimeType,message); - -@override -String toString() { - return 'ConnectionStatus.error(message: $message)'; -} - - -} - -/// @nodoc -abstract mixin class $ConnectionStatus_ErrorCopyWith<$Res> implements $ConnectionStatusCopyWith<$Res> { - factory $ConnectionStatus_ErrorCopyWith(ConnectionStatus_Error value, $Res Function(ConnectionStatus_Error) _then) = _$ConnectionStatus_ErrorCopyWithImpl; -@useResult -$Res call({ - String message -}); - - - - -} -/// @nodoc -class _$ConnectionStatus_ErrorCopyWithImpl<$Res> - implements $ConnectionStatus_ErrorCopyWith<$Res> { - _$ConnectionStatus_ErrorCopyWithImpl(this._self, this._then); - - final ConnectionStatus_Error _self; - final $Res Function(ConnectionStatus_Error) _then; - -/// Create a copy of ConnectionStatus -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') $Res call({Object? message = null,}) { - return _then(ConnectionStatus_Error( -message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - -// dart format on diff --git a/mobile/lib/src/rust/api/state.dart b/mobile/lib/src/rust/api/state.dart deleted file mode 100644 index ac4924d9..00000000 --- a/mobile/lib/src/rust/api/state.dart +++ /dev/null @@ -1,101 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These functions are ignored because they are not marked as `pub`: `collect_layout_ids_vec` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt` - -/// Get all projects from the cached remote state. -List getProjects({required String connId}) => - RustLib.instance.api.crateApiStateGetProjects(connId: connId); - -/// Get the focused project ID from the cached remote state. -String? getFocusedProjectId({required String connId}) => - RustLib.instance.api.crateApiStateGetFocusedProjectId(connId: connId); - -/// Check if a terminal has unprocessed output (dirty flag). -bool isDirty({required String connId, required String terminalId}) => RustLib - .instance - .api - .crateApiStateIsDirty(connId: connId, terminalId: terminalId); - -/// Send a special key (e.g. "Enter", "Tab", "Escape") to a terminal. -/// -/// The key name is deserialized from JSON (e.g. `"Enter"`, `"CtrlC"`, `"ArrowUp"`). -Future sendSpecialKey({ - required String connId, - required String terminalId, - required String key, -}) => RustLib.instance.api.crateApiStateSendSpecialKey( - connId: connId, - terminalId: terminalId, - key: key, -); - -/// Get all terminal IDs from the cached remote state (flat list). -List getAllTerminalIds({required String connId}) => - RustLib.instance.api.crateApiStateGetAllTerminalIds(connId: connId); - -/// Create a new terminal in the given project via POST /v1/actions. -Future createTerminal({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateCreateTerminal( - connId: connId, - projectId: projectId, -); - -/// Close a terminal in the given project via POST /v1/actions. -Future closeTerminal({ - required String connId, - required String projectId, - required String terminalId, -}) => RustLib.instance.api.crateApiStateCloseTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, -); - -/// Flat FFI-friendly project info. -class ProjectInfo { - final String id; - final String name; - final String path; - final bool showInOverview; - final List terminalIds; - final Map terminalNames; - - const ProjectInfo({ - required this.id, - required this.name, - required this.path, - required this.showInOverview, - required this.terminalIds, - required this.terminalNames, - }); - - @override - int get hashCode => - id.hashCode ^ - name.hashCode ^ - path.hashCode ^ - showInOverview.hashCode ^ - terminalIds.hashCode ^ - terminalNames.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ProjectInfo && - runtimeType == other.runtimeType && - id == other.id && - name == other.name && - path == other.path && - showInOverview == other.showInOverview && - terminalIds == other.terminalIds && - terminalNames == other.terminalNames; -} diff --git a/mobile/lib/src/rust/api/terminal.dart b/mobile/lib/src/rust/api/terminal.dart deleted file mode 100644 index 38e37931..00000000 --- a/mobile/lib/src/rust/api/terminal.dart +++ /dev/null @@ -1,254 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt` - -/// Get the visible terminal cells for rendering. -List getVisibleCells({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetVisibleCells( - connId: connId, - terminalId: terminalId, -); - -/// Get the current cursor state. -CursorState getCursor({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalGetCursor( - connId: connId, - terminalId: terminalId, - ); - -/// Scroll the terminal display (positive = up, negative = down). -void scroll({ - required String connId, - required String terminalId, - required int delta, -}) => RustLib.instance.api.crateApiTerminalScroll( - connId: connId, - terminalId: terminalId, - delta: delta, -); - -/// Get scroll info: total lines, visible lines, display offset. -ScrollInfo getScrollInfo({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetScrollInfo( - connId: connId, - terminalId: terminalId, -); - -/// Start a character-level selection at col/row. -void startSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalStartSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Start a word (semantic) selection at col/row. -void startWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalStartWordSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Extend the current selection to col/row. -void updateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalUpdateSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Clear the current selection. -void clearSelection({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalClearSelection( - connId: connId, - terminalId: terminalId, - ); - -/// Get the selected text, if any. -String? getSelectedText({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalGetSelectedText( - connId: connId, - terminalId: terminalId, - ); - -/// Get selection bounds for rendering. -SelectionBounds? getSelectionBounds({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetSelectionBounds( - connId: connId, - terminalId: terminalId, -); - -/// Send text input to a terminal. -Future sendText({ - required String connId, - required String terminalId, - required String text, -}) => RustLib.instance.api.crateApiTerminalSendText( - connId: connId, - terminalId: terminalId, - text: text, -); - -/// Resize a terminal. -void resizeTerminal({ - required String connId, - required String terminalId, - required int cols, - required int rows, -}) => RustLib.instance.api.crateApiTerminalResizeTerminal( - connId: connId, - terminalId: terminalId, - cols: cols, - rows: rows, -); - -/// Cell data for FFI transfer (flat, no pointers). -class CellData { - /// The character in this cell. - final String character; - - /// Foreground color as ARGB packed u32. - final int fg; - - /// Background color as ARGB packed u32. - final int bg; - - /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). - final int flags; - - const CellData({ - required this.character, - required this.fg, - required this.bg, - required this.flags, - }); - - @override - int get hashCode => - character.hashCode ^ fg.hashCode ^ bg.hashCode ^ flags.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CellData && - runtimeType == other.runtimeType && - character == other.character && - fg == other.fg && - bg == other.bg && - flags == other.flags; -} - -/// Cursor shape variants. -enum CursorShape { block, underline, beam } - -/// Cursor state for FFI transfer. -class CursorState { - final int col; - final int row; - final CursorShape shape; - final bool visible; - - const CursorState({ - required this.col, - required this.row, - required this.shape, - required this.visible, - }); - - @override - int get hashCode => - col.hashCode ^ row.hashCode ^ shape.hashCode ^ visible.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CursorState && - runtimeType == other.runtimeType && - col == other.col && - row == other.row && - shape == other.shape && - visible == other.visible; -} - -/// Scroll info for FFI transfer. -class ScrollInfo { - final int totalLines; - final int visibleLines; - final int displayOffset; - - const ScrollInfo({ - required this.totalLines, - required this.visibleLines, - required this.displayOffset, - }); - - @override - int get hashCode => - totalLines.hashCode ^ visibleLines.hashCode ^ displayOffset.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ScrollInfo && - runtimeType == other.runtimeType && - totalLines == other.totalLines && - visibleLines == other.visibleLines && - displayOffset == other.displayOffset; -} - -/// Selection bounds for FFI transfer. -class SelectionBounds { - final int startCol; - final int startRow; - final int endCol; - final int endRow; - - const SelectionBounds({ - required this.startCol, - required this.startRow, - required this.endCol, - required this.endRow, - }); - - @override - int get hashCode => - startCol.hashCode ^ startRow.hashCode ^ endCol.hashCode ^ endRow.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SelectionBounds && - runtimeType == other.runtimeType && - startCol == other.startCol && - startRow == other.startRow && - endCol == other.endCol && - endRow == other.endRow; -} diff --git a/mobile/lib/src/rust/frb_generated.dart b/mobile/lib/src/rust/frb_generated.dart deleted file mode 100644 index bf15aa8d..00000000 --- a/mobile/lib/src/rust/frb_generated.dart +++ /dev/null @@ -1,1718 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -import 'api/connection.dart'; -import 'api/state.dart'; -import 'api/terminal.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'frb_generated.io.dart' - if (dart.library.js_interop) 'frb_generated.web.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -/// Main entrypoint of the Rust API -class RustLib extends BaseEntrypoint { - @internal - static final instance = RustLib._(); - - RustLib._(); - - /// Initialize flutter_rust_bridge - static Future init({ - RustLibApi? api, - BaseHandler? handler, - ExternalLibrary? externalLibrary, - bool forceSameCodegenVersion = true, - }) async { - await instance.initImpl( - api: api, - handler: handler, - externalLibrary: externalLibrary, - forceSameCodegenVersion: forceSameCodegenVersion, - ); - } - - /// Initialize flutter_rust_bridge in mock mode. - /// No libraries for FFI are loaded. - static void initMock({required RustLibApi api}) { - instance.initMockImpl(api: api); - } - - /// Dispose flutter_rust_bridge - /// - /// The call to this function is optional, since flutter_rust_bridge (and everything else) - /// is automatically disposed when the app stops. - static void dispose() => instance.disposeImpl(); - - @override - ApiImplConstructor get apiImplConstructor => - RustLibApiImpl.new; - - @override - WireConstructor get wireConstructor => - RustLibWire.fromExternalLibrary; - - @override - Future executeRustInitializers() async { - await api.crateApiConnectionInitApp(); - } - - @override - ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => - kDefaultExternalLibraryLoaderConfig; - - @override - String get codegenVersion => '2.11.1'; - - @override - int get rustContentHash => -1973712882; - - static const kDefaultExternalLibraryLoaderConfig = - ExternalLibraryLoaderConfig( - stem: 'okena_mobile_native', - ioDirectory: 'native/target/release/', - webPrefix: 'pkg/', - ); -} - -abstract class RustLibApi extends BaseApi { - void crateApiTerminalClearSelection({ - required String connId, - required String terminalId, - }); - - Future crateApiStateCloseTerminal({ - required String connId, - required String projectId, - required String terminalId, - }); - - String crateApiConnectionConnect({ - required String host, - required int port, - String? savedToken, - }); - - ConnectionStatus crateApiConnectionConnectionStatus({required String connId}); - - Future crateApiStateCreateTerminal({ - required String connId, - required String projectId, - }); - - void crateApiConnectionDisconnect({required String connId}); - - List crateApiStateGetAllTerminalIds({required String connId}); - - CursorState crateApiTerminalGetCursor({ - required String connId, - required String terminalId, - }); - - String? crateApiStateGetFocusedProjectId({required String connId}); - - List crateApiStateGetProjects({required String connId}); - - ScrollInfo crateApiTerminalGetScrollInfo({ - required String connId, - required String terminalId, - }); - - String? crateApiTerminalGetSelectedText({ - required String connId, - required String terminalId, - }); - - SelectionBounds? crateApiTerminalGetSelectionBounds({ - required String connId, - required String terminalId, - }); - - String? crateApiConnectionGetToken({required String connId}); - - List crateApiTerminalGetVisibleCells({ - required String connId, - required String terminalId, - }); - - Future crateApiConnectionInitApp(); - - bool crateApiStateIsDirty({ - required String connId, - required String terminalId, - }); - - Future crateApiConnectionPair({ - required String connId, - required String code, - }); - - void crateApiTerminalResizeTerminal({ - required String connId, - required String terminalId, - required int cols, - required int rows, - }); - - void crateApiTerminalScroll({ - required String connId, - required String terminalId, - required int delta, - }); - - double crateApiConnectionSecondsSinceActivity({required String connId}); - - Future crateApiStateSendSpecialKey({ - required String connId, - required String terminalId, - required String key, - }); - - Future crateApiTerminalSendText({ - required String connId, - required String terminalId, - required String text, - }); - - void crateApiTerminalStartSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - void crateApiTerminalStartWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - void crateApiTerminalUpdateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); -} - -class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { - RustLibApiImpl({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @override - void crateApiTerminalClearSelection({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalClearSelectionConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalClearSelectionConstMeta => - const TaskConstMeta( - debugName: "clear_selection", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiStateCloseTerminal({ - required String connId, - required String projectId, - required String terminalId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 2, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateCloseTerminalConstMeta, - argValues: [connId, projectId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateCloseTerminalConstMeta => const TaskConstMeta( - debugName: "close_terminal", - argNames: ["connId", "projectId", "terminalId"], - ); - - @override - String crateApiConnectionConnect({ - required String host, - required int port, - String? savedToken, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(host, serializer); - sse_encode_u_16(port, serializer); - sse_encode_opt_String(savedToken, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionConnectConstMeta, - argValues: [host, port, savedToken], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionConnectConstMeta => const TaskConstMeta( - debugName: "connect", - argNames: ["host", "port", "savedToken"], - ); - - @override - ConnectionStatus crateApiConnectionConnectionStatus({ - required String connId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_connection_status, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionConnectionStatusConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionConnectionStatusConstMeta => - const TaskConstMeta(debugName: "connection_status", argNames: ["connId"]); - - @override - Future crateApiStateCreateTerminal({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 5, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateCreateTerminalConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateCreateTerminalConstMeta => - const TaskConstMeta( - debugName: "create_terminal", - argNames: ["connId", "projectId"], - ); - - @override - void crateApiConnectionDisconnect({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionDisconnectConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionDisconnectConstMeta => - const TaskConstMeta(debugName: "disconnect", argNames: ["connId"]); - - @override - List crateApiStateGetAllTerminalIds({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_String, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetAllTerminalIdsConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetAllTerminalIdsConstMeta => - const TaskConstMeta( - debugName: "get_all_terminal_ids", - argNames: ["connId"], - ); - - @override - CursorState crateApiTerminalGetCursor({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_cursor_state, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetCursorConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetCursorConstMeta => const TaskConstMeta( - debugName: "get_cursor", - argNames: ["connId", "terminalId"], - ); - - @override - String? crateApiStateGetFocusedProjectId({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetFocusedProjectIdConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetFocusedProjectIdConstMeta => - const TaskConstMeta( - debugName: "get_focused_project_id", - argNames: ["connId"], - ); - - @override - List crateApiStateGetProjects({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_project_info, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetProjectsConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetProjectsConstMeta => - const TaskConstMeta(debugName: "get_projects", argNames: ["connId"]); - - @override - ScrollInfo crateApiTerminalGetScrollInfo({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_scroll_info, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetScrollInfoConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetScrollInfoConstMeta => - const TaskConstMeta( - debugName: "get_scroll_info", - argNames: ["connId", "terminalId"], - ); - - @override - String? crateApiTerminalGetSelectedText({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetSelectedTextConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetSelectedTextConstMeta => - const TaskConstMeta( - debugName: "get_selected_text", - argNames: ["connId", "terminalId"], - ); - - @override - SelectionBounds? crateApiTerminalGetSelectionBounds({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_box_autoadd_selection_bounds, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetSelectionBoundsConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetSelectionBoundsConstMeta => - const TaskConstMeta( - debugName: "get_selection_bounds", - argNames: ["connId", "terminalId"], - ); - - @override - String? crateApiConnectionGetToken({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionGetTokenConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionGetTokenConstMeta => - const TaskConstMeta(debugName: "get_token", argNames: ["connId"]); - - @override - List crateApiTerminalGetVisibleCells({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_cell_data, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetVisibleCellsConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetVisibleCellsConstMeta => - const TaskConstMeta( - debugName: "get_visible_cells", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiConnectionInitApp() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 16, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionInitAppConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionInitAppConstMeta => - const TaskConstMeta(debugName: "init_app", argNames: []); - - @override - bool crateApiStateIsDirty({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_bool, - decodeErrorData: null, - ), - constMeta: kCrateApiStateIsDirtyConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateIsDirtyConstMeta => const TaskConstMeta( - debugName: "is_dirty", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiConnectionPair({ - required String connId, - required String code, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(code, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 18, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiConnectionPairConstMeta, - argValues: [connId, code], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionPairConstMeta => - const TaskConstMeta(debugName: "pair", argNames: ["connId", "code"]); - - @override - void crateApiTerminalResizeTerminal({ - required String connId, - required String terminalId, - required int cols, - required int rows, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(cols, serializer); - sse_encode_u_16(rows, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalResizeTerminalConstMeta, - argValues: [connId, terminalId, cols, rows], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => - const TaskConstMeta( - debugName: "resize_terminal", - argNames: ["connId", "terminalId", "cols", "rows"], - ); - - @override - void crateApiTerminalScroll({ - required String connId, - required String terminalId, - required int delta, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_i_32(delta, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalScrollConstMeta, - argValues: [connId, terminalId, delta], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalScrollConstMeta => const TaskConstMeta( - debugName: "scroll", - argNames: ["connId", "terminalId", "delta"], - ); - - @override - double crateApiConnectionSecondsSinceActivity({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_f_64, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionSecondsSinceActivityConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionSecondsSinceActivityConstMeta => - const TaskConstMeta( - debugName: "seconds_since_activity", - argNames: ["connId"], - ); - - @override - Future crateApiStateSendSpecialKey({ - required String connId, - required String terminalId, - required String key, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(key, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 22, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSendSpecialKeyConstMeta, - argValues: [connId, terminalId, key], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSendSpecialKeyConstMeta => - const TaskConstMeta( - debugName: "send_special_key", - argNames: ["connId", "terminalId", "key"], - ); - - @override - Future crateApiTerminalSendText({ - required String connId, - required String terminalId, - required String text, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(text, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 23, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiTerminalSendTextConstMeta, - argValues: [connId, terminalId, text], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalSendTextConstMeta => const TaskConstMeta( - debugName: "send_text", - argNames: ["connId", "terminalId", "text"], - ); - - @override - void crateApiTerminalStartSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalStartSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalStartSelectionConstMeta => - const TaskConstMeta( - debugName: "start_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - void crateApiTerminalStartWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalStartWordSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalStartWordSelectionConstMeta => - const TaskConstMeta( - debugName: "start_word_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - void crateApiTerminalUpdateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalUpdateSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalUpdateSelectionConstMeta => - const TaskConstMeta( - debugName: "update_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return AnyhowException(raw as String); - } - - @protected - Map dco_decode_Map_String_String_None(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return Map.fromEntries( - dco_decode_list_record_string_string( - raw, - ).map((e) => MapEntry(e.$1, e.$2)), - ); - } - - @protected - String dco_decode_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as String; - } - - @protected - bool dco_decode_bool(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as bool; - } - - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_selection_bounds(raw); - } - - @protected - CellData dco_decode_cell_data(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return CellData( - character: dco_decode_String(arr[0]), - fg: dco_decode_u_32(arr[1]), - bg: dco_decode_u_32(arr[2]), - flags: dco_decode_u_8(arr[3]), - ); - } - - @protected - ConnectionStatus dco_decode_connection_status(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - switch (raw[0]) { - case 0: - return ConnectionStatus_Disconnected(); - case 1: - return ConnectionStatus_Connecting(); - case 2: - return ConnectionStatus_Connected(); - case 3: - return ConnectionStatus_Pairing(); - case 4: - return ConnectionStatus_Error(message: dco_decode_String(raw[1])); - default: - throw Exception("unreachable"); - } - } - - @protected - CursorShape dco_decode_cursor_shape(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return CursorShape.values[raw as int]; - } - - @protected - CursorState dco_decode_cursor_state(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return CursorState( - col: dco_decode_u_16(arr[0]), - row: dco_decode_u_16(arr[1]), - shape: dco_decode_cursor_shape(arr[2]), - visible: dco_decode_bool(arr[3]), - ); - } - - @protected - double dco_decode_f_64(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as double; - } - - @protected - int dco_decode_i_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - List dco_decode_list_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_String).toList(); - } - - @protected - List dco_decode_list_cell_data(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_cell_data).toList(); - } - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint8List; - } - - @protected - List dco_decode_list_project_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_project_info).toList(); - } - - @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_record_string_string).toList(); - } - - @protected - String? dco_decode_opt_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_String(raw); - } - - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_selection_bounds(raw); - } - - @protected - ProjectInfo dco_decode_project_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 6) - throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); - return ProjectInfo( - id: dco_decode_String(arr[0]), - name: dco_decode_String(arr[1]), - path: dco_decode_String(arr[2]), - showInOverview: dco_decode_bool(arr[3]), - terminalIds: dco_decode_list_String(arr[4]), - terminalNames: dco_decode_Map_String_String_None(arr[5]), - ); - } - - @protected - (String, String) dco_decode_record_string_string(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 2) { - throw Exception('Expected 2 elements, got ${arr.length}'); - } - return (dco_decode_String(arr[0]), dco_decode_String(arr[1])); - } - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 3) - throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); - return ScrollInfo( - totalLines: dco_decode_u_32(arr[0]), - visibleLines: dco_decode_u_32(arr[1]), - displayOffset: dco_decode_u_32(arr[2]), - ); - } - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return SelectionBounds( - startCol: dco_decode_u_16(arr[0]), - startRow: dco_decode_i_32(arr[1]), - endCol: dco_decode_u_16(arr[2]), - endRow: dco_decode_i_32(arr[3]), - ); - } - - @protected - int dco_decode_u_16(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - int dco_decode_u_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - int dco_decode_u_8(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - void dco_decode_unit(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return; - } - - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_String(deserializer); - return AnyhowException(inner); - } - - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_record_string_string(deserializer); - return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); - } - - @protected - String sse_decode_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_prim_u_8_strict(deserializer); - return utf8.decoder.convert(inner); - } - - @protected - bool sse_decode_bool(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8() != 0; - } - - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_selection_bounds(deserializer)); - } - - @protected - CellData sse_decode_cell_data(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_character = sse_decode_String(deserializer); - var var_fg = sse_decode_u_32(deserializer); - var var_bg = sse_decode_u_32(deserializer); - var var_flags = sse_decode_u_8(deserializer); - return CellData( - character: var_character, - fg: var_fg, - bg: var_bg, - flags: var_flags, - ); - } - - @protected - ConnectionStatus sse_decode_connection_status(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var tag_ = sse_decode_i_32(deserializer); - switch (tag_) { - case 0: - return ConnectionStatus_Disconnected(); - case 1: - return ConnectionStatus_Connecting(); - case 2: - return ConnectionStatus_Connected(); - case 3: - return ConnectionStatus_Pairing(); - case 4: - var var_message = sse_decode_String(deserializer); - return ConnectionStatus_Error(message: var_message); - default: - throw UnimplementedError(''); - } - } - - @protected - CursorShape sse_decode_cursor_shape(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_i_32(deserializer); - return CursorShape.values[inner]; - } - - @protected - CursorState sse_decode_cursor_state(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_col = sse_decode_u_16(deserializer); - var var_row = sse_decode_u_16(deserializer); - var var_shape = sse_decode_cursor_shape(deserializer); - var var_visible = sse_decode_bool(deserializer); - return CursorState( - col: var_col, - row: var_row, - shape: var_shape, - visible: var_visible, - ); - } - - @protected - double sse_decode_f_64(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getFloat64(); - } - - @protected - int sse_decode_i_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getInt32(); - } - - @protected - List sse_decode_list_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_String(deserializer)); - } - return ans_; - } - - @protected - List sse_decode_list_cell_data(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_cell_data(deserializer)); - } - return ans_; - } - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint8List(len_); - } - - @protected - List sse_decode_list_project_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_project_info(deserializer)); - } - return ans_; - } - - @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = <(String, String)>[]; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_record_string_string(deserializer)); - } - return ans_; - } - - @protected - String? sse_decode_opt_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_String(deserializer)); - } else { - return null; - } - } - - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_selection_bounds(deserializer)); - } else { - return null; - } - } - - @protected - ProjectInfo sse_decode_project_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_id = sse_decode_String(deserializer); - var var_name = sse_decode_String(deserializer); - var var_path = sse_decode_String(deserializer); - var var_showInOverview = sse_decode_bool(deserializer); - var var_terminalIds = sse_decode_list_String(deserializer); - var var_terminalNames = sse_decode_Map_String_String_None(deserializer); - return ProjectInfo( - id: var_id, - name: var_name, - path: var_path, - showInOverview: var_showInOverview, - terminalIds: var_terminalIds, - terminalNames: var_terminalNames, - ); - } - - @protected - (String, String) sse_decode_record_string_string( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_field0 = sse_decode_String(deserializer); - var var_field1 = sse_decode_String(deserializer); - return (var_field0, var_field1); - } - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_totalLines = sse_decode_u_32(deserializer); - var var_visibleLines = sse_decode_u_32(deserializer); - var var_displayOffset = sse_decode_u_32(deserializer); - return ScrollInfo( - totalLines: var_totalLines, - visibleLines: var_visibleLines, - displayOffset: var_displayOffset, - ); - } - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_startCol = sse_decode_u_16(deserializer); - var var_startRow = sse_decode_i_32(deserializer); - var var_endCol = sse_decode_u_16(deserializer); - var var_endRow = sse_decode_i_32(deserializer); - return SelectionBounds( - startCol: var_startCol, - startRow: var_startRow, - endCol: var_endCol, - endRow: var_endRow, - ); - } - - @protected - int sse_decode_u_16(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint16(); - } - - @protected - int sse_decode_u_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint32(); - } - - @protected - int sse_decode_u_8(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8(); - } - - @protected - void sse_decode_unit(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - } - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.message, serializer); - } - - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_record_string_string( - self.entries.map((e) => (e.key, e.value)).toList(), - serializer, - ); - } - - @protected - void sse_encode_String(String self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); - } - - @protected - void sse_encode_bool(bool self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self ? 1 : 0); - } - - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_selection_bounds(self, serializer); - } - - @protected - void sse_encode_cell_data(CellData self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.character, serializer); - sse_encode_u_32(self.fg, serializer); - sse_encode_u_32(self.bg, serializer); - sse_encode_u_8(self.flags, serializer); - } - - @protected - void sse_encode_connection_status( - ConnectionStatus self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - switch (self) { - case ConnectionStatus_Disconnected(): - sse_encode_i_32(0, serializer); - case ConnectionStatus_Connecting(): - sse_encode_i_32(1, serializer); - case ConnectionStatus_Connected(): - sse_encode_i_32(2, serializer); - case ConnectionStatus_Pairing(): - sse_encode_i_32(3, serializer); - case ConnectionStatus_Error(message: final message): - sse_encode_i_32(4, serializer); - sse_encode_String(message, serializer); - } - } - - @protected - void sse_encode_cursor_shape(CursorShape self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.index, serializer); - } - - @protected - void sse_encode_cursor_state(CursorState self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_16(self.col, serializer); - sse_encode_u_16(self.row, serializer); - sse_encode_cursor_shape(self.shape, serializer); - sse_encode_bool(self.visible, serializer); - } - - @protected - void sse_encode_f_64(double self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putFloat64(self); - } - - @protected - void sse_encode_i_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putInt32(self); - } - - @protected - void sse_encode_list_String(List self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_String(item, serializer); - } - } - - @protected - void sse_encode_list_cell_data( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_cell_data(item, serializer); - } - } - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint8List(self); - } - - @protected - void sse_encode_list_project_info( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_project_info(item, serializer); - } - } - - @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_record_string_string(item, serializer); - } - } - - @protected - void sse_encode_opt_String(String? self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_String(self, serializer); - } - } - - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_selection_bounds(self, serializer); - } - } - - @protected - void sse_encode_project_info(ProjectInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.id, serializer); - sse_encode_String(self.name, serializer); - sse_encode_String(self.path, serializer); - sse_encode_bool(self.showInOverview, serializer); - sse_encode_list_String(self.terminalIds, serializer); - sse_encode_Map_String_String_None(self.terminalNames, serializer); - } - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.$1, serializer); - sse_encode_String(self.$2, serializer); - } - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_32(self.totalLines, serializer); - sse_encode_u_32(self.visibleLines, serializer); - sse_encode_u_32(self.displayOffset, serializer); - } - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_16(self.startCol, serializer); - sse_encode_i_32(self.startRow, serializer); - sse_encode_u_16(self.endCol, serializer); - sse_encode_i_32(self.endRow, serializer); - } - - @protected - void sse_encode_u_16(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint16(self); - } - - @protected - void sse_encode_u_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint32(self); - } - - @protected - void sse_encode_u_8(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self); - } - - @protected - void sse_encode_unit(void self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - } -} diff --git a/mobile/lib/src/rust/frb_generated.io.dart b/mobile/lib/src/rust/frb_generated.io.dart deleted file mode 100644 index f6b9363a..00000000 --- a/mobile/lib/src/rust/frb_generated.io.dart +++ /dev/null @@ -1,311 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -import 'api/connection.dart'; -import 'api/state.dart'; -import 'api/terminal.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi' as ffi; -import 'frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; - -abstract class RustLibApiImplPlatform extends BaseApiImpl { - RustLibApiImplPlatform({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - - @protected - Map dco_decode_Map_String_String_None(dynamic raw); - - @protected - String dco_decode_String(dynamic raw); - - @protected - bool dco_decode_bool(dynamic raw); - - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); - - @protected - CellData dco_decode_cell_data(dynamic raw); - - @protected - ConnectionStatus dco_decode_connection_status(dynamic raw); - - @protected - CursorShape dco_decode_cursor_shape(dynamic raw); - - @protected - CursorState dco_decode_cursor_state(dynamic raw); - - @protected - double dco_decode_f_64(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - List dco_decode_list_String(dynamic raw); - - @protected - List dco_decode_list_cell_data(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); - - @protected - List dco_decode_list_project_info(dynamic raw); - - @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw); - - @protected - String? dco_decode_opt_String(dynamic raw); - - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); - - @protected - ProjectInfo dco_decode_project_info(dynamic raw); - - @protected - (String, String) dco_decode_record_string_string(dynamic raw); - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw); - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw); - - @protected - int dco_decode_u_16(dynamic raw); - - @protected - int dco_decode_u_32(dynamic raw); - - @protected - int dco_decode_u_8(dynamic raw); - - @protected - void dco_decode_unit(dynamic raw); - - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - CellData sse_decode_cell_data(SseDeserializer deserializer); - - @protected - ConnectionStatus sse_decode_connection_status(SseDeserializer deserializer); - - @protected - CursorShape sse_decode_cursor_shape(SseDeserializer deserializer); - - @protected - CursorState sse_decode_cursor_state(SseDeserializer deserializer); - - @protected - double sse_decode_f_64(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - List sse_decode_list_String(SseDeserializer deserializer); - - @protected - List sse_decode_list_cell_data(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - List sse_decode_list_project_info(SseDeserializer deserializer); - - @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ); - - @protected - String? sse_decode_opt_String(SseDeserializer deserializer); - - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - ProjectInfo sse_decode_project_info(SseDeserializer deserializer); - - @protected - (String, String) sse_decode_record_string_string( - SseDeserializer deserializer, - ); - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); - - @protected - int sse_decode_u_16(SseDeserializer deserializer); - - @protected - int sse_decode_u_32(SseDeserializer deserializer); - - @protected - int sse_decode_u_8(SseDeserializer deserializer); - - @protected - void sse_decode_unit(SseDeserializer deserializer); - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ); - - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_bool(bool self, SseSerializer serializer); - - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_cell_data(CellData self, SseSerializer serializer); - - @protected - void sse_encode_connection_status( - ConnectionStatus self, - SseSerializer serializer, - ); - - @protected - void sse_encode_cursor_shape(CursorShape self, SseSerializer serializer); - - @protected - void sse_encode_cursor_state(CursorState self, SseSerializer serializer); - - @protected - void sse_encode_f_64(double self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_String(List self, SseSerializer serializer); - - @protected - void sse_encode_list_cell_data(List self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_project_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_String(String? self, SseSerializer serializer); - - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ); - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_u_16(int self, SseSerializer serializer); - - @protected - void sse_encode_u_32(int self, SseSerializer serializer); - - @protected - void sse_encode_u_8(int self, SseSerializer serializer); - - @protected - void sse_encode_unit(void self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => - RustLibWire(lib.ffiDynamicLibrary); - - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - RustLibWire(ffi.DynamicLibrary dynamicLibrary) - : _lookup = dynamicLibrary.lookup; -} diff --git a/mobile/lib/src/rust/frb_generated.web.dart b/mobile/lib/src/rust/frb_generated.web.dart deleted file mode 100644 index b0f66d8d..00000000 --- a/mobile/lib/src/rust/frb_generated.web.dart +++ /dev/null @@ -1,311 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -// Static analysis wrongly picks the IO variant, thus ignore this -// ignore_for_file: argument_type_not_assignable - -import 'api/connection.dart'; -import 'api/state.dart'; -import 'api/terminal.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; - -abstract class RustLibApiImplPlatform extends BaseApiImpl { - RustLibApiImplPlatform({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - - @protected - Map dco_decode_Map_String_String_None(dynamic raw); - - @protected - String dco_decode_String(dynamic raw); - - @protected - bool dco_decode_bool(dynamic raw); - - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); - - @protected - CellData dco_decode_cell_data(dynamic raw); - - @protected - ConnectionStatus dco_decode_connection_status(dynamic raw); - - @protected - CursorShape dco_decode_cursor_shape(dynamic raw); - - @protected - CursorState dco_decode_cursor_state(dynamic raw); - - @protected - double dco_decode_f_64(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - List dco_decode_list_String(dynamic raw); - - @protected - List dco_decode_list_cell_data(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); - - @protected - List dco_decode_list_project_info(dynamic raw); - - @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw); - - @protected - String? dco_decode_opt_String(dynamic raw); - - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); - - @protected - ProjectInfo dco_decode_project_info(dynamic raw); - - @protected - (String, String) dco_decode_record_string_string(dynamic raw); - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw); - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw); - - @protected - int dco_decode_u_16(dynamic raw); - - @protected - int dco_decode_u_32(dynamic raw); - - @protected - int dco_decode_u_8(dynamic raw); - - @protected - void dco_decode_unit(dynamic raw); - - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - CellData sse_decode_cell_data(SseDeserializer deserializer); - - @protected - ConnectionStatus sse_decode_connection_status(SseDeserializer deserializer); - - @protected - CursorShape sse_decode_cursor_shape(SseDeserializer deserializer); - - @protected - CursorState sse_decode_cursor_state(SseDeserializer deserializer); - - @protected - double sse_decode_f_64(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - List sse_decode_list_String(SseDeserializer deserializer); - - @protected - List sse_decode_list_cell_data(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - List sse_decode_list_project_info(SseDeserializer deserializer); - - @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ); - - @protected - String? sse_decode_opt_String(SseDeserializer deserializer); - - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - ProjectInfo sse_decode_project_info(SseDeserializer deserializer); - - @protected - (String, String) sse_decode_record_string_string( - SseDeserializer deserializer, - ); - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); - - @protected - int sse_decode_u_16(SseDeserializer deserializer); - - @protected - int sse_decode_u_32(SseDeserializer deserializer); - - @protected - int sse_decode_u_8(SseDeserializer deserializer); - - @protected - void sse_decode_unit(SseDeserializer deserializer); - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ); - - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_bool(bool self, SseSerializer serializer); - - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_cell_data(CellData self, SseSerializer serializer); - - @protected - void sse_encode_connection_status( - ConnectionStatus self, - SseSerializer serializer, - ); - - @protected - void sse_encode_cursor_shape(CursorShape self, SseSerializer serializer); - - @protected - void sse_encode_cursor_state(CursorState self, SseSerializer serializer); - - @protected - void sse_encode_f_64(double self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_String(List self, SseSerializer serializer); - - @protected - void sse_encode_list_cell_data(List self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_project_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_String(String? self, SseSerializer serializer); - - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ); - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_u_16(int self, SseSerializer serializer); - - @protected - void sse_encode_u_32(int self, SseSerializer serializer); - - @protected - void sse_encode_u_8(int self, SseSerializer serializer); - - @protected - void sse_encode_unit(void self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - RustLibWire.fromExternalLibrary(ExternalLibrary lib); -} - -@JS('wasm_bindgen') -external RustLibWasmModule get wasmModule; - -@JS() -@anonymous -extension type RustLibWasmModule._(JSObject _) implements JSObject {} diff --git a/mobile/lib/src/screens/pairing_screen.dart b/mobile/lib/src/screens/pairing_screen.dart deleted file mode 100644 index 0abb5d68..00000000 --- a/mobile/lib/src/screens/pairing_screen.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../providers/connection_provider.dart'; -import '../widgets/status_indicator.dart'; -import '../../src/rust/api/connection.dart'; - -class PairingScreen extends StatefulWidget { - const PairingScreen({super.key}); - - @override - State createState() => _PairingScreenState(); -} - -class _PairingScreenState extends State { - final _codeController = TextEditingController(); - bool _submitting = false; - - @override - void dispose() { - _codeController.dispose(); - super.dispose(); - } - - Future _submitCode() async { - final code = _codeController.text.trim(); - if (code.isEmpty) return; - - setState(() => _submitting = true); - final provider = context.read(); - await provider.pair(code); - if (mounted) { - setState(() => _submitting = false); - } - } - - @override - Widget build(BuildContext context) { - final provider = context.watch(); - final showCodeInput = provider.isPairing; - final isError = provider.status is ConnectionStatus_Error; - final errorMessage = isError - ? (provider.status as ConnectionStatus_Error).message - : null; - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => provider.disconnect(), - ), - title: Text(provider.activeServer?.displayName ?? 'Connecting'), - ), - body: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: StatusIndicator(status: provider.status), - ), - const SizedBox(height: 32), - if (!showCodeInput && !isError) ...[ - const Center(child: CircularProgressIndicator()), - const SizedBox(height: 16), - Text( - 'Connecting to server...', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - if (showCodeInput) ...[ - Text( - 'Enter Pairing Code', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'Check the Okena desktop app for the pairing code.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - const SizedBox(height: 24), - TextField( - controller: _codeController, - decoration: const InputDecoration( - labelText: 'XXXX-XXXX', - border: OutlineInputBorder(), - ), - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 24, - letterSpacing: 4, - fontFamily: 'monospace', - ), - textCapitalization: TextCapitalization.characters, - keyboardType: TextInputType.text, - autofocus: true, - onSubmitted: (_) => _submitCode(), - ), - const SizedBox(height: 16), - FilledButton( - onPressed: _submitting ? null : _submitCode, - child: _submitting - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Pair'), - ), - ], - if (isError) ...[ - Icon(Icons.error_outline, size: 48, color: Colors.red.shade300), - const SizedBox(height: 16), - Text( - errorMessage ?? 'Connection failed', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.red.shade300), - ), - const SizedBox(height: 24), - OutlinedButton( - onPressed: () { - final server = provider.activeServer; - if (server != null) { - provider.disconnect(); - provider.connectTo(server); - } - }, - child: const Text('Retry'), - ), - ], - ], - ), - ), - ); - } -} diff --git a/mobile/lib/src/screens/server_list_screen.dart b/mobile/lib/src/screens/server_list_screen.dart deleted file mode 100644 index 0e823db1..00000000 --- a/mobile/lib/src/screens/server_list_screen.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../models/saved_server.dart'; -import '../providers/connection_provider.dart'; - -class ServerListScreen extends StatelessWidget { - const ServerListScreen({super.key}); - - @override - Widget build(BuildContext context) { - final provider = context.watch(); - - return Scaffold( - appBar: AppBar( - title: const Text('Okena'), - centerTitle: true, - ), - body: provider.servers.isEmpty - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.terminal, - size: 64, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'No servers yet', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text( - 'Add a server to get started', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ], - ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: provider.servers.length, - itemBuilder: (context, index) { - final server = provider.servers[index]; - return Dismissible( - key: ValueKey('${server.host}:${server.port}'), - direction: DismissDirection.endToStart, - background: Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 24), - color: Colors.red, - child: const Icon(Icons.delete, color: Colors.white), - ), - onDismissed: (_) => provider.removeServer(server), - child: ListTile( - leading: const Icon(Icons.dns), - title: Text(server.displayName), - subtitle: server.label != null - ? Text('${server.host}:${server.port}') - : null, - trailing: const Icon(Icons.chevron_right), - onTap: () => provider.connectTo(server), - ), - ); - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showAddServerSheet(context), - child: const Icon(Icons.add), - ), - ); - } - - void _showAddServerSheet(BuildContext context) { - final hostController = TextEditingController(); - final portController = TextEditingController(text: '19100'); - final provider = context.read(); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (ctx) => Padding( - padding: EdgeInsets.only( - left: 24, - right: 24, - top: 24, - bottom: MediaQuery.of(ctx).viewInsets.bottom + 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Add Server', - style: Theme.of(ctx).textTheme.titleLarge, - ), - const SizedBox(height: 16), - TextField( - controller: hostController, - decoration: const InputDecoration( - labelText: 'Host', - border: OutlineInputBorder(), - hintText: '192.168.1.100', - ), - autofocus: true, - keyboardType: TextInputType.url, - ), - const SizedBox(height: 12), - TextField( - controller: portController, - decoration: const InputDecoration( - labelText: 'Port', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 16), - FilledButton( - onPressed: () { - final host = hostController.text.trim(); - final port = - int.tryParse(portController.text.trim()) ?? 19100; - if (host.isNotEmpty) { - provider.addServer(SavedServer(host: host, port: port)); - Navigator.of(ctx).pop(); - } - }, - child: const Text('Add'), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/src/screens/workspace_screen.dart b/mobile/lib/src/screens/workspace_screen.dart deleted file mode 100644 index f708dade..00000000 --- a/mobile/lib/src/screens/workspace_screen.dart +++ /dev/null @@ -1,332 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../providers/connection_provider.dart'; -import '../providers/workspace_provider.dart'; -import '../rust/api/state.dart' as state_ffi; -import '../widgets/project_drawer.dart'; -import '../widgets/key_toolbar.dart'; -import '../widgets/terminal_view.dart'; - -class WorkspaceScreen extends StatelessWidget { - const WorkspaceScreen({super.key}); - - @override - Widget build(BuildContext context) { - final workspace = context.watch(); - final connection = context.watch(); - final project = workspace.selectedProject; - final connId = connection.connId; - final selectedTerminalId = workspace.selectedTerminalId; - - return Scaffold( - appBar: AppBar( - title: _ProjectSwitcher( - projects: workspace.projects, - selectedProjectId: workspace.selectedProjectId, - onSelect: (id) => workspace.selectProject(id), - ), - leading: Builder( - builder: (ctx) => IconButton( - icon: const Icon(Icons.menu), - onPressed: () => Scaffold.of(ctx).openDrawer(), - ), - ), - actions: [ - // Connection quality indicator - if (connId != null) - Padding( - padding: const EdgeInsets.only(right: 4), - child: _ConnectionDot( - secondsSinceActivity: workspace.secondsSinceActivity, - ), - ), - if (connId != null && project != null) - IconButton( - icon: const Icon(Icons.add), - tooltip: 'New Terminal', - onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); - }, - ), - ], - ), - drawer: const ProjectDrawer(), - body: connId == null || project == null - ? const Center(child: Text('No project selected')) - : selectedTerminalId == null - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'No terminals', - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); - }, - icon: const Icon(Icons.add), - label: const Text('New Terminal'), - ), - ], - ), - ) - : Column( - children: [ - if (project.terminalIds.length > 1) - _TerminalTabBar( - terminalIds: project.terminalIds, - terminalNames: project.terminalNames, - selectedTerminalId: selectedTerminalId, - projectId: project.id, - connId: connId, - onSelect: (id) => workspace.selectTerminal(id), - ), - Expanded( - child: TerminalView( - connId: connId, - terminalId: selectedTerminalId, - onTerminalSwipe: (direction) { - final ids = project.terminalIds; - if (ids.length <= 1) return; - final idx = ids.indexOf(selectedTerminalId); - if (idx < 0) return; - final newIdx = (idx + direction).clamp(0, ids.length - 1); - if (newIdx != idx) { - workspace.selectTerminal(ids[newIdx]); - } - }, - ), - ), - KeyToolbar( - connId: connId, - terminalId: selectedTerminalId, - ), - ], - ), - ); - } -} - -/// Tappable project name in AppBar that opens a dropdown to switch projects. -class _ProjectSwitcher extends StatelessWidget { - final List projects; - final String? selectedProjectId; - final ValueChanged onSelect; - - const _ProjectSwitcher({ - required this.projects, - required this.selectedProjectId, - required this.onSelect, - }); - - @override - Widget build(BuildContext context) { - final selected = projects - .where((p) => p.id == selectedProjectId) - .firstOrNull ?? - projects.firstOrNull; - final name = selected?.name ?? 'No Project'; - - if (projects.length <= 1) { - return Text(name); - } - - return GestureDetector( - onTap: () => _showProjectMenu(context), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - name, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 4), - const Icon(Icons.arrow_drop_down, size: 20), - ], - ), - ); - } - - void _showProjectMenu(BuildContext context) { - final RenderBox button = context.findRenderObject() as RenderBox; - final overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(Offset(0, button.size.height), ancestor: overlay), - button.localToGlobal(button.size.bottomRight(Offset.zero), - ancestor: overlay), - ), - Offset.zero & overlay.size, - ); - - showMenu( - context: context, - position: position, - items: projects.map((p) { - return PopupMenuItem( - value: p.id, - child: Row( - children: [ - Icon( - Icons.folder, - size: 18, - color: p.id == selectedProjectId - ? Theme.of(context).colorScheme.primary - : null, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - p.name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: p.id == selectedProjectId - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ), - ], - ), - ); - }).toList(), - ).then((value) { - if (value != null) { - onSelect(value); - } - }); - } -} - -/// Horizontal tab bar showing terminals in the current project. -class _TerminalTabBar extends StatelessWidget { - final List terminalIds; - final Map terminalNames; - final String selectedTerminalId; - final String projectId; - final String connId; - final ValueChanged onSelect; - - const _TerminalTabBar({ - required this.terminalIds, - required this.terminalNames, - required this.selectedTerminalId, - required this.projectId, - required this.connId, - required this.onSelect, - }); - - @override - Widget build(BuildContext context) { - return Container( - height: 36, - color: const Color(0xFF252526), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: terminalIds.length, - padding: const EdgeInsets.symmetric(horizontal: 4), - itemBuilder: (context, index) { - final tid = terminalIds[index]; - final isSelected = tid == selectedTerminalId; - final name = terminalNames[tid] ?? 'Terminal ${index + 1}'; - - return GestureDetector( - onTap: () => onSelect(tid), - onLongPress: () => _showCloseDialog(context, tid, name), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF3C3C3C) - : Colors.transparent, - borderRadius: BorderRadius.circular(4), - ), - alignment: Alignment.center, - child: Text( - name, - style: TextStyle( - color: isSelected ? Colors.white : Colors.white54, - fontSize: 12, - fontFamily: 'JetBrainsMono', - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ); - }, - ), - ); - } - - void _showCloseDialog( - BuildContext context, String terminalId, String name) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Close terminal'), - content: Text('Close "$name"?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(); - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - child: const Text('Close', - style: TextStyle(color: Colors.redAccent)), - ), - ], - ), - ); - } -} - -/// Small colored dot indicating connection quality. -class _ConnectionDot extends StatelessWidget { - final double secondsSinceActivity; - - const _ConnectionDot({required this.secondsSinceActivity}); - - @override - Widget build(BuildContext context) { - final Color color; - if (secondsSinceActivity < 3) { - color = Colors.green; - } else if (secondsSinceActivity < 10) { - color = Colors.orange; - } else { - color = Colors.red; - } - - return Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - ); - } -} diff --git a/mobile/lib/src/theme/app_theme.dart b/mobile/lib/src/theme/app_theme.dart deleted file mode 100644 index 02977931..00000000 --- a/mobile/lib/src/theme/app_theme.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:ui'; - -class TerminalTheme { - static const fontFamily = 'JetBrainsMono'; - static const defaultFontSize = 13.0; - static const lineHeightFactor = 1.2; - - static const bgColor = Color(0xFF1E1E1E); - static const fgColor = Color(0xFFCCCCCC); - static const cursorColor = Color(0xFFAEAFAD); - static const selectionColor = Color(0x40264F78); -} diff --git a/mobile/lib/src/widgets/key_toolbar.dart b/mobile/lib/src/widgets/key_toolbar.dart deleted file mode 100644 index 65b99eab..00000000 --- a/mobile/lib/src/widgets/key_toolbar.dart +++ /dev/null @@ -1,677 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../src/rust/api/state.dart' as state_ffi; -import '../../src/rust/api/terminal.dart' as ffi; - -const _kComposeHistoryKey = 'compose_history'; -const _kMaxHistory = 30; - -class KeyToolbar extends StatefulWidget { - final String connId; - final String? terminalId; - - const KeyToolbar({ - super.key, - required this.connId, - this.terminalId, - }); - - @override - State createState() => _KeyToolbarState(); -} - -class _KeyToolbarState extends State { - bool _ctrlActive = false; - bool _altActive = false; - - // Compose history - List _composeHistory = []; - - @override - void initState() { - super.initState(); - _loadComposeHistory(); - } - - Future _loadComposeHistory() async { - final prefs = await SharedPreferences.getInstance(); - final history = prefs.getStringList(_kComposeHistoryKey); - if (history != null) { - setState(() => _composeHistory = history); - } - } - - Future _saveComposeHistory() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList(_kComposeHistoryKey, _composeHistory); - } - - void _addToHistory(String text) { - _composeHistory.remove(text); // dedup - _composeHistory.insert(0, text); - if (_composeHistory.length > _kMaxHistory) { - _composeHistory = _composeHistory.sublist(0, _kMaxHistory); - } - _saveComposeHistory(); - } - - void _sendSpecialKey(String key) { - final tid = widget.terminalId; - if (tid == null) return; - state_ffi.sendSpecialKey( - connId: widget.connId, - terminalId: tid, - key: key, - ); - } - - void _sendText(String text) { - final tid = widget.terminalId; - if (tid == null) return; - ffi.sendText( - connId: widget.connId, - terminalId: tid, - text: text, - ); - } - - void _sendCtrlChar(String letter) { - final code = letter.toLowerCase().codeUnitAt(0); - if (code >= 0x61 && code <= 0x7A) { - _sendText(String.fromCharCode(code - 0x60)); - } - } - - void _onCtrlTap() { - HapticFeedback.lightImpact(); - setState(() => _ctrlActive = !_ctrlActive); - } - - void _onAltTap() { - HapticFeedback.lightImpact(); - setState(() => _altActive = !_altActive); - } - - void _handleKey(String key) { - if (_ctrlActive) { - if (key.length == 1) { - final code = key.codeUnitAt(0); - if (code >= 0x61 && code <= 0x7A) { - _sendText(String.fromCharCode(code - 0x60)); - } else if (code >= 0x41 && code <= 0x5A) { - _sendText(String.fromCharCode(code - 0x40)); - } - } - setState(() => _ctrlActive = false); - } else { - _sendSpecialKey(key); - } - if (_altActive) { - setState(() => _altActive = false); - } - } - - void _pasteFromClipboard() async { - HapticFeedback.lightImpact(); - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data?.text != null && data!.text!.isNotEmpty) { - _sendText(data.text!); - } - } - - void _showCtrlGrid() { - const shortcuts = [ - ('C', 'kill'), - ('D', 'eof'), - ('Z', 'suspend'), - ('L', 'clear'), - ('A', 'bol'), - ('E', 'eol'), - ('R', 'search'), - ('W', 'del word'), - ('U', 'del left'), - ('K', 'del right'), - ('P', 'prev'), - ('N', 'next'), - ]; - - showModalBottomSheet( - context: context, - backgroundColor: const Color(0xFF252526), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 16, 12, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.only(left: 4, bottom: 12), - child: Text( - 'CTRL + ...', - style: TextStyle( - color: Colors.white54, - fontSize: 13, - fontFamily: 'JetBrainsMono', - ), - ), - ), - GridView.count( - shrinkWrap: true, - crossAxisCount: 4, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - childAspectRatio: 1.6, - physics: const NeverScrollableScrollPhysics(), - children: shortcuts.map((s) { - final (letter, label) = s; - return Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - HapticFeedback.lightImpact(); - _sendCtrlChar(letter); - Navigator.of(ctx).pop(); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '^$letter', - style: const TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - fontFamily: 'JetBrainsMono', - ), - ), - const SizedBox(height: 2), - Text( - label, - style: const TextStyle( - color: Colors.white38, - fontSize: 10, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - SizedBox(height: MediaQuery.of(ctx).padding.bottom), - ], - ), - ); - }, - ); - } - - void _showComposeSheet() { - final controller = TextEditingController(); - bool sendEnter = true; - int historyIdx = -1; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: const Color(0xFF252526), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return StatefulBuilder( - builder: (ctx, setSheetState) { - void submit() { - final text = controller.text; - if (text.isEmpty) return; - HapticFeedback.mediumImpact(); - _sendText(text); - if (sendEnter) { - _sendSpecialKey('Enter'); - } - _addToHistory(text); - Navigator.of(ctx).pop(); - } - - void historyUp() { - if (_composeHistory.isEmpty) return; - final newIdx = (historyIdx + 1).clamp(0, _composeHistory.length - 1); - if (newIdx != historyIdx) { - setSheetState(() { - historyIdx = newIdx; - controller.text = _composeHistory[historyIdx]; - controller.selection = TextSelection.collapsed( - offset: controller.text.length, - ); - }); - } - } - - void historyDown() { - if (historyIdx <= 0) { - setSheetState(() { - historyIdx = -1; - controller.text = ''; - }); - return; - } - setSheetState(() { - historyIdx--; - controller.text = _composeHistory[historyIdx]; - controller.selection = TextSelection.collapsed( - offset: controller.text.length, - ); - }); - } - - return Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: MediaQuery.of(ctx).viewInsets.bottom + 16, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Top row: history nav + enter toggle - Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_upward, size: 20), - color: _composeHistory.isNotEmpty - ? Colors.white70 - : Colors.white24, - onPressed: _composeHistory.isNotEmpty ? historyUp : null, - tooltip: 'Previous command', - visualDensity: VisualDensity.compact, - ), - IconButton( - icon: const Icon(Icons.arrow_downward, size: 20), - color: historyIdx > 0 ? Colors.white70 : Colors.white24, - onPressed: historyIdx >= 0 ? historyDown : null, - tooltip: 'Next command', - visualDensity: VisualDensity.compact, - ), - const Spacer(), - GestureDetector( - onTap: () { - setSheetState(() => sendEnter = !sendEnter); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: sendEnter - ? const Color(0xFF007ACC) - : const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.keyboard_return, - size: 14, - color: - sendEnter ? Colors.white : Colors.white54, - ), - const SizedBox(width: 4), - Text( - 'Enter', - style: TextStyle( - color: sendEnter - ? Colors.white - : Colors.white54, - fontSize: 12, - fontFamily: 'JetBrainsMono', - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: controller, - autofocus: true, - maxLines: null, - minLines: 3, - style: const TextStyle( - color: Colors.white, - fontFamily: 'JetBrainsMono', - fontSize: 14, - ), - decoration: InputDecoration( - hintText: 'Enter command...', - hintStyle: const TextStyle(color: Colors.white38), - filled: true, - fillColor: const Color(0xFF3C3C3C), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - suffixIcon: IconButton( - icon: const Icon(Icons.send, color: Color(0xFF007ACC)), - onPressed: submit, - ), - ), - ), - ], - ), - ); - }, - ); - }, - ); - } - - void _sendShiftTab() { - HapticFeedback.lightImpact(); - // Shift+Tab = reverse tab escape sequence - _sendText('\x1b[Z'); - } - - void _showMoreKeys() { - showModalBottomSheet( - context: context, - backgroundColor: const Color(0xFF252526), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 16, 12, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.only(left: 4, bottom: 12), - child: Text( - 'More keys', - style: TextStyle( - color: Colors.white54, - fontSize: 13, - fontFamily: 'JetBrainsMono', - ), - ), - ), - GridView.count( - shrinkWrap: true, - crossAxisCount: 4, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - childAspectRatio: 1.8, - physics: const NeverScrollableScrollPhysics(), - children: [ - _buildGridKey(ctx, 'TAB', () => _sendSpecialKey('Tab')), - _buildGridKey(ctx, 'ALT', () { - _onAltTap(); - Navigator.of(ctx).pop(); - }, toggle: _altActive), - _buildGridKey(ctx, 'DEL', () => _sendSpecialKey('Delete')), - _buildGridKey(ctx, 'HOME', () => _sendSpecialKey('Home')), - _buildGridKey(ctx, 'END', () => _sendSpecialKey('End')), - _buildGridKey(ctx, 'PG\u2191', () => _sendSpecialKey('PageUp')), - _buildGridKey(ctx, 'PG\u2193', () => _sendSpecialKey('PageDown')), - ], - ), - SizedBox(height: MediaQuery.of(ctx).padding.bottom), - ], - ), - ); - }, - ); - } - - Widget _buildGridKey(BuildContext ctx, String label, VoidCallback onTap, - {bool toggle = false}) { - return Material( - color: toggle ? const Color(0xFF007ACC) : const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - if (!toggle) Navigator.of(ctx).pop(); - }, - child: Center( - child: Text( - label, - style: TextStyle( - color: toggle ? Colors.white : Colors.white70, - fontSize: 14, - fontWeight: toggle ? FontWeight.bold : FontWeight.normal, - fontFamily: 'JetBrainsMono', - ), - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final modifierActive = _ctrlActive || _altActive; - return Container( - color: const Color(0xFF252526), - child: SafeArea( - top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (modifierActive) - Container(height: 2, color: const Color(0xFF007ACC)), - Padding( - padding: const EdgeInsets.fromLTRB(2, 4, 2, 4), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Left: two rows of action keys (~75% width) - Expanded( - flex: 3, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row(children: [ - _buildIconKey(Icons.edit_note, _showComposeSheet), - _buildIconKey(Icons.content_paste, _pasteFromClipboard), - _buildKey('ESC', () => _sendSpecialKey('Escape')), - _buildKey('ENT', () => _sendSpecialKey('Enter')), - ]), - Row(children: [ - _buildToggleKey( - 'CTRL', _ctrlActive, - onTap: _onCtrlTap, - onLongPress: _showCtrlGrid, - ), - _buildKey('TAB', () => _sendSpecialKey('Tab')), - _buildKey('S+T', _sendShiftTab), - _buildIconKey(Icons.more_horiz, _showMoreKeys), - ]), - ], - ), - ), - // Divider - Container( - width: 1, - color: Colors.white10, - margin: const EdgeInsets.symmetric(horizontal: 3, vertical: 6), - ), - // Right: arrow d-pad (~25% width) - Expanded( - flex: 1, - child: _buildDpad(), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildDpad() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Top row: spacer | ↑ | spacer - Row( - children: [ - const Expanded(child: SizedBox()), - Expanded(child: _buildDpadKey('\u2191', () => _handleKey('ArrowUp'))), - const Expanded(child: SizedBox()), - ], - ), - const SizedBox(height: 2), - // Bottom row: ← | ↓ | → - Row( - children: [ - Expanded(child: _buildDpadKey('\u2190', () => _handleKey('ArrowLeft'))), - Expanded(child: _buildDpadKey('\u2193', () => _handleKey('ArrowDown'))), - Expanded(child: _buildDpadKey('\u2192', () => _handleKey('ArrowRight'))), - ], - ), - ], - ), - ); - } - - Widget _buildDpadKey(String label, VoidCallback onTap) { - return Padding( - padding: const EdgeInsets.all(1), - child: Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - height: 30, - alignment: Alignment.center, - child: Text( - label, - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - fontFamily: 'JetBrainsMono', - ), - ), - ), - ), - ), - ); - } - - Widget _buildKey(String label, VoidCallback onTap) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.5, vertical: 5), - child: Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - alignment: Alignment.center, - child: Text( - label, - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - fontFamily: 'JetBrainsMono', - ), - ), - ), - ), - ), - ), - ); - } - - Widget _buildIconKey(IconData icon, VoidCallback onTap) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.5, vertical: 5), - child: Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - alignment: Alignment.center, - child: Icon(icon, color: Colors.white70, size: 16), - ), - ), - ), - ), - ); - } - - Widget _buildToggleKey( - String label, - bool active, { - required VoidCallback onTap, - VoidCallback? onLongPress, - }) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.5, vertical: 5), - child: Material( - color: active ? const Color(0xFF007ACC) : const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: onTap, - onLongPress: onLongPress, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - alignment: Alignment.center, - child: Text( - label, - style: TextStyle( - color: active ? Colors.white : Colors.white70, - fontSize: 12, - fontWeight: active ? FontWeight.bold : FontWeight.normal, - fontFamily: 'JetBrainsMono', - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/src/widgets/project_drawer.dart b/mobile/lib/src/widgets/project_drawer.dart deleted file mode 100644 index 5383fc2c..00000000 --- a/mobile/lib/src/widgets/project_drawer.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../providers/connection_provider.dart'; -import '../providers/workspace_provider.dart'; -import '../rust/api/state.dart' as state_ffi; -import 'status_indicator.dart'; - -class ProjectDrawer extends StatelessWidget { - const ProjectDrawer({super.key}); - - @override - Widget build(BuildContext context) { - final workspace = context.watch(); - final connection = context.watch(); - - return Drawer( - child: Column( - children: [ - DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Okena', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 4), - if (connection.activeServer != null) - Text( - connection.activeServer!.displayName, - style: Theme.of(context).textTheme.bodySmall, - ), - const Spacer(), - StatusIndicator(status: connection.status), - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: workspace.projects.length, - itemBuilder: (context, index) { - final project = workspace.projects[index]; - final isSelected = project.id == workspace.selectedProjectId; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: Icon( - Icons.folder, - color: isSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text(project.name), - subtitle: Text( - project.path, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - selected: isSelected, - onTap: () { - workspace.selectProject(project.id); - }, - ), - if (isSelected) ...[ - ...project.terminalIds.asMap().entries.map((entry) { - final idx = entry.key; - final tid = entry.value; - final isTerminalSelected = - tid == workspace.selectedTerminalId; - final name = - project.terminalNames[tid] ?? 'Terminal ${idx + 1}'; - return ListTile( - contentPadding: - const EdgeInsets.only(left: 56, right: 16), - leading: Icon( - Icons.terminal, - size: 20, - color: isTerminalSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text( - name, - style: TextStyle( - fontSize: 14, - color: isTerminalSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - ), - selected: isTerminalSelected, - dense: true, - onTap: () { - workspace.selectTerminal(tid); - Navigator.of(context).pop(); - }, - onLongPress: () { - _showCloseDialog( - context, - connId: connection.connId!, - projectId: project.id, - terminalId: tid, - name: name, - ); - }, - ); - }), - if (connection.connId != null) - ListTile( - contentPadding: - const EdgeInsets.only(left: 56, right: 16), - leading: Icon( - Icons.add, - size: 20, - color: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - title: Text( - 'New Terminal', - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - dense: true, - onTap: () { - state_ffi.createTerminal( - connId: connection.connId!, - projectId: project.id, - ); - Navigator.of(context).pop(); - }, - ), - ], - ], - ); - }, - ), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.link_off), - title: const Text('Disconnect'), - onTap: () { - Navigator.of(context).pop(); - connection.disconnect(); - }, - ), - ], - ), - ); - } - - void _showCloseDialog( - BuildContext context, { - required String connId, - required String projectId, - required String terminalId, - required String name, - }) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Close terminal'), - content: Text('Close "$name"?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(); // dialog - Navigator.of(context).pop(); // drawer - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - child: const Text('Close', - style: TextStyle(color: Colors.redAccent)), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/src/widgets/status_indicator.dart b/mobile/lib/src/widgets/status_indicator.dart deleted file mode 100644 index 65e2aaf3..00000000 --- a/mobile/lib/src/widgets/status_indicator.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../src/rust/api/connection.dart'; - -class StatusIndicator extends StatelessWidget { - final ConnectionStatus status; - - const StatusIndicator({super.key, required this.status}); - - @override - Widget build(BuildContext context) { - final (color, label) = switch (status) { - ConnectionStatus_Disconnected() => (Colors.grey, 'Disconnected'), - ConnectionStatus_Connecting() => (Colors.orange, 'Connecting'), - ConnectionStatus_Connected() => (Colors.green, 'Connected'), - ConnectionStatus_Pairing() => (Colors.blue, 'Pairing'), - ConnectionStatus_Error(:final message) => (Colors.red, 'Error: $message'), - }; - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 6), - Flexible( - child: Text( - label, - style: TextStyle(color: color, fontSize: 12), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } -} diff --git a/mobile/lib/src/widgets/terminal_painter.dart b/mobile/lib/src/widgets/terminal_painter.dart deleted file mode 100644 index f4292501..00000000 --- a/mobile/lib/src/widgets/terminal_painter.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -import '../../src/rust/api/terminal.dart'; -import '../theme/app_theme.dart'; - -// Flag bitmask constants matching Rust CellData.flags -const _kBold = 1; -const _kItalic = 2; -const _kUnderline = 4; -const _kStrikethrough = 8; -const _kInverse = 16; -const _kDim = 32; - -TextDecoration flagsToDecoration(int flags) { - final decorations = []; - if (flags & _kUnderline != 0) decorations.add(TextDecoration.underline); - if (flags & _kStrikethrough != 0) { - decorations.add(TextDecoration.lineThrough); - } - if (decorations.isEmpty) return TextDecoration.none; - return TextDecoration.combine(decorations); -} - -Color argbToColor(int argb) { - // Rust sends ARGB as u32: 0xAARRGGBB - return Color(argb); -} - -class TerminalPainter extends CustomPainter { - final List cells; - final CursorState cursor; - final int cols; - final int rows; - final double cellWidth; - final double cellHeight; - final double fontSize; - final String fontFamily; - final SelectionBounds? selection; - final ScrollInfo? scrollInfo; - - TerminalPainter({ - required this.cells, - required this.cursor, - required this.cols, - required this.rows, - required this.cellWidth, - required this.cellHeight, - required this.fontSize, - required this.fontFamily, - this.selection, - this.scrollInfo, - }); - - bool _isCellInSelection(int col, int row, SelectionBounds sel) { - // Selection bounds are in buffer coordinates; convert visual row - // to buffer row for comparison: buffer_row = visual_row - display_offset - final offset = scrollInfo?.displayOffset.toInt() ?? 0; - final bufferRow = row - offset; - - final sr = sel.startRow; - final er = sel.endRow; - final sc = sel.startCol; - final ec = sel.endCol; - - if (bufferRow < sr || bufferRow > er) return false; - if (sr == er) { - // Single line selection - return col >= sc && col <= ec; - } - if (bufferRow == sr) return col >= sc; - if (bufferRow == er) return col <= ec; - return true; // Middle line — fully selected - } - - @override - void paint(Canvas canvas, Size size) { - final bgPaint = Paint(); - - // Pass 1: Background rectangles + selection highlight - for (int i = 0; i < cells.length && i < cols * rows; i++) { - final cell = cells[i]; - final col = i % cols; - final row = i ~/ cols; - final x = col * cellWidth; - final y = row * cellHeight; - - var bgArgb = cell.bg; - var fgArgb = cell.fg; - if (cell.flags & _kInverse != 0) { - final tmp = bgArgb; - bgArgb = fgArgb; - fgArgb = tmp; - } - - final bgColor = argbToColor(bgArgb); - // Only draw non-default backgrounds - if (bgColor != TerminalTheme.bgColor && bgColor.a > 0) { - bgPaint.color = bgColor; - canvas.drawRect(Rect.fromLTWH(x, y, cellWidth, cellHeight), bgPaint); - } - - // Selection highlight - if (selection != null && _isCellInSelection(col, row, selection!)) { - bgPaint.color = const Color(0x40264F78); - canvas.drawRect(Rect.fromLTWH(x, y, cellWidth, cellHeight), bgPaint); - } - } - - // Pass 2: Text characters - for (int i = 0; i < cells.length && i < cols * rows; i++) { - final cell = cells[i]; - if (cell.character.isEmpty || cell.character == ' ') continue; - - final col = i % cols; - final row = i ~/ cols; - final x = col * cellWidth; - final y = row * cellHeight; - - var fgArgb = cell.fg; - var bgArgb = cell.bg; - if (cell.flags & _kInverse != 0) { - fgArgb = bgArgb; - // Don't need bgArgb here for text painting - } - - var fgColor = argbToColor(fgArgb); - if (cell.flags & _kDim != 0) { - fgColor = fgColor.withAlpha((fgColor.a * 0.5).round()); - } - - final tp = TextPainter( - text: TextSpan( - text: cell.character, - style: TextStyle( - fontFamily: fontFamily, - fontSize: fontSize, - color: fgColor, - fontWeight: - cell.flags & _kBold != 0 ? FontWeight.bold : FontWeight.normal, - fontStyle: - cell.flags & _kItalic != 0 ? FontStyle.italic : FontStyle.normal, - decoration: flagsToDecoration(cell.flags), - decorationColor: fgColor, - ), - ), - textDirection: ui.TextDirection.ltr, - )..layout(); - - // Center the character in the cell - final dx = x + (cellWidth - tp.width) / 2; - final dy = y + (cellHeight - tp.height) / 2; - tp.paint(canvas, Offset(dx, dy)); - } - - // Pass 3: Cursor - if (cursor.visible && cursor.col < cols && cursor.row < rows) { - final cx = cursor.col * cellWidth; - final cy = cursor.row * cellHeight; - final cursorPaint = Paint()..color = TerminalTheme.cursorColor; - - switch (cursor.shape) { - case CursorShape.block: - cursorPaint.color = TerminalTheme.cursorColor.withAlpha(128); - canvas.drawRect( - Rect.fromLTWH(cx, cy, cellWidth, cellHeight), - cursorPaint, - ); - case CursorShape.beam: - cursorPaint.strokeWidth = 2; - canvas.drawLine( - Offset(cx, cy), - Offset(cx, cy + cellHeight), - cursorPaint, - ); - case CursorShape.underline: - cursorPaint.strokeWidth = 2; - canvas.drawLine( - Offset(cx, cy + cellHeight - 1), - Offset(cx + cellWidth, cy + cellHeight - 1), - cursorPaint, - ); - } - } - - // Pass 4: Scroll indicator - if (scrollInfo != null && scrollInfo!.totalLines > scrollInfo!.visibleLines) { - final total = scrollInfo!.totalLines; - final visible = scrollInfo!.visibleLines; - final offset = scrollInfo!.displayOffset; - - if (total > 0 && visible > 0) { - final trackHeight = size.height; - final thumbHeight = (visible / total * trackHeight).clamp(20.0, trackHeight); - // offset=0 means at bottom, max offset = total - visible - final maxOffset = total - visible; - final thumbTop = maxOffset > 0 - ? (1.0 - offset / maxOffset) * (trackHeight - thumbHeight) - : 0.0; - - final scrollPaint = Paint() - ..color = const Color(0x40FFFFFF) - ..style = PaintingStyle.fill; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(size.width - 4, thumbTop, 3, thumbHeight), - const Radius.circular(1.5), - ), - scrollPaint, - ); - } - } - } - - @override - bool shouldRepaint(TerminalPainter oldDelegate) => true; -} diff --git a/mobile/lib/src/widgets/terminal_view.dart b/mobile/lib/src/widgets/terminal_view.dart deleted file mode 100644 index 712873d3..00000000 --- a/mobile/lib/src/widgets/terminal_view.dart +++ /dev/null @@ -1,518 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../../src/rust/api/terminal.dart' as ffi; -import '../../src/rust/api/state.dart' as state_ffi; -import '../theme/app_theme.dart'; -import 'terminal_painter.dart'; - -// Sentinel buffer: keeps spaces in the TextField so backspace always has -// something to delete. Without this, Android's soft keyboard backspace -// is a no-op on an empty field and onChanged never fires. -const _kSentinel = ' '; // 8 spaces - -class TerminalView extends StatefulWidget { - final String connId; - final String terminalId; - - /// Called when the user swipes horizontally to switch terminals. - /// direction: -1 = swipe right (prev), 1 = swipe left (next). - final ValueChanged? onTerminalSwipe; - - const TerminalView({ - super.key, - required this.connId, - required this.terminalId, - this.onTerminalSwipe, - }); - - @override - State createState() => _TerminalViewState(); -} - -class _TerminalViewState extends State { - List _cells = []; - ffi.CursorState _cursor = const ffi.CursorState( - col: 0, - row: 0, - shape: ffi.CursorShape.block, - visible: true, - ); - int _cols = 80; - int _rows = 24; - final double _fontSize = TerminalTheme.defaultFontSize; - double _cellWidth = 0; - double _cellHeight = 0; - Timer? _refreshTimer; - Timer? _resizeDebounce; - - // Keyboard input: TextField with its own FocusNode, delta-based tracking - late final FocusNode _inputFocusNode; - final _textController = TextEditingController(text: _kSentinel); - String _lastInputText = _kSentinel; - - // Scroll state - ffi.ScrollInfo _scrollInfo = const ffi.ScrollInfo( - totalLines: 0, - visibleLines: 0, - displayOffset: 0, - ); - double _scrollAccumulator = 0; - - // Selection state - bool _isSelecting = false; - ffi.SelectionBounds? _selection; - - // Gesture tracking for scroll vs swipe disambiguation - Offset? _dragStart; - - @override - void initState() { - super.initState(); - _inputFocusNode = FocusNode(onKeyEvent: _onKeyEvent); - _computeCellSize(); - _startRefreshLoop(); - } - - @override - void didUpdateWidget(TerminalView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.connId != widget.connId || - oldWidget.terminalId != widget.terminalId) { - _isSelecting = false; - _selection = null; - // Resize first so the grid matches the mobile viewport before fetching cells - if (_cols > 0 && _rows > 0) { - ffi.resizeTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - } - _fetchCells(); - } - } - - @override - void dispose() { - _refreshTimer?.cancel(); - _resizeDebounce?.cancel(); - _inputFocusNode.dispose(); - _textController.dispose(); - super.dispose(); - } - - void _computeCellSize() { - final tp = TextPainter( - text: TextSpan( - text: 'M', - style: TextStyle( - fontFamily: TerminalTheme.fontFamily, - fontSize: _fontSize, - ), - ), - textDirection: ui.TextDirection.ltr, - )..layout(); - - _cellWidth = tp.width; - _cellHeight = tp.height * TerminalTheme.lineHeightFactor; - } - - void _startRefreshLoop() { - _refreshTimer = Timer.periodic( - const Duration(milliseconds: 33), // ~30fps - (_) => _checkDirty(), - ); - } - - void _checkDirty() { - if (!mounted) return; - final dirty = - state_ffi.isDirty(connId: widget.connId, terminalId: widget.terminalId); - if (dirty) { - _fetchCells(); - } - } - - void _fetchCells() { - if (!mounted) return; - setState(() { - _cells = ffi.getVisibleCells( - connId: widget.connId, - terminalId: widget.terminalId, - ); - _cursor = ffi.getCursor( - connId: widget.connId, - terminalId: widget.terminalId, - ); - _scrollInfo = ffi.getScrollInfo( - connId: widget.connId, - terminalId: widget.terminalId, - ); - if (_isSelecting) { - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - } - }); - } - - void _onLayout(BoxConstraints constraints) { - if (_cellWidth <= 0 || _cellHeight <= 0) return; - - final newCols = (constraints.maxWidth / _cellWidth).floor().clamp(1, 500); - final newRows = (constraints.maxHeight / _cellHeight).floor().clamp(1, 200); - - if (newCols != _cols || newRows != _rows) { - _cols = newCols; - _rows = newRows; - _resizeDebounce?.cancel(); - _resizeDebounce = Timer(const Duration(milliseconds: 100), () { - _doResize(); - }); - // First resize is immediate so the terminal doesn't flash at wrong size - if (!_hasResized) { - _doResize(); - } - } - } - - bool _hasResized = false; - - void _doResize() { - _hasResized = true; - ffi.resizeTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - _fetchCells(); - } - - // --- Touch to cell conversion --- - (int col, int row) _touchToCell(Offset pos) { - final col = (pos.dx / _cellWidth).floor().clamp(0, _cols - 1); - final row = (pos.dy / _cellHeight).floor().clamp(0, _rows - 1); - return (col, row); - } - - // --- Scroll handling --- - void _onVerticalDragUpdate(DragUpdateDetails details) { - if (_cellHeight <= 0) return; - _scrollAccumulator += details.delta.dy; - final lines = (_scrollAccumulator / _cellHeight).truncate(); - if (lines != 0) { - _scrollAccumulator -= lines * _cellHeight; - ffi.scroll( - connId: widget.connId, - terminalId: widget.terminalId, - delta: lines, - ); - _fetchCells(); - } - } - - void _onVerticalDragEnd(DragEndDetails details) { - _scrollAccumulator = 0; - } - - // --- Selection handling --- - void _onLongPressStart(LongPressStartDetails details) { - final (col, row) = _touchToCell(details.localPosition); - ffi.startSelection( - connId: widget.connId, - terminalId: widget.terminalId, - col: col, - row: row, - ); - setState(() { - _isSelecting = true; - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - }); - } - - void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (!_isSelecting) return; - final (col, row) = _touchToCell(details.localPosition); - ffi.updateSelection( - connId: widget.connId, - terminalId: widget.terminalId, - col: col, - row: row, - ); - setState(() { - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - }); - } - - void _onLongPressEnd(LongPressEndDetails details) { - if (!_isSelecting) return; - _copySelectionAndClear(); - } - - void _onDoubleTap() { - // Use the last known tap position for word selection - if (_lastTapPosition != null) { - final (col, row) = _touchToCell(_lastTapPosition!); - ffi.startWordSelection( - connId: widget.connId, - terminalId: widget.terminalId, - col: col, - row: row, - ); - setState(() { - _isSelecting = true; - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - }); - _copySelectionAndClear(); - } - } - - Offset? _lastTapPosition; - - void _onTapDown(TapDownDetails details) { - _lastTapPosition = details.localPosition; - } - - void _onTap() { - // Clear selection on single tap if one exists - if (_isSelecting) { - ffi.clearSelection( - connId: widget.connId, - terminalId: widget.terminalId, - ); - setState(() { - _isSelecting = false; - _selection = null; - }); - return; - } - _inputFocusNode.requestFocus(); - } - - void _copySelectionAndClear() { - final text = ffi.getSelectedText( - connId: widget.connId, - terminalId: widget.terminalId, - ); - if (text != null && text.isNotEmpty) { - Clipboard.setData(ClipboardData(text: text)); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 1), - ), - ); - } - } - ffi.clearSelection( - connId: widget.connId, - terminalId: widget.terminalId, - ); - setState(() { - _isSelecting = false; - _selection = null; - }); - } - - // --- Horizontal swipe handling --- - void _onHorizontalDragStart(DragStartDetails details) { - _dragStart = details.localPosition; - } - - void _onHorizontalDragEnd(DragEndDetails details) { - final velocity = details.velocity.pixelsPerSecond; - if (velocity.dx.abs() > 300 && _dragStart != null) { - final direction = velocity.dx > 0 ? -1 : 1; // right swipe = prev, left = next - widget.onTerminalSwipe?.call(direction); - } - _dragStart = null; - } - - void _resetSentinel() { - _textController.text = _kSentinel; - _textController.selection = - TextSelection.collapsed(offset: _kSentinel.length); - _lastInputText = _kSentinel; - } - - void _onTextChanged(String newText) { - if (newText.length > _lastInputText.length) { - // Characters added — send the delta - final delta = newText.substring(_lastInputText.length); - ffi.sendText( - connId: widget.connId, - terminalId: widget.terminalId, - text: delta, - ); - } else if (newText.length < _lastInputText.length) { - // Characters deleted — user pressed backspace - final deletedCount = _lastInputText.length - newText.length; - for (int i = 0; i < deletedCount; i++) { - state_ffi.sendSpecialKey( - connId: widget.connId, - terminalId: widget.terminalId, - key: 'Backspace', - ); - } - } - _lastInputText = newText; - - // Re-seed if buffer runs low (backspace ate into the sentinel) - if (newText.length < 3) { - _resetSentinel(); - } - // Reset if too long to prevent unbounded growth - if (newText.length > 200) { - _resetSentinel(); - } - } - - KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - final key = event.logicalKey; - String? specialKey; - - if (key == LogicalKeyboardKey.enter) { - specialKey = 'Enter'; - } else if (key == LogicalKeyboardKey.backspace) { - specialKey = 'Backspace'; - } else if (key == LogicalKeyboardKey.arrowUp) { - specialKey = 'ArrowUp'; - } else if (key == LogicalKeyboardKey.arrowDown) { - specialKey = 'ArrowDown'; - } else if (key == LogicalKeyboardKey.arrowLeft) { - specialKey = 'ArrowLeft'; - } else if (key == LogicalKeyboardKey.arrowRight) { - specialKey = 'ArrowRight'; - } else if (key == LogicalKeyboardKey.home) { - specialKey = 'Home'; - } else if (key == LogicalKeyboardKey.end) { - specialKey = 'End'; - } else if (key == LogicalKeyboardKey.pageUp) { - specialKey = 'PageUp'; - } else if (key == LogicalKeyboardKey.pageDown) { - specialKey = 'PageDown'; - } else if (key == LogicalKeyboardKey.delete) { - specialKey = 'Delete'; - } else if (key == LogicalKeyboardKey.tab) { - specialKey = 'Tab'; - } else if (key == LogicalKeyboardKey.escape) { - specialKey = 'Escape'; - } - - if (specialKey != null) { - state_ffi.sendSpecialKey( - connId: widget.connId, - terminalId: widget.terminalId, - key: specialKey, - ); - return KeyEventResult.handled; - } - - // Let TextField handle normal character input via onChanged - return KeyEventResult.ignored; - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - _onLayout(constraints); - - return GestureDetector( - onTapDown: _onTapDown, - onTap: _onTap, - onDoubleTap: _onDoubleTap, - // Vertical drag for scrollback - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - // Long press for selection - onLongPressStart: _onLongPressStart, - onLongPressMoveUpdate: _onLongPressMoveUpdate, - onLongPressEnd: _onLongPressEnd, - // Horizontal drag for terminal switching - onHorizontalDragStart: _onHorizontalDragStart, - onHorizontalDragEnd: _onHorizontalDragEnd, - behavior: HitTestBehavior.opaque, - child: Container( - color: TerminalTheme.bgColor, - width: constraints.maxWidth, - height: constraints.maxHeight, - child: Stack( - children: [ - // Terminal canvas - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: TerminalPainter( - cells: _cells, - cursor: _cursor, - cols: _cols, - rows: _rows, - cellWidth: _cellWidth, - cellHeight: _cellHeight, - fontSize: _fontSize, - fontFamily: TerminalTheme.fontFamily, - selection: _selection, - scrollInfo: _scrollInfo, - ), - ), - // Transparent text field for soft keyboard input. - // Sized 1x1 in-layout (not off-screen) so Android shows the - // keyboard. Opacity > 0 to keep IME interaction working. - Positioned( - left: 0, - bottom: 0, - width: 1, - height: 1, - child: Opacity( - opacity: 0.01, - child: TextField( - focusNode: _inputFocusNode, - controller: _textController, - autofocus: false, - enableSuggestions: false, - autocorrect: false, - showCursor: false, - enableInteractiveSelection: false, - onChanged: _onTextChanged, - keyboardType: TextInputType.text, - textInputAction: TextInputAction.none, - decoration: const InputDecoration.collapsed( - hintText: '', - ), - style: const TextStyle( - color: Colors.transparent, - fontSize: 1, - height: 1, - ), - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/mobile/linux/.gitignore b/mobile/linux/.gitignore deleted file mode 100644 index d3896c98..00000000 --- a/mobile/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/mobile/linux/CMakeLists.txt b/mobile/linux/CMakeLists.txt deleted file mode 100644 index 9a39eb1b..00000000 --- a/mobile/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "mobile") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.mobile") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/mobile/linux/flutter/CMakeLists.txt b/mobile/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd0164..00000000 --- a/mobile/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/mobile/linux/flutter/generated_plugin_registrant.cc b/mobile/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d2..00000000 --- a/mobile/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/mobile/linux/flutter/generated_plugin_registrant.h b/mobile/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/mobile/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/mobile/linux/flutter/generated_plugins.cmake b/mobile/linux/flutter/generated_plugins.cmake deleted file mode 100644 index cc30e9e3..00000000 --- a/mobile/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - rust_lib_mobile -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/mobile/linux/runner/CMakeLists.txt b/mobile/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc7..00000000 --- a/mobile/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/mobile/linux/runner/main.cc b/mobile/linux/runner/main.cc deleted file mode 100644 index e7c5c543..00000000 --- a/mobile/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/mobile/linux/runner/my_application.cc b/mobile/linux/runner/my_application.cc deleted file mode 100644 index c03496ad..00000000 --- a/mobile/linux/runner/my_application.cc +++ /dev/null @@ -1,148 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "mobile"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "mobile"); - } - - gtk_window_set_default_size(window, 1280, 720); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, "flags", - G_APPLICATION_NON_UNIQUE, nullptr)); -} diff --git a/mobile/linux/runner/my_application.h b/mobile/linux/runner/my_application.h deleted file mode 100644 index db16367a..00000000 --- a/mobile/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, - my_application, - MY, - APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml deleted file mode 100644 index 75f20247..00000000 --- a/mobile/native/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "okena_mobile_native" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib", "staticlib"] - -[dependencies] -okena-core = { path = "../../crates/okena-core", features = ["client"] } -flutter_rust_bridge = "=2.11.1" -alacritty_terminal = "0.25" -tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "time"] } -tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } -log = "0.4" -anyhow = "1.0" -async-channel = "2.3" -futures = "0.3" -parking_lot = "0.12" -uuid = { version = "1.10", features = ["v4"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/mobile/native/src/api/connection.rs b/mobile/native/src/api/connection.rs deleted file mode 100644 index 76a62193..00000000 --- a/mobile/native/src/api/connection.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::client::manager::ConnectionManager; - -/// Connection status returned via FFI. -/// -/// Simplified version of okena_core's ConnectionStatus — collapses `Reconnecting { attempt }` -/// into `Connecting` since mobile UI doesn't need the attempt count. -#[derive(Debug, Clone)] -pub enum ConnectionStatus { - Disconnected, - Connecting, - Connected, - Pairing, - Error { message: String }, -} - -impl From for ConnectionStatus { - fn from(status: okena_core::client::ConnectionStatus) -> Self { - match status { - okena_core::client::ConnectionStatus::Disconnected => ConnectionStatus::Disconnected, - okena_core::client::ConnectionStatus::Connecting => ConnectionStatus::Connecting, - okena_core::client::ConnectionStatus::Connected => ConnectionStatus::Connected, - okena_core::client::ConnectionStatus::Pairing => ConnectionStatus::Pairing, - okena_core::client::ConnectionStatus::Reconnecting { .. } => { - ConnectionStatus::Connecting - } - okena_core::client::ConnectionStatus::Error(msg) => { - ConnectionStatus::Error { message: msg } - } - } - } -} - -/// Initialize the app (called once at startup). -#[flutter_rust_bridge::frb(init)] -pub fn init_app() { - flutter_rust_bridge::setup_default_user_utils(); - ConnectionManager::init(); -} - -/// Connect to an Okena remote server. Returns a connection ID. -/// If a saved token is provided, it will be used to skip pairing. -/// -/// TODO(mobile-tls): expose `tls: bool` here (and a Dart toggle + -/// fingerprint-verification UI) to let the app opt into TLS, then run -/// `flutter_rust_bridge_codegen generate`. The whole Rust stack below already -/// supports it — `add_connection` takes `tls` and the client pins the cert on -/// first connect — this FFI just hardcodes `false` until the codegen pass lands. -#[flutter_rust_bridge::frb(sync)] -pub fn connect(host: String, port: u16, saved_token: Option) -> String { - let mgr = ConnectionManager::get(); - let conn_id = mgr.add_connection(&host, port, saved_token, false); - mgr.connect(&conn_id); - conn_id -} - -/// Get the current auth token for a connection (if paired). -#[flutter_rust_bridge::frb(sync)] -pub fn get_token(conn_id: String) -> Option { - ConnectionManager::get().get_token(&conn_id) -} - -/// Pair with the server using a pairing code. -pub async fn pair(conn_id: String, code: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.pair(&conn_id, &code); - Ok(()) -} - -/// Disconnect from a server. -#[flutter_rust_bridge::frb(sync)] -pub fn disconnect(conn_id: String) { - ConnectionManager::get().disconnect(&conn_id); -} - -/// Get current connection status. -#[flutter_rust_bridge::frb(sync)] -pub fn connection_status(conn_id: String) -> ConnectionStatus { - ConnectionManager::get().get_status(&conn_id).into() -} - -/// Get seconds since last WS activity (terminal output). -/// Returns a large value if the connection doesn't exist. -#[flutter_rust_bridge::frb(sync)] -pub fn seconds_since_activity(conn_id: String) -> f64 { - ConnectionManager::get().seconds_since_activity(&conn_id) -} diff --git a/mobile/native/src/api/state.rs b/mobile/native/src/api/state.rs deleted file mode 100644 index 7ae29f3b..00000000 --- a/mobile/native/src/api/state.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use crate::client::manager::ConnectionManager; -use okena_core::api::ActionRequest; -use okena_core::client::{collect_state_terminal_ids, WsClientMessage}; -use okena_core::keys::SpecialKey; - -/// Flat FFI-friendly project info. -#[derive(Debug, Clone)] -pub struct ProjectInfo { - pub id: String, - pub name: String, - pub path: String, - pub show_in_overview: bool, - pub terminal_ids: Vec, - pub terminal_names: HashMap, -} - -/// Get all projects from the cached remote state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_projects(conn_id: String) -> Vec { - let mgr = ConnectionManager::get(); - let state = match mgr.get_state(&conn_id) { - Some(s) => s, - None => return Vec::new(), - }; - - state - .projects - .iter() - .map(|p| { - let terminal_ids = if let Some(ref layout) = p.layout { - let mut ids = Vec::new(); - collect_layout_ids_vec(layout, &mut ids); - ids - } else { - Vec::new() - }; - ProjectInfo { - id: p.id.clone(), - name: p.name.clone(), - path: p.path.clone(), - show_in_overview: p.show_in_overview, - terminal_ids, - terminal_names: p.terminal_names.clone(), - } - }) - .collect() -} - -/// Get the focused project ID from the cached remote state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_focused_project_id(conn_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.get_state(&conn_id) - .and_then(|s| s.focused_project_id.clone()) -} - -/// Check if a terminal has unprocessed output (dirty flag). -#[flutter_rust_bridge::frb(sync)] -pub fn is_dirty(conn_id: String, terminal_id: String) -> bool { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.is_dirty()) - .unwrap_or(false) -} - -/// Send a special key (e.g. "Enter", "Tab", "Escape") to a terminal. -/// -/// The key name is deserialized from JSON (e.g. `"Enter"`, `"CtrlC"`, `"ArrowUp"`). -pub async fn send_special_key( - conn_id: String, - terminal_id: String, - key: String, -) -> anyhow::Result<()> { - let special_key: SpecialKey = serde_json::from_value(serde_json::Value::String(key.clone())) - .map_err(|_| anyhow::anyhow!("Unknown special key: {}", key))?; - let text = String::from_utf8_lossy(special_key.to_bytes()).to_string(); - let mgr = ConnectionManager::get(); - mgr.send_ws_message( - &conn_id, - WsClientMessage::SendText { - terminal_id, - text, - }, - ); - Ok(()) -} - -fn collect_layout_ids_vec(node: &okena_core::api::ApiLayoutNode, ids: &mut Vec) { - match node { - okena_core::api::ApiLayoutNode::Terminal { terminal_id, .. } => { - if let Some(id) = terminal_id { - ids.push(id.clone()); - } - } - okena_core::api::ApiLayoutNode::Split { children, .. } - | okena_core::api::ApiLayoutNode::Tabs { children, .. } => { - for child in children { - collect_layout_ids_vec(child, ids); - } - } - } -} - -/// Get all terminal IDs from the cached remote state (flat list). -#[flutter_rust_bridge::frb(sync)] -pub fn get_all_terminal_ids(conn_id: String) -> Vec { - let mgr = ConnectionManager::get(); - match mgr.get_state(&conn_id) { - Some(state) => collect_state_terminal_ids(&state), - None => Vec::new(), - } -} - -/// Create a new terminal in the given project via POST /v1/actions. -pub async fn create_terminal(conn_id: String, project_id: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::CreateTerminal { project_id }, - ) - .await -} - -/// Close a terminal in the given project via POST /v1/actions. -pub async fn close_terminal( - conn_id: String, - project_id: String, - terminal_id: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::CloseTerminal { - project_id, - terminal_id, - }, - ) - .await -} - diff --git a/mobile/native/src/api/terminal.rs b/mobile/native/src/api/terminal.rs deleted file mode 100644 index 70d40a19..00000000 --- a/mobile/native/src/api/terminal.rs +++ /dev/null @@ -1,187 +0,0 @@ -use crate::client::manager::ConnectionManager; -use okena_core::client::WsClientMessage; -use okena_core::theme::DARK_THEME; - -/// Cell data for FFI transfer (flat, no pointers). -#[derive(Debug, Clone)] -pub struct CellData { - /// The character in this cell. - pub character: String, - /// Foreground color as ARGB packed u32. - pub fg: u32, - /// Background color as ARGB packed u32. - pub bg: u32, - /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). - pub flags: u8, -} - -/// Cursor shape variants. -#[derive(Debug, Clone)] -pub enum CursorShape { - Block, - Underline, - Beam, -} - -/// Cursor state for FFI transfer. -#[derive(Debug, Clone)] -pub struct CursorState { - pub col: u16, - pub row: u16, - pub shape: CursorShape, - pub visible: bool, -} - -/// Get the visible terminal cells for rendering. -#[flutter_rust_bridge::frb(sync)] -pub fn get_visible_cells(conn_id: String, terminal_id: String) -> Vec { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.get_visible_cells(&DARK_THEME) - }) - .unwrap_or_default() -} - -/// Get the current cursor state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_cursor(conn_id: String, terminal_id: String) -> CursorState { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.get_cursor()) - .unwrap_or(CursorState { - col: 0, - row: 0, - shape: CursorShape::Block, - visible: true, - }) -} - -/// Scroll info for FFI transfer. -#[derive(Debug, Clone)] -pub struct ScrollInfo { - pub total_lines: u32, - pub visible_lines: u32, - pub display_offset: u32, -} - -/// Selection bounds for FFI transfer. -#[derive(Debug, Clone)] -pub struct SelectionBounds { - pub start_col: u16, - pub start_row: i32, - pub end_col: u16, - pub end_row: i32, -} - -/// Scroll the terminal display (positive = up, negative = down). -#[flutter_rust_bridge::frb(sync)] -pub fn scroll(conn_id: String, terminal_id: String, delta: i32) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.scroll(delta); - }); -} - -/// Get scroll info: total lines, visible lines, display offset. -#[flutter_rust_bridge::frb(sync)] -pub fn get_scroll_info(conn_id: String, terminal_id: String) -> ScrollInfo { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - let (total, visible, offset) = holder.scroll_info(); - ScrollInfo { - total_lines: total as u32, - visible_lines: visible as u32, - display_offset: offset as u32, - } - }) - .unwrap_or(ScrollInfo { - total_lines: 0, - visible_lines: 0, - display_offset: 0, - }) -} - -/// Start a character-level selection at col/row. -#[flutter_rust_bridge::frb(sync)] -pub fn start_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.start_selection(col as usize, row as usize); - }); -} - -/// Start a word (semantic) selection at col/row. -#[flutter_rust_bridge::frb(sync)] -pub fn start_word_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.start_word_selection(col as usize, row as usize); - }); -} - -/// Extend the current selection to col/row. -#[flutter_rust_bridge::frb(sync)] -pub fn update_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.update_selection(col as usize, row as usize); - }); -} - -/// Clear the current selection. -#[flutter_rust_bridge::frb(sync)] -pub fn clear_selection(conn_id: String, terminal_id: String) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.clear_selection(); - }); -} - -/// Get the selected text, if any. -#[flutter_rust_bridge::frb(sync)] -pub fn get_selected_text(conn_id: String, terminal_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.get_selected_text() - }) - .flatten() -} - -/// Get selection bounds for rendering. -#[flutter_rust_bridge::frb(sync)] -pub fn get_selection_bounds(conn_id: String, terminal_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.selection_bounds().map(|((sc, sr), (ec, er))| SelectionBounds { - start_col: sc as u16, - start_row: sr, - end_col: ec as u16, - end_row: er, - }) - }) - .flatten() -} - -/// Send text input to a terminal. -pub async fn send_text(conn_id: String, terminal_id: String, text: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_ws_message( - &conn_id, - WsClientMessage::SendText { - terminal_id, - text, - }, - ); - Ok(()) -} - -/// Resize a terminal. -#[flutter_rust_bridge::frb(sync)] -pub fn resize_terminal( - conn_id: String, - terminal_id: String, - cols: u16, - rows: u16, -) { - let mgr = ConnectionManager::get(); - mgr.resize_terminal(&conn_id, &terminal_id, cols, rows); -} diff --git a/mobile/native/src/frb_generated.rs b/mobile/native/src/frb_generated.rs deleted file mode 100644 index b9b4ea32..00000000 --- a/mobile/native/src/frb_generated.rs +++ /dev/null @@ -1,1765 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -#![allow( - non_camel_case_types, - unused, - non_snake_case, - clippy::needless_return, - clippy::redundant_closure_call, - clippy::redundant_closure, - clippy::useless_conversion, - clippy::unit_arg, - clippy::unused_unit, - clippy::double_parens, - clippy::let_and_return, - clippy::too_many_arguments, - clippy::match_single_binding, - clippy::clone_on_copy, - clippy::let_unit_value, - clippy::deref_addrof, - clippy::explicit_auto_deref, - clippy::borrow_deref_ref, - clippy::needless_borrow -)] - -// Section: imports - -use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; -use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; -use flutter_rust_bridge::{Handler, IntoIntoDart}; - -// Section: boilerplate - -flutter_rust_bridge::frb_generated_boilerplate!( - default_stream_sink_codec = SseCodec, - default_rust_opaque = RustOpaqueMoi, - default_rust_auto_opaque = RustAutoOpaqueMoi, -); -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1973712882; - -// Section: executor - -flutter_rust_bridge::frb_generated_default_handler!(); - -// Section: wire_funcs - -fn wire__crate__api__terminal__clear_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "clear_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::clear_selection(api_conn_id, api_terminal_id); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__close_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "close_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::close_terminal( - api_conn_id, - api_project_id, - api_terminal_id, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__connection__connect_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "connect", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_host = ::sse_decode(&mut deserializer); - let api_port = ::sse_decode(&mut deserializer); - let api_saved_token = >::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::connection::connect( - api_host, - api_port, - api_saved_token, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__connection_status_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "connection_status", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::connection::connection_status(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__create_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "create_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::create_terminal(api_conn_id, api_project_id).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__connection__disconnect_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "disconnect", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::connection::disconnect(api_conn_id); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_all_terminal_ids_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_all_terminal_ids", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::get_all_terminal_ids(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_cursor_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_cursor", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_cursor( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_focused_project_id_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_focused_project_id", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::get_focused_project_id(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_projects_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_projects", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::state::get_projects(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_scroll_info_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_scroll_info", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_scroll_info( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_selected_text_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_selected_text", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selected_text( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_selection_bounds_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_selection_bounds", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selection_bounds( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__get_token_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_token", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::connection::get_token(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_visible_cells_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_visible_cells", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_visible_cells( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__init_app_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "init_app", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - deserializer.end(); - move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::connection::init_app(); - })?; - Ok(output_ok) - })()) - } - }, - ) -} -fn wire__crate__api__state__is_dirty_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "is_dirty", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::is_dirty(api_conn_id, api_terminal_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__pair_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "pair", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_code = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::connection::pair(api_conn_id, api_code).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__resize_terminal_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "resize_terminal", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_cols = ::sse_decode(&mut deserializer); - let api_rows = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::resize_terminal( - api_conn_id, - api_terminal_id, - api_cols, - api_rows, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__scroll_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "scroll", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_delta = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::scroll(api_conn_id, api_terminal_id, api_delta); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__seconds_since_activity_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "seconds_since_activity", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok( - crate::api::connection::seconds_since_activity(api_conn_id), - )?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__send_special_key_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_special_key", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_key = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::send_special_key( - api_conn_id, - api_terminal_id, - api_key, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__send_text_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_text", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_text = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::terminal::send_text(api_conn_id, api_terminal_id, api_text) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__start_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::start_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__start_word_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_word_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::start_word_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__update_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "update_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::update_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} - -// Section: dart2rust - -impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); - } -} - -impl SseDecode for std::collections::HashMap { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return inner.into_iter().collect(); - } -} - -impl SseDecode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return String::from_utf8(inner).unwrap(); - } -} - -impl SseDecode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() != 0 - } -} - -impl SseDecode for crate::api::terminal::CellData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_character = ::sse_decode(deserializer); - let mut var_fg = ::sse_decode(deserializer); - let mut var_bg = ::sse_decode(deserializer); - let mut var_flags = ::sse_decode(deserializer); - return crate::api::terminal::CellData { - character: var_character, - fg: var_fg, - bg: var_bg, - flags: var_flags, - }; - } -} - -impl SseDecode for crate::api::connection::ConnectionStatus { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut tag_ = ::sse_decode(deserializer); - match tag_ { - 0 => { - return crate::api::connection::ConnectionStatus::Disconnected; - } - 1 => { - return crate::api::connection::ConnectionStatus::Connecting; - } - 2 => { - return crate::api::connection::ConnectionStatus::Connected; - } - 3 => { - return crate::api::connection::ConnectionStatus::Pairing; - } - 4 => { - let mut var_message = ::sse_decode(deserializer); - return crate::api::connection::ConnectionStatus::Error { - message: var_message, - }; - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseDecode for crate::api::terminal::CursorShape { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return match inner { - 0 => crate::api::terminal::CursorShape::Block, - 1 => crate::api::terminal::CursorShape::Underline, - 2 => crate::api::terminal::CursorShape::Beam, - _ => unreachable!("Invalid variant for CursorShape: {}", inner), - }; - } -} - -impl SseDecode for crate::api::terminal::CursorState { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_col = ::sse_decode(deserializer); - let mut var_row = ::sse_decode(deserializer); - let mut var_shape = ::sse_decode(deserializer); - let mut var_visible = ::sse_decode(deserializer); - return crate::api::terminal::CursorState { - col: var_col, - row: var_row, - shape: var_shape, - visible: var_visible, - }; - } -} - -impl SseDecode for f64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_f64::().unwrap() - } -} - -impl SseDecode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i32::().unwrap() - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec<(String, String)> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(<(String, String)>::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode( - deserializer, - )); - } else { - return None; - } - } -} - -impl SseDecode for crate::api::state::ProjectInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_id = ::sse_decode(deserializer); - let mut var_name = ::sse_decode(deserializer); - let mut var_path = ::sse_decode(deserializer); - let mut var_isVisible = ::sse_decode(deserializer); - let mut var_terminalIds = >::sse_decode(deserializer); - let mut var_terminalNames = - >::sse_decode(deserializer); - return crate::api::state::ProjectInfo { - id: var_id, - name: var_name, - path: var_path, - show_in_overview: var_isVisible, - terminal_ids: var_terminalIds, - terminal_names: var_terminalNames, - }; - } -} - -impl SseDecode for (String, String) { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_field0 = ::sse_decode(deserializer); - let mut var_field1 = ::sse_decode(deserializer); - return (var_field0, var_field1); - } -} - -impl SseDecode for crate::api::terminal::ScrollInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_totalLines = ::sse_decode(deserializer); - let mut var_visibleLines = ::sse_decode(deserializer); - let mut var_displayOffset = ::sse_decode(deserializer); - return crate::api::terminal::ScrollInfo { - total_lines: var_totalLines, - visible_lines: var_visibleLines, - display_offset: var_displayOffset, - }; - } -} - -impl SseDecode for crate::api::terminal::SelectionBounds { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_startCol = ::sse_decode(deserializer); - let mut var_startRow = ::sse_decode(deserializer); - let mut var_endCol = ::sse_decode(deserializer); - let mut var_endRow = ::sse_decode(deserializer); - return crate::api::terminal::SelectionBounds { - start_col: var_startCol, - start_row: var_startRow, - end_col: var_endCol, - end_row: var_endRow, - }; - } -} - -impl SseDecode for u16 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u16::().unwrap() - } -} - -impl SseDecode for u32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u32::().unwrap() - } -} - -impl SseDecode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() - } -} - -impl SseDecode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} -} - -fn pde_ffi_dispatcher_primary_impl( - func_id: i32, - port: flutter_rust_bridge::for_generated::MessagePort, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 2 => wire__crate__api__state__close_terminal_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__state__create_terminal_impl(port, ptr, rust_vec_len, data_len), - 16 => wire__crate__api__connection__init_app_impl(port, ptr, rust_vec_len, data_len), - 18 => wire__crate__api__connection__pair_impl(port, ptr, rust_vec_len, data_len), - 22 => wire__crate__api__state__send_special_key_impl(port, ptr, rust_vec_len, data_len), - 23 => wire__crate__api__terminal__send_text_impl(port, ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} - -fn pde_ffi_dispatcher_sync_impl( - func_id: i32, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 1 => wire__crate__api__terminal__clear_selection_impl(ptr, rust_vec_len, data_len), - 3 => wire__crate__api__connection__connect_impl(ptr, rust_vec_len, data_len), - 4 => wire__crate__api__connection__connection_status_impl(ptr, rust_vec_len, data_len), - 6 => wire__crate__api__connection__disconnect_impl(ptr, rust_vec_len, data_len), - 7 => wire__crate__api__state__get_all_terminal_ids_impl(ptr, rust_vec_len, data_len), - 8 => wire__crate__api__terminal__get_cursor_impl(ptr, rust_vec_len, data_len), - 9 => wire__crate__api__state__get_focused_project_id_impl(ptr, rust_vec_len, data_len), - 10 => wire__crate__api__state__get_projects_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__terminal__get_scroll_info_impl(ptr, rust_vec_len, data_len), - 12 => wire__crate__api__terminal__get_selected_text_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__terminal__get_selection_bounds_impl(ptr, rust_vec_len, data_len), - 14 => wire__crate__api__connection__get_token_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__terminal__get_visible_cells_impl(ptr, rust_vec_len, data_len), - 17 => wire__crate__api__state__is_dirty_impl(ptr, rust_vec_len, data_len), - 19 => wire__crate__api__terminal__resize_terminal_impl(ptr, rust_vec_len, data_len), - 20 => wire__crate__api__terminal__scroll_impl(ptr, rust_vec_len, data_len), - 21 => { - wire__crate__api__connection__seconds_since_activity_impl(ptr, rust_vec_len, data_len) - } - 24 => wire__crate__api__terminal__start_selection_impl(ptr, rust_vec_len, data_len), - 25 => wire__crate__api__terminal__start_word_selection_impl(ptr, rust_vec_len, data_len), - 26 => wire__crate__api__terminal__update_selection_impl(ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} - -// Section: rust2dart - -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::CellData { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.character.into_into_dart().into_dart(), - self.fg.into_into_dart().into_dart(), - self.bg.into_into_dart().into_dart(), - self.flags.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::CellData -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::CellData -{ - fn into_into_dart(self) -> crate::api::terminal::CellData { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::connection::ConnectionStatus { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - crate::api::connection::ConnectionStatus::Disconnected => [0.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Connecting => [1.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Connected => [2.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Pairing => [3.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Error { message } => { - [4.into_dart(), message.into_into_dart().into_dart()].into_dart() - } - _ => { - unimplemented!(""); - } - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::connection::ConnectionStatus -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::connection::ConnectionStatus -{ - fn into_into_dart(self) -> crate::api::connection::ConnectionStatus { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::CursorShape { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - Self::Block => 0.into_dart(), - Self::Underline => 1.into_dart(), - Self::Beam => 2.into_dart(), - _ => unreachable!(), - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::CursorShape -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::CursorShape -{ - fn into_into_dart(self) -> crate::api::terminal::CursorShape { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::CursorState { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.col.into_into_dart().into_dart(), - self.row.into_into_dart().into_dart(), - self.shape.into_into_dart().into_dart(), - self.visible.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::CursorState -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::CursorState -{ - fn into_into_dart(self) -> crate::api::terminal::CursorState { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::state::ProjectInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.id.into_into_dart().into_dart(), - self.name.into_into_dart().into_dart(), - self.path.into_into_dart().into_dart(), - self.show_in_overview.into_into_dart().into_dart(), - self.terminal_ids.into_into_dart().into_dart(), - self.terminal_names.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::state::ProjectInfo -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::state::ProjectInfo -{ - fn into_into_dart(self) -> crate::api::state::ProjectInfo { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::ScrollInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.total_lines.into_into_dart().into_dart(), - self.visible_lines.into_into_dart().into_dart(), - self.display_offset.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::ScrollInfo -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::ScrollInfo -{ - fn into_into_dart(self) -> crate::api::terminal::ScrollInfo { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::SelectionBounds { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.start_col.into_into_dart().into_dart(), - self.start_row.into_into_dart().into_dart(), - self.end_col.into_into_dart().into_dart(), - self.end_row.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::SelectionBounds -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::SelectionBounds -{ - fn into_into_dart(self) -> crate::api::terminal::SelectionBounds { - self - } -} - -impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(format!("{:?}", self), serializer); - } -} - -impl SseEncode for std::collections::HashMap { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.into_iter().collect(), serializer); - } -} - -impl SseEncode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.into_bytes(), serializer); - } -} - -impl SseEncode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self as _).unwrap(); - } -} - -impl SseEncode for crate::api::terminal::CellData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.character, serializer); - ::sse_encode(self.fg, serializer); - ::sse_encode(self.bg, serializer); - ::sse_encode(self.flags, serializer); - } -} - -impl SseEncode for crate::api::connection::ConnectionStatus { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - match self { - crate::api::connection::ConnectionStatus::Disconnected => { - ::sse_encode(0, serializer); - } - crate::api::connection::ConnectionStatus::Connecting => { - ::sse_encode(1, serializer); - } - crate::api::connection::ConnectionStatus::Connected => { - ::sse_encode(2, serializer); - } - crate::api::connection::ConnectionStatus::Pairing => { - ::sse_encode(3, serializer); - } - crate::api::connection::ConnectionStatus::Error { message } => { - ::sse_encode(4, serializer); - ::sse_encode(message, serializer); - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseEncode for crate::api::terminal::CursorShape { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode( - match self { - crate::api::terminal::CursorShape::Block => 0, - crate::api::terminal::CursorShape::Underline => 1, - crate::api::terminal::CursorShape::Beam => 2, - _ => { - unimplemented!(""); - } - }, - serializer, - ); - } -} - -impl SseEncode for crate::api::terminal::CursorState { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.col, serializer); - ::sse_encode(self.row, serializer); - ::sse_encode(self.shape, serializer); - ::sse_encode(self.visible, serializer); - } -} - -impl SseEncode for f64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_f64::(self).unwrap(); - } -} - -impl SseEncode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_i32::(self).unwrap(); - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec<(String, String)> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - <(String, String)>::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for crate::api::state::ProjectInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.id, serializer); - ::sse_encode(self.name, serializer); - ::sse_encode(self.path, serializer); - ::sse_encode(self.show_in_overview, serializer); - >::sse_encode(self.terminal_ids, serializer); - >::sse_encode(self.terminal_names, serializer); - } -} - -impl SseEncode for (String, String) { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.0, serializer); - ::sse_encode(self.1, serializer); - } -} - -impl SseEncode for crate::api::terminal::ScrollInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.total_lines, serializer); - ::sse_encode(self.visible_lines, serializer); - ::sse_encode(self.display_offset, serializer); - } -} - -impl SseEncode for crate::api::terminal::SelectionBounds { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.start_col, serializer); - ::sse_encode(self.start_row, serializer); - ::sse_encode(self.end_col, serializer); - ::sse_encode(self.end_row, serializer); - } -} - -impl SseEncode for u16 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u16::(self).unwrap(); - } -} - -impl SseEncode for u32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u32::(self).unwrap(); - } -} - -impl SseEncode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self).unwrap(); - } -} - -impl SseEncode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} -} - -#[cfg(not(target_family = "wasm"))] -mod io { - // This file is automatically generated, so please do not edit it. - // @generated by `flutter_rust_bridge`@ 2.11.1. - - // Section: imports - - use super::*; - use flutter_rust_bridge::for_generated::byteorder::{ - NativeEndian, ReadBytesExt, WriteBytesExt, - }; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; - use flutter_rust_bridge::{Handler, IntoIntoDart}; - - // Section: boilerplate - - flutter_rust_bridge::frb_generated_boilerplate_io!(); -} -#[cfg(not(target_family = "wasm"))] -pub use io::*; - -/// cbindgen:ignore -#[cfg(target_family = "wasm")] -mod web { - // This file is automatically generated, so please do not edit it. - // @generated by `flutter_rust_bridge`@ 2.11.1. - - // Section: imports - - use super::*; - use flutter_rust_bridge::for_generated::byteorder::{ - NativeEndian, ReadBytesExt, WriteBytesExt, - }; - use flutter_rust_bridge::for_generated::wasm_bindgen; - use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; - use flutter_rust_bridge::{Handler, IntoIntoDart}; - - // Section: boilerplate - - flutter_rust_bridge::frb_generated_boilerplate_web!(); -} -#[cfg(target_family = "wasm")] -pub use web::*; diff --git a/mobile/native/src/lib.rs b/mobile/native/src/lib.rs deleted file mode 100644 index 3c536c58..00000000 --- a/mobile/native/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))] - -pub mod api; -pub mod client; -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod frb_generated; diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml deleted file mode 100644 index 3868c624..00000000 --- a/mobile/pubspec.yaml +++ /dev/null @@ -1,97 +0,0 @@ -name: mobile -description: "Okena mobile client — remote terminal access." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: ^3.10.8 - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - rust_lib_mobile: - path: rust_builder - flutter_rust_bridge: 2.11.1 - freezed_annotation: ^3.1.0 - provider: ^6.1.0 - shared_preferences: ^2.2.0 - google_fonts: ^6.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 - integration_test: - sdk: flutter - build_runner: ^2.11.0 - freezed: ^3.2.5 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - fonts: - - family: JetBrainsMono - fonts: - - asset: fonts/JetBrainsMono-Regular.ttf - - asset: fonts/JetBrainsMono-Bold.ttf - weight: 700 - - asset: fonts/JetBrainsMono-Italic.ttf - style: italic - - asset: fonts/JetBrainsMono-BoldItalic.ttf - weight: 700 - style: italic diff --git a/mobile/rn/.eslintrc.js b/mobile/rn/.eslintrc.js new file mode 100644 index 00000000..0e657aac --- /dev/null +++ b/mobile/rn/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + root: true, + extends: '@react-native', + rules: { + // Formatting is handled by the opt-in `npm run format` (prettier), not + // enforced as a lint error — the hand-authored sources use deliberate + // alignment (e.g. the binding-contract comment boxes) that we keep as-is. + 'prettier/prettier': 'off', + + // Bitwise ops are core to this codebase: decoding the packed cell buffer + // and ARGB ⇄ channel/CSS conversion (native/cells.ts, theme.ts). + 'no-bitwise': 'off', + + // `void promise` is the intentional "fire-and-forget" marker used across the + // stores (e.g. `void conn.loadServers()`), so it stays allowed. + 'no-void': 'off', + + // The sources use concise single-line guards (`if (cond) return;`); we don't + // force braces on them. ESLint still flags real correctness issues. + curly: 'off', + }, +}; diff --git a/mobile/rn/.gitignore b/mobile/rn/.gitignore new file mode 100644 index 00000000..6f00f421 --- /dev/null +++ b/mobile/rn/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +# RN host project artifacts (generated by `npx @react-native-community/cli init`) +android/ +ios/ +.expo/ +*.log + +# ubrn output: generated bindings + checked-out rust (run `npm run ubrn:android|ios`) +src/generated/ +cpp/generated/ +rust_modules/ + +# Local ubrn turbo-module package. Only the hand-authored package.json is +# tracked; everything else (TS/cpp/Kotlin bindings, gradle lib, the Rust +# static lib in jniLibs) is generated by `npm run ubrn:android|ios` — see README. +modules/okena-mobile-ffi/* +!modules/okena-mobile-ffi/package.json diff --git a/mobile/rn/.prettierrc b/mobile/rn/.prettierrc new file mode 100644 index 00000000..79d0274d --- /dev/null +++ b/mobile/rn/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/mobile/rn/.watchmanconfig b/mobile/rn/.watchmanconfig new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/mobile/rn/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/mobile/rn/CLAUDE.md b/mobile/rn/CLAUDE.md new file mode 100644 index 00000000..ae6e4766 --- /dev/null +++ b/mobile/rn/CLAUDE.md @@ -0,0 +1,47 @@ +# Mobile App — React Native (uniffi over the Rust core) + +Remote terminal client for Android/iOS. RN UI over the shared Rust core +(`crates/okena-mobile-ffi`, exposed via uniffi/ubrn as a JSI TurboModule). Native terminal +rendering with `react-native-skia` — **no `xterm.js`**. Replaced the retired Flutter app. + +Architecture overview: `../../docs/mobile-status.md`. Migration plan: `../RN_MIGRATION.md`. + +## Commands + +```bash +cd mobile/rn +npm ci +npm run typecheck # tsc --noEmit, strict +npm run lint # eslint +npm test # jest +npm run format # prettier (opt-in; NOT enforced by lint) +``` + +Device build (needs Android NDK / Xcode — see `README.md`): `npm run ubrn:android|ios` then +`npm run android|ios`. The native host dirs (`android/`, `ios/`) and ubrn output +(`src/generated/`) are generated and gitignored. + +## Key boundaries + +- **`src/native/okena.ts`** — the `OkenaNative` TS interface: the hand-maintained contract for + the ~60 functions in `crates/okena-mobile-ffi/src/lib.rs`. Keep both sides in sync. + `getOkenaNative()` `require`s the ubrn-generated module from `src/generated` (throws with a + "run ubrn" message until generated). +- **`src/native/cells.ts`** — decoder for the packed cell buffer from `get_visible_cells_packed` + (the render hot path). Its byte layout is the contract the Rust encoder must match; the jest + smoke test (`__tests__/cells.test.ts`) guards it. +- The native module is **dependency-injected**, never imported globally: stores via + `configureConnectionStore` / `configureWorkspaceStore`, `TerminalView`/`TerminalPane` via a + `native` prop. This keeps everything testable with a mock and lets `tsc`/jest run with no + native module present. + +## Conventions + +- **Package manager: npm** (`package-lock.json`). Don't switch — RN autolinking / CocoaPods / + ubrn are validated against npm/yarn. +- **State: zustand** stores with polling (mirrors the old provider cadence): fast (500ms) while + connecting, slow (1–2s) when connected. +- **ESLint** enforces correctness only; `prettier/prettier`, `no-bitwise` (cell/ARGB decoding), + `no-void` (fire-and-forget), and `curly` are off by design (see `.eslintrc.js`). +- **uniffi ⇄ ubrn version pairing** must match: `uniffi = "0.31"` (crate) ↔ + `uniffi-bindgen-react-native@0.31.0-3` (devDep). Bump together. diff --git a/mobile/rn/README.md b/mobile/rn/README.md new file mode 100644 index 00000000..c5d3184f --- /dev/null +++ b/mobile/rn/README.md @@ -0,0 +1,197 @@ +# Okena mobile — React Native + +The **React Native** mobile client: the UI layer over the shared Rust core +(`crates/okena-mobile-ffi`, exposed to TypeScript via uniffi/ubrn), with a native +`react-native-skia` terminal renderer (**no `xterm.js`**). This replaces the retired Flutter +app; the migration plan is [`../RN_MIGRATION.md`](../RN_MIGRATION.md) and the architecture +overview is [`../../docs/mobile-status.md`](../../docs/mobile-status.md). + +## What's here + +A complete RN 0.76 project **minus the native host directories** (`android/`, `ios/`), which +are machine-generated (see step 1 below). What is in the repo: + +- **JS host config** — `index.js`, `app.json`, `metro.config.js`, `babel.config.js`, + `react-native.config.js`, `tsconfig.json`, `jest.config.js`, `.eslintrc.js`, `.prettierrc`. +- **The native↔TS binding contract** — `src/native/okena.ts`: the `OkenaNative` interface + (the ~60 functions exported from `crates/okena-mobile-ffi/src/lib.rs`) + all record/enum + types, plus `getOkenaNative()` which resolves the ubrn-generated module from `src/generated`. +- **The packed-cell decoder** — `src/native/cells.ts`: reads the little-endian cell buffer + that `get_visible_cells_packed` produces (the render hot path). +- **App** — screens (`ServerList`, `Pairing`, `Workspace`), zustand stores (dependency- + injected, so testable with a mock `OkenaNative`), `TerminalView` (Skia 3-pass paint), + `KeyToolbar`, `LayoutRenderer`, `ProjectDrawer`, theme, and the JetBrainsMono fonts + (`assets/`). + +### Verified vs. not verified + +Verified in CI / on any machine (no mobile toolchain needed): + +```bash +cd mobile/rn +npm ci +npm run typecheck # tsc --noEmit, strict +npm run lint # eslint +npm test # jest (packed-cell decoder smoke test) +``` + +The ubrn cross-compile, the Skia native binaries, and an on-device run need the mobile +toolchain — see the device steps below, and the **verified Android run** notes next for the +gotchas that the generic steps don't mention. + +> Package manager: **npm** (the lockfile is `package-lock.json`). RN 0.76 native autolinking, +> CocoaPods, and ubrn are validated against npm/yarn — don't swap in a different manager here. + +--- + +## Verified Android run — what it actually took (2026-06) + +The full chain has been run end-to-end on an Android **emulator**: build → TLS connect → pair → +workspace → live terminal (Skia paint of colored shell output). The generic steps below are +correct in spirit but several things needed fixing/wiring that aren't obvious — captured here. + +**The turbo module is a local package.** ubrn treats a project as a *library* and would clobber +the app's root `android/build.gradle`, so the generated module is kept as its own package at +`modules/okena-mobile-ffi/` and the app depends on it (`"okena-mobile-ffi": "file:./modules/okena-mobile-ffi"`, +autolinked + a Metro symlink). Only that package's hand-authored `package.json` (with +`codegenConfig`) is committed; **everything else under it is generated by ubrn and gitignored** +(incl. the multi-hundred-MB Rust static lib in `jniLibs` — never commit it). `getOkenaNative()` +in `src/native/okena.ts` `require`s `'okena-mobile-ffi'` (its entry installs the JSI bindings). + +**Fixes that are committed** (needed to build + run at all): + +- **TLS provider → `ring`.** `okena-core` (and the desktop server) now build rustls with the + `ring` crypto provider, not `aws-lc-rs` — `aws-lc-rs`'s jitter-entropy init **segfaults on + Android** during the TLS handshake. (Supersedes the "rustls-tls, no OpenSSL" note below.) +- **ubrn enum/record adapters** (`src/native/okena.ts`). ubrn 0.31 emits enums in shapes the + hand-written contract didn't match: `ConnectionStatus` as a PascalCase-`.tag` class (payload + under `.inner`), `CursorShape` as a numeric enum, `ProjectInfo.terminalNames` as a JS `Map`. + `getOkenaNative()` translates these at the boundary to the app's `{ kind }` / string / object + contract. Plain-string enums (split direction, diff mode) and other records pass through. +- **Skia `setEmbolden` dropped** (`src/components/TerminalView.tsx`). react-native-skia 1.12.4's + native binding rejects the (typed `boolean`) arg with *"Value is false, expected a number"*; + all four JetBrainsMono variants are bundled so synthetic bold is unused anyway. +- **`@ubjs/core` Metro alias** (`metro.config.js`). ubrn 0.31's generated bindings import the + TS runtime as `@ubjs/core`; we alias it to the runtime shipped inside + `uniffi-bindgen-react-native` (same bytes, guaranteed version match) instead of a 2nd install. + +**Post-generation fixups** — apply after `npm run ubrn:android` (ubrn regenerates these, so they +are *not* committed; a small post-gen step should automate them): + +- `modules/okena-mobile-ffi/android/CMakeLists.txt` resolves the runtime via + `node -p "require.resolve('uniffi-bindgen-react-native/package.json')"`, which **throws on + Node ≥ 20** (`ERR_PACKAGE_PATH_NOT_EXPORTED` — the package's `exports` doesn't expose + `./package.json`). Replace it with `node -e "…require.resolve('uniffi-bindgen-react-native')…"` + sliced back to the package root. +- The generated lib `build.gradle` (AGP 8 path) references `src/main/AndroidManifestNew.xml`, + but ubrn doesn't create it — add a minimal namespace-less ``. + +**Pairing:** get the 6-char code from the desktop host with `okena pair` (CLI, prints +`XXXX-XXXX`, 60s TTL) and enter it as-is (with the dash). From the emulator the host is `10.0.2.2`. + +**Emulator caveat (rendering):** `react-native-skia`'s GL present crashes **intermittently** +inside the Android emulator's EGL driver (`libEGL_emulation.so` → `createNativeSync` / +`eglSwapBuffers`, from `RNSkia::OpenGLWindowContext::present`). It is **not an app bug** and does +not happen on physical devices; the `-gpu host|swiftshader_indirect|angle_indirect` flags don't +help (the guest EGL is fixed by the system image). **Use a real Android device** for stable +terminal rendering. + +--- + +## Device-side setup (run on a machine with the RN toolchain) + +Prereqs: Node ≥ 18, Watchman, JDK 17, Android SDK + NDK + `cargo-ndk` (Android), Xcode + +CocoaPods (iOS), and the Rust mobile targets: + +```bash +rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android +rustup target add aarch64-apple-ios aarch64-apple-ios-sim +cargo install cargo-ndk +``` + +### 1. Generate the native host projects (`android/`, `ios/`) + +`@shopify/react-native-skia` and the ubrn TurboModule are Fabric/TurboModules, so a **bare** +RN app (new architecture ON — the RN 0.76 default) is required; Expo Go won't work. + +```bash +# from a temp dir: generate a host with the SAME app name as app.json ("OkenaMobile") +npx @react-native-community/cli@latest init OkenaMobile --version 0.76.5 +# copy ONLY the generated native dirs into this project: +cp -R OkenaMobile/android OkenaMobile/ios ./ +``` + +The JS/config files in this repo (`index.js`, `app.json`, `metro.config.js`, …) already match +what the template produces, so you only need its `android/` and `ios/` directories (both are +gitignored here). Confirm new-arch is on: `android/gradle.properties → newArchEnabled=true`. + +### 2. Install JS deps + link fonts + +```bash +npm ci +npx react-native-asset # links assets/JetBrainsMono-*.ttf (react-native.config.js) +``` + +The Skia renderer additionally loads the same ttf via `useFont(require('../../assets/...'))` +in `WorkspaceScreen.tsx`, so the fonts are both linked (for ``) and bundled. + +### 3. Generate the Rust↔TS bindings with ubrn + +`uniffi-bindgen-react-native` (`ubrn`) cross-compiles `crates/okena-mobile-ffi` and emits the +JSI TurboModule + TypeScript into `src/generated` (gitignored). Config: `ubrn.config.yaml`. + +```bash +npm run ubrn:android # ubrn build android --config ubrn.config.yaml --and-generate --release +npm run ubrn:ios # ubrn build ios --config ubrn.config.yaml --and-generate --release +( cd ios && pod install ) # pick up the generated xcframework +``` + +`getOkenaNative()` (`src/native/okena.ts`) already `require`s `../generated`, so once this +runs the app is wired — no code edit needed. + +> **Version pairing:** ubrn and uniffi minor versions must match. This repo pins +> `uniffi-bindgen-react-native@0.31.0-3` (devDependency) ↔ `uniffi = "0.31"` in +> `crates/okena-mobile-ffi/Cargo.toml`. If `ubrn` reports a metadata/contract-version +> mismatch, bump both together. + +> **NDK / TLS:** ensure `$HOME/.cargo/bin` is on the PATH the Gradle daemon sees. The crate +> already selects `rustls-tls` (via `okena-core`'s `client` feature), so no OpenSSL is +> cross-compiled for the NDK. + +### 4. Run + +```bash +npm run android # device/emulator +npm run ios # simulator +``` + +### 5. Phase-0 spikes (validate the two unknowns — see `../RN_MIGRATION.md` §3) + +- **S1 (toolchain):** confirm `initApp()` + `connect()` + `connectionStatus()` work end-to-end + through the ubrn module on a real Android device *and* iOS sim. +- **S2 (rendering):** confirm `react-native-skia` sustains the cell-grid paint at 60fps. + +--- + +## File map + +``` +mobile/rn/ +├── index.js · app.json # RN entry + app name +├── metro.config.js · babel.config.js # bundler + transpiler +├── react-native.config.js # font asset linking +├── ubrn.config.yaml # ubrn: crate path, targets, output dirs +├── jest.config.js · __tests__/ # jest (cells decoder smoke test) +├── .eslintrc.js · .prettierrc # lint + format +├── tsconfig.json · package.json +├── assets/JetBrainsMono-*.ttf # bundled monospace fonts +└── src/ + ├── App.tsx · theme.ts + ├── native/ + │ ├── okena.ts # OkenaNative contract + getOkenaNative() + │ └── cells.ts # packed cell-buffer decoder + ├── state/ # zustand stores (DI), persistence, navigation + ├── screens/ # ServerList, Pairing, Workspace + ├── components/ # TerminalView (Skia), KeyToolbar, LayoutRenderer, … + └── models/ # SavedServer, LayoutNode +``` diff --git a/mobile/rn/__tests__/cells.test.ts b/mobile/rn/__tests__/cells.test.ts new file mode 100644 index 00000000..0c6def03 --- /dev/null +++ b/mobile/rn/__tests__/cells.test.ts @@ -0,0 +1,84 @@ +/** + * Smoke test for the packed visible-cell decoder — the contract the Rust + * `get_visible_cells_packed` encoder (crates/okena-mobile-ffi) must satisfy. + */ +import { + decodeCells, + decodeCellsView, + argbToChannels, + argbToCss, + FLAG_BOLD, + HEADER_BYTES, + CELL_BYTES, +} from '../src/native/cells'; + +interface Cell { + codepoint: number; + fg: number; + bg: number; + flags: number; +} + +/** Build a packed buffer exactly as the Rust encoder does (LE throughout). */ +function packGrid(cols: number, rows: number, cells: Cell[]): ArrayBuffer { + const buf = new ArrayBuffer(HEADER_BYTES + cols * rows * CELL_BYTES); + const v = new DataView(buf); + v.setUint16(0, cols, true); + v.setUint16(2, rows, true); + let off = HEADER_BYTES; + for (const c of cells) { + v.setUint32(off + 0, c.codepoint, true); + v.setUint32(off + 4, c.fg, true); + v.setUint32(off + 8, c.bg, true); + v.setUint8(off + 12, c.flags); + off += CELL_BYTES; + } + return buf; +} + +const SAMPLE: Cell[] = [ + {codepoint: 0x41 /* 'A' */, fg: 0xff112233, bg: 0xff000000, flags: FLAG_BOLD}, + {codepoint: 0x20 /* ' ' */, fg: 0, bg: 0, flags: 0}, +]; + +describe('decodeCells', () => { + it('decodes header + cells round-trip from the packed format', () => { + const grid = decodeCells(packGrid(2, 1, SAMPLE)); + expect(grid.cols).toBe(2); + expect(grid.rows).toBe(1); + expect(grid.cells).toHaveLength(2); + expect(grid.cells[0]).toEqual({ + codepoint: 0x41, + fg: 0xff112233, + bg: 0xff000000, + flags: FLAG_BOLD, + }); + expect(grid.cells[1].codepoint).toBe(0x20); + }); + + it('throws on a truncated buffer', () => { + const short = packGrid(2, 1, SAMPLE).slice(0, 10); + expect(() => decodeCells(short)).toThrow(RangeError); + }); +}); + +describe('PackedCells (zero-alloc view)', () => { + it('exposes per-cell getters matching decodeCells', () => { + const view = decodeCellsView(packGrid(2, 1, SAMPLE)); + expect(view.count).toBe(2); + expect(view.codepoint(0)).toBe(0x41); + expect(view.char(0)).toBe('A'); + expect(view.flags(0) & FLAG_BOLD).toBe(FLAG_BOLD); + expect(view.isBlank(1)).toBe(true); + }); +}); + +describe('ARGB helpers', () => { + it('unpacks channels from 0xAARRGGBB', () => { + expect(argbToChannels(0xff112233)).toEqual({a: 255, r: 0x11, g: 0x22, b: 0x33}); + }); + + it('formats an rgba() string', () => { + expect(argbToCss(0xff112233)).toBe('rgba(17, 34, 51, 1.0000)'); + }); +}); diff --git a/mobile/rn/app.json b/mobile/rn/app.json new file mode 100644 index 00000000..3b784f6c --- /dev/null +++ b/mobile/rn/app.json @@ -0,0 +1,4 @@ +{ + "name": "OkenaMobile", + "displayName": "Okena" +} diff --git a/mobile/fonts/JetBrainsMono-Bold.ttf b/mobile/rn/assets/JetBrainsMono-Bold.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-Bold.ttf rename to mobile/rn/assets/JetBrainsMono-Bold.ttf diff --git a/mobile/fonts/JetBrainsMono-BoldItalic.ttf b/mobile/rn/assets/JetBrainsMono-BoldItalic.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-BoldItalic.ttf rename to mobile/rn/assets/JetBrainsMono-BoldItalic.ttf diff --git a/mobile/fonts/JetBrainsMono-Italic.ttf b/mobile/rn/assets/JetBrainsMono-Italic.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-Italic.ttf rename to mobile/rn/assets/JetBrainsMono-Italic.ttf diff --git a/mobile/fonts/JetBrainsMono-Regular.ttf b/mobile/rn/assets/JetBrainsMono-Regular.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-Regular.ttf rename to mobile/rn/assets/JetBrainsMono-Regular.ttf diff --git a/mobile/rn/babel.config.js b/mobile/rn/babel.config.js new file mode 100644 index 00000000..f7b3da3b --- /dev/null +++ b/mobile/rn/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +}; diff --git a/mobile/rn/index.js b/mobile/rn/index.js new file mode 100644 index 00000000..deb22c84 --- /dev/null +++ b/mobile/rn/index.js @@ -0,0 +1,16 @@ +/** + * Okena mobile — React Native entry point. + * + * Registers the root component with the native host. The app name must match + * `app.json`'s `name` and the value the generated Android/iOS host projects + * pass to `ReactActivityDelegate` / `RCTRootView`. + * + * @format + */ + +import {AppRegistry} from 'react-native'; + +import App from './src/App'; +import {name as appName} from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/mobile/rn/jest.config.js b/mobile/rn/jest.config.js new file mode 100644 index 00000000..8eb675e9 --- /dev/null +++ b/mobile/rn/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'react-native', +}; diff --git a/mobile/rn/metro.config.js b/mobile/rn/metro.config.js new file mode 100644 index 00000000..386d3753 --- /dev/null +++ b/mobile/rn/metro.config.js @@ -0,0 +1,32 @@ +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const path = require('path'); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('@react-native/metro-config').MetroConfig} + */ + +// ubrn (uniffi-bindgen-react-native) 0.31 generates bindings that import the +// TypeScript runtime as `@ubjs/core` (its new published identity). That runtime +// is the *same bytes* already shipped inside `uniffi-bindgen-react-native` +// (typescript/dist), so we alias `@ubjs/core` to it rather than installing a +// second, potentially version-skewed copy. See node_modules/uniffi-bindgen-react-native/README.md. +const ubrnRuntime = path.resolve( + __dirname, + 'node_modules/uniffi-bindgen-react-native/typescript/dist/cjs/index.js', +); + +const config = { + resolver: { + resolveRequest: (context, moduleName, platform) => { + if (moduleName === '@ubjs/core') { + return {type: 'sourceFile', filePath: ubrnRuntime}; + } + return context.resolveRequest(context, moduleName, platform); + }, + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/mobile/rn/modules/okena-mobile-ffi/package.json b/mobile/rn/modules/okena-mobile-ffi/package.json new file mode 100644 index 00000000..2bed4871 --- /dev/null +++ b/mobile/rn/modules/okena-mobile-ffi/package.json @@ -0,0 +1,26 @@ +{ + "name": "okena-mobile-ffi", + "version": "0.0.0", + "private": true, + "description": "ubrn-generated React Native turbo module for crates/okena-mobile-ffi (local, gitignored — regenerated by `npm run ubrn:android|ios`).", + "main": "src/index.tsx", + "react-native": "src/index.tsx", + "source": "src/index.tsx", + "files": [ + "src", + "android", + "cpp" + ], + "codegenConfig": { + "name": "OkenaMobileFfi", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.okenamobilern" + } + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } +} diff --git a/mobile/rn/package-lock.json b/mobile/rn/package-lock.json new file mode 100644 index 00000000..1c293c05 --- /dev/null +++ b/mobile/rn/package-lock.json @@ -0,0 +1,12561 @@ +{ + "name": "okena-mobile-rn", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "okena-mobile-rn", + "version": "0.0.0", + "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.0", + "@shopify/react-native-skia": "^1.5.0", + "okena-mobile-ffi": "file:./modules/okena-mobile-ffi", + "react": "18.3.1", + "react-native": "0.76.5", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native/babel-preset": "0.76.5", + "@react-native/eslint-config": "0.76.5", + "@react-native/metro-config": "0.76.5", + "@react-native/typescript-config": "0.76.5", + "@types/jest": "^29.5.13", + "@types/react": "^18.3.12", + "@types/react-test-renderer": "^18.3.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "18.3.1", + "typescript": "^5.6.3", + "uniffi-bindgen-react-native": "0.31.0-3" + }, + "engines": { + "node": ">=18" + } + }, + "modules/okena-mobile-ffi": { + "version": "0.0.0", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.29.7.tgz", + "integrity": "sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native-community/cli": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz", + "integrity": "sha512-xIGPytx2bj5HxFk0c7S25AVuJowHmEFg5LFC9XosKc0TSOjP1r6zGC6OqC/arQV/pNuqmZN2IFnpgJn0Bn+hhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-clean": "15.0.1", + "@react-native-community/cli-config": "15.0.1", + "@react-native-community/cli-debugger-ui": "15.0.1", + "@react-native-community/cli-doctor": "15.0.1", + "@react-native-community/cli-server-api": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "@react-native-community/cli-types": "15.0.1", + "chalk": "^4.1.2", + "commander": "^9.4.1", + "deepmerge": "^4.3.0", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.1.3", + "prompts": "^2.4.2", + "semver": "^7.5.2" + }, + "bin": { + "rnc-cli": "build/bin.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native-community/cli-clean": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-15.0.1.tgz", + "integrity": "sha512-flGTfT005UZvW2LAXVowZ/7ri22oiiZE4pPgMvc8klRxO5uofKIRuohgiHybHtiCo/HNqIz45JmZJvuFrhc4Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-config": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-15.0.1.tgz", + "integrity": "sha512-SL3/9zIyzQQPKWei0+W1gNHxCPurrxqpODUWnVLoP38DNcvYCGtsRayw/4DsXgprZfBC+FsscNpd3IDJrG59XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "cosmiconfig": "^9.0.0", + "deepmerge": "^4.3.0", + "fast-glob": "^3.3.2", + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli-config-apple": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-15.0.1.tgz", + "integrity": "sha512-GEHUx4NRp9W9or6vygn0TgNeFkcJdNjrtko0vQEJAS4gJdWqP/9LqqwJNlUfaW5jHBN7TKALAMlfRmI12Op3sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-config/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@react-native-community/cli-config/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-native-community/cli-config/node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-config/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@react-native-community/cli-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-config/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@react-native-community/cli-debugger-ui": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-15.0.1.tgz", + "integrity": "sha512-xkT2TLS8zg5r7Vl9l/2f7JVUoFECnVBS+B5ivrSu2PNZhKkr9lRmJFxC9aVLFb5lIxQQKNDvEyiIDNfP7wjJiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "serve-static": "^1.13.1" + } + }, + "node_modules/@react-native-community/cli-doctor": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-15.0.1.tgz", + "integrity": "sha512-YCu44lZR3zZxJJYVTqYZFz9cT9KBfbKI4q2MnKOvkamt00XY3usooMqfuwBAdvM/yvpx7M5w8kbM/nPyj4YCvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-apple": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "command-exists": "^1.2.8", + "deepmerge": "^4.3.0", + "envinfo": "^7.13.0", + "execa": "^5.0.0", + "node-stream-zip": "^1.9.1", + "ora": "^5.4.1", + "semver": "^7.5.2", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1", + "yaml": "^2.2.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-platform-android": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-15.0.1.tgz", + "integrity": "sha512-QlAMomj6H6TY6pHwjTYMsHDQLP5eLzjAmyW1qb03w/kyS/72elK2bjsklNWJrscFY9TMQLqw7qoAsXf1m5t/dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.4.1", + "logkitty": "^0.7.1" + } + }, + "node_modules/@react-native-community/cli-platform-apple": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-15.0.1.tgz", + "integrity": "sha512-iQj1Dt2fr/Q7X2CQhyhWnece3eLDCark1osfiwpViksOfTH2WdpNS3lIwlFcIKhsieFU7YYwbNuFqQ3tF9Dlvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config-apple": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-xml-parser": "^4.4.1" + } + }, + "node_modules/@react-native-community/cli-platform-ios": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-15.0.1.tgz", + "integrity": "sha512-6pKzXEIgGL20eE1uOn8iSsNBlMzO1LG+pQOk+7mvD172EPhKm/lRzUVDX5gO/2jvsGoNw6VUW0JX1FI2firwqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-platform-apple": "15.0.1" + } + }, + "node_modules/@react-native-community/cli-server-api": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-15.0.1.tgz", + "integrity": "sha512-f3rb3t1ELLaMSX5/LWO/IykglBIgiP3+pPnyl8GphHnBpf3bdIcp7fHlHLemvHE06YxT2nANRxRPjy1gNskenA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-debugger-ui": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": { + "version": "15.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz", + "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@react-native-community/cli-tools": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-15.0.1.tgz", + "integrity": "sha512-N79A+u/94roanfmNohVcNGu6Xg+0idh63JHZFLC9OJJuZwTifGMLDfSTHZATpR1J7rebozQ5ClcSUePavErnSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "mime": "^2.4.1", + "open": "^6.2.0", + "ora": "^5.4.1", + "prompts": "^2.4.2", + "semver": "^7.5.2", + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "devOptional": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli-types": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-15.0.1.tgz", + "integrity": "sha512-sWiJ62kkGu2mgYni2dsPxOMBzpwTjNsDH1ubY4mqcNEI9Zmzs0vRwwDUEhYqwNGys9+KpBKoZRrT2PAlhO84xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@react-native-community/cli/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-community/cli/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", + "integrity": "sha512-MN5dasWo37MirVcKWuysRkRr4BjNc81SXwUtJYstwbn8oEkfnwR9DaqdDTo/hHOnTdhafffLIa2xOOHcjDIGEw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.5.tgz", + "integrity": "sha512-xe7HSQGop4bnOLMaXt0aU+rIatMNEQbz242SDl8V9vx5oOTI0VbZV9yLy6yBc6poUlYbcboF20YVjoRsxX4yww==", + "license": "MIT", + "dependencies": { + "@react-native/codegen": "0.76.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.5.tgz", + "integrity": "sha512-1Nu5Um4EogOdppBLI4pfupkteTjWfmI0hqW8ezWTg7Bezw0FtBj8yS8UYVd3wTnDFT9A5mA2VNoNUqomJnvj2A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.76.5", + "babel-plugin-syntax-hermes-parser": "^0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.5.tgz", + "integrity": "sha512-FoZ9VRQ5MpgtDAnVo1rT9nNRfjnWpE40o1GeJSDlpUMttd36bVXvsDm8W/NhX8BKTWXSX+CPQJsRcvN1UPYGKg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.23.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.5.tgz", + "integrity": "sha512-3MKMnlU0cZOWlMhz5UG6WqACJiWUrE3XwBEumzbMmZw3Iw3h+fIsn+7kLLE5EhzqLt0hg5Y4cgYFi4kOaNgq+g==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.76.5", + "@react-native/metro-babel-transformer": "0.76.5", + "chalk": "^4.0.0", + "execa": "^5.1.1", + "invariant": "^2.2.4", + "metro": "^0.81.0", + "metro-config": "^0.81.0", + "metro-core": "^0.81.0", + "node-fetch": "^2.2.0", + "readline": "^1.3.0", + "semver": "^7.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-native-community/cli-server-api": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli-server-api": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz", + "integrity": "sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz", + "integrity": "sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.76.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/eslint-config": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.76.5.tgz", + "integrity": "sha512-FnzjnwuWrpuJaBfjLMEPtGe6dy3d2Mc3cnoOGF5ghDbpHP2JUHp1GoKRZdZpJlGXJyQTi8wULpyKK6v8jM0dOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/eslint-parser": "^7.25.1", + "@react-native/eslint-plugin": "0.76.5", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.76.5.tgz", + "integrity": "sha512-yAd3349bvWXlegStk6o/lOofRVmr/uSLNdAEsFXw18OlxjnBSx9U3teJtQNA91DfquQAcmSgf1lIBv+MUJ+fnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.76.5.tgz", + "integrity": "sha512-7KSyD0g0KhbngITduC8OABn0MAlJfwjIdze7nA4Oe1q3R7qmAv+wQzW+UEXvPah8m1WqFjYTkQwz/4mK3XrQGw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.76.5.tgz", + "integrity": "sha512-ggM8tcKTcaqyKQcXMIvcB0vVfqr9ZRhWVxWIdiFO1mPvJyS6n+a+lLGkgQAyO8pfH0R1qw6K9D0nqbbDo865WQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.5.tgz", + "integrity": "sha512-Cm9G5Sg5BDty3/MKa3vbCAJtT3YHhlEaPlQALLykju7qBS+pHZV9bE9hocfyyvc5N/osTIGWxG5YOfqTeMu1oQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.76.5", + "hermes-parser": "0.23.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.76.5.tgz", + "integrity": "sha512-+bklxpRj1BAFzAwOI29MjdddwlC6wTJYlnMK9a77GiowELNeRO4R8UD1dRepOoSVpPFfFlLbFiqYQXqBrbl1pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native/js-polyfills": "0.76.5", + "@react-native/metro-babel-transformer": "0.76.5", + "metro-config": "^0.81.0", + "metro-runtime": "^0.81.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.5.tgz", + "integrity": "sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==", + "license": "MIT" + }, + "node_modules/@react-native/typescript-config": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/typescript-config/-/typescript-config-0.76.5.tgz", + "integrity": "sha512-dRbY4XQTUUxR5Oq+S+2/5JQVU6WL0qvNnAz51jiXllC+hp5L4bljSxlzaj5CJ9vzGNFzm56m5Y9Q6MltoIU4Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.76.5.tgz", + "integrity": "sha512-M/fW1fTwxrHbcx0OiVOIxzG6rKC0j9cR9Csf80o77y1Xry0yrNPpAlf8D1ev3LvHsiAUiRNFlauoPtodrs2J1A==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@shopify/react-native-skia": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-1.12.4.tgz", + "integrity": "sha512-8QDIBKSU7XB3Lc1kAv4jSFddTQK8AE+1AEoJnQLNllsiex1gufLQ8kN7rs9zii+iboSY8tYKT7ocV+5cE2Exdw==", + "license": "MIT", + "dependencies": { + "canvaskit-wasm": "0.40.0", + "react-reconciler": "0.27.0" + }, + "bin": { + "setup-skia-web": "scripts/setup-canvaskit.js" + }, + "peerDependencies": { + "react": ">=18.0 <19.0.0", + "react-native": ">=0.64 <0.78.0", + "react-native-reanimated": ">=2.0.0" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-vAhnk0tG2eGa37lkU9+s5SoroCsRI08xnsWFiAXOuPH2jqzMbcXvKExXViPi1P5fIklDeCvXqyrdmipFaSkZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "^18" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webgpu/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", + "integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==", + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-fragments": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", + "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^1.0.7", + "slice-ansi": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/ansi-fragments/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdirsjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", + "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", + "integrity": "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.23.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", + "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvaskit-wasm": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz", + "integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==", + "license": "BSD-3-Clause", + "dependencies": { + "@webgpu/types": "0.1.21" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/errorhandler": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", + "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-ft-flow": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-2.0.3.tgz", + "integrity": "sha512-Vbsd/b+LYA99jUbsL6viEUWShFaYQt2YQs3QN3f+aeszOhh2sgdcU0mjzDyD4yyBvMc8qy2uwvBBWfMzEX06tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "@babel/eslint-parser": "^7.12.0", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-4.1.0.tgz", + "integrity": "sha512-QLo7rzTBOl43FvVqDdq5Ql9IoElIuTdjrz9SKAXCvULvBoRZ44JGSkx9z4999ZusCsb4rK3gjS8gOGyeYqZv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-react-native-globals": "^0.1.1" + }, + "peerDependencies": { + "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-native-globals": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", + "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", + "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/flow-parser": { + "version": "0.317.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.317.0.tgz", + "integrity": "sha512-pzwkCzruTUg6f5HH7N0OvVjX7dVc361tnsEkSCbMC9cJ5zxZY84de8l+DraCmnGsIbi+jQPPxtTKfBJFnYCJZQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz", + "integrity": "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.1.tgz", + "integrity": "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.23.1" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-android": { + "version": "250231.0.0", + "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", + "integrity": "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==", + "license": "BSD-2-Clause" + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD" + }, + "node_modules/jscodeshift": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.14.0.tgz", + "integrity": "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.13.16", + "@babel/parser": "^7.13.16", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/preset-flow": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "babel-core": "^7.0.0-bridge.0", + "chalk": "^4.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.21.0", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/jscodeshift/node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-fragments": "^0.2.1", + "dayjs": "^1.8.15", + "yargs": "^15.1.0" + }, + "bin": { + "logkitty": "bin/logkitty.js" + } + }, + "node_modules/logkitty/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/logkitty/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/logkitty/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metro": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.81.5.tgz", + "integrity": "sha512-YpFF0DDDpDVygeca2mAn7K0+us+XKmiGk4rIYMz/CRdjFoCGqAei/IQSpV0UrGfQbToSugpMQeQJveaWSH88Hg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.25.1", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.81.5", + "metro-cache": "0.81.5", + "metro-cache-key": "0.81.5", + "metro-config": "0.81.5", + "metro-core": "0.81.5", + "metro-file-map": "0.81.5", + "metro-resolver": "0.81.5", + "metro-runtime": "0.81.5", + "metro-source-map": "0.81.5", + "metro-symbolicate": "0.81.5", + "metro-transform-plugins": "0.81.5", + "metro-transform-worker": "0.81.5", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.81.5.tgz", + "integrity": "sha512-oKCQuajU5srm+ZdDcFg86pG/U8hkSjBlkyFjz380SZ4TTIiI5F+OQB830i53D8hmqmcosa4wR/pnKv8y4Q3dLw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.25.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/metro-cache": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.81.5.tgz", + "integrity": "sha512-wOsXuEgmZMZ5DMPoz1pEDerjJ11AuMy9JifH4yNW7NmWS0ghCRqvDxk13LsElzLshey8C+my/tmXauXZ3OqZgg==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "metro-core": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache-key": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.81.5.tgz", + "integrity": "sha512-lGWnGVm1UwO8faRZ+LXQUesZSmP1LOg14OVR+KNPBip8kbMECbQJ8c10nGesw28uQT7AE0lwQThZPXlxDyCLKQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.81.5.tgz", + "integrity": "sha512-oDRAzUvj6RNRxratFdcVAqtAsg+T3qcKrGdqGZFUdwzlFJdHGR9Z413sW583uD2ynsuOjA2QB6US8FdwiBdNKg==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.81.5", + "metro-cache": "0.81.5", + "metro-core": "0.81.5", + "metro-runtime": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-core": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.81.5.tgz", + "integrity": "sha512-+2R0c8ByfV2N7CH5wpdIajCWa8escUFd8TukfoXyBq/vb6yTCsznoA25FhNXJ+MC/cz1L447Zj3vdUfCXIZBwg==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.81.5.tgz", + "integrity": "sha512-mW1PKyiO3qZvjeeVjj1brhkmIotObA3/9jdbY1fQQYvEWM6Ml7bN/oJCRDGn2+bJRlG+J8pwyJ+DgdrM4BsKyg==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro-file-map/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/metro-minify-terser": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.81.5.tgz", + "integrity": "sha512-/mn4AxjANnsSS3/Bb+zA1G5yIS5xygbbz/OuPaJYs0CPcZCaWt66D+65j4Ft/nJkffUxcwE9mk4ubpkl3rjgtw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-resolver": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.81.5.tgz", + "integrity": "sha512-6BX8Nq3g3go3FxcyXkVbWe7IgctjDTk6D9flq+P201DfHHQ28J+DWFpVelFcrNTn4tIfbP/Bw7u/0g2BGmeXfQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-runtime": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.81.5.tgz", + "integrity": "sha512-M/Gf71ictUKP9+77dV/y8XlAWg7xl76uhU7ggYFUwEdOHHWPG6gLBr1iiK0BmTjPFH8yRo/xyqMli4s3oGorPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.81.5.tgz", + "integrity": "sha512-Jz+CjvCKLNbJZYJTBeN3Kq9kIJf6b61MoLBdaOQZJ5Ajhw6Pf95Nn21XwA8BwfUYgajsi6IXsp/dTZsYJbN00Q==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.81.5", + "nullthrows": "^1.1.1", + "ob1": "0.81.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.81.5.tgz", + "integrity": "sha512-X3HV3n3D6FuTE11UWFICqHbFMdTavfO48nXsSpnNGFkUZBexffu0Xd+fYKp+DJLNaQr3S+lAs8q9CgtDTlRRuA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.81.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.81.5.tgz", + "integrity": "sha512-MmHhVx/1dJC94FN7m3oHgv5uOjKH8EX8pBeu1pnPMxbJrx6ZuIejO0k84zTSaQTZ8RxX1wqwzWBpXAWPjEX8mA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.81.5.tgz", + "integrity": "sha512-lUFyWVHa7lZFRSLJEv+m4jH8WrR5gU7VIjUlg4XmxQfV8ngY4V10ARKynLhMYPeQGl7Qvf+Ayg0eCZ272YZ4Mg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.81.5", + "metro-babel-transformer": "0.81.5", + "metro-cache": "0.81.5", + "metro-cache-key": "0.81.5", + "metro-minify-terser": "0.81.5", + "metro-source-map": "0.81.5", + "metro-transform-plugins": "0.81.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/metro/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nocache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", + "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.5.tgz", + "integrity": "sha512-iNpbeXPLmaiT9I5g16gFFFjsF3sGxLpYG2EGP3dfFB4z+l9X60mp/yRzStHhMtuNt8qmf7Ww80nOPQHngHhnIQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/okena-mobile-ffi": { + "resolved": "modules/okena-mobile-ffi", + "link": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.2.tgz", + "integrity": "sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.5.tgz", + "integrity": "sha512-op2p2kB+lqMF1D7AdX4+wvaR0OPFbvWYs+VBE7bwsb99Cn9xISrLRLAgFflZedQsa5HvnOGrULhtnmItbIKVVw==", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.6.3", + "@react-native/assets-registry": "0.76.5", + "@react-native/codegen": "0.76.5", + "@react-native/community-cli-plugin": "0.76.5", + "@react-native/gradle-plugin": "0.76.5", + "@react-native/js-polyfills": "0.76.5", + "@react-native/normalize-colors": "0.76.5", + "@react-native/virtualized-lists": "0.76.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "^0.23.1", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "commander": "^12.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.6.3", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.81.0", + "metro-source-map": "^0.81.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^5.3.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-is": "^18.3.1", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-test-renderer/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", + "license": "BSD" + }, + "node_modules/recast": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.5.tgz", + "integrity": "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==", + "license": "MIT", + "dependencies": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "devOptional": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "license": "MIT", + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/uniffi-bindgen-react-native": { + "version": "0.31.0-3", + "resolved": "https://registry.npmjs.org/uniffi-bindgen-react-native/-/uniffi-bindgen-react-native-0.31.0-3.tgz", + "integrity": "sha512-br7giBJRr/j00rdMXhOZGMGuDWMAVUcxC9O3lco0KRqzEAxUz00+gegPW5tKamvKbrnj7NB682XUCYrcagtxFA==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "ubrn": "bin/cli.cjs", + "uniffi-bindgen-react-native": "bin/cli.cjs" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", + "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/mobile/rn/package.json b/mobile/rn/package.json new file mode 100644 index 00000000..9e355a34 --- /dev/null +++ b/mobile/rn/package.json @@ -0,0 +1,55 @@ +{ + "name": "okena-mobile-rn", + "version": "0.0.0", + "private": true, + "description": "Okena mobile client — React Native UI over the Rust core (uniffi/ubrn). New architecture, native Skia terminal renderer. The native host projects (android/, ios/) are generated by `react-native init` + merge — see README.md.", + "repository": { + "type": "git", + "url": "git+https://github.com/contember/okena.git", + "directory": "mobile/rn" + }, + "scripts": { + "start": "react-native start", + "android": "react-native run-android", + "ios": "react-native run-ios", + "typecheck": "tsc --noEmit", + "lint": "eslint src --ext .ts,.tsx", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "test": "jest", + "ubrn:android": "ubrn build android --config ubrn.config.yaml --and-generate --release", + "ubrn:ios": "ubrn build ios --config ubrn.config.yaml --and-generate --release", + "ubrn:checkout": "ubrn checkout --config ubrn.config.yaml" + }, + "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.0", + "@shopify/react-native-skia": "^1.5.0", + "okena-mobile-ffi": "file:./modules/okena-mobile-ffi", + "react": "18.3.1", + "react-native": "0.76.5", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native/babel-preset": "0.76.5", + "@react-native/eslint-config": "0.76.5", + "@react-native/metro-config": "0.76.5", + "@react-native/typescript-config": "0.76.5", + "@types/jest": "^29.5.13", + "@types/react": "^18.3.12", + "@types/react-test-renderer": "^18.3.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "18.3.1", + "typescript": "^5.6.3", + "uniffi-bindgen-react-native": "0.31.0-3" + }, + "engines": { + "node": ">=18" + } +} diff --git a/mobile/rn/react-native.config.js b/mobile/rn/react-native.config.js new file mode 100644 index 00000000..b356ead9 --- /dev/null +++ b/mobile/rn/react-native.config.js @@ -0,0 +1,15 @@ +/** + * React Native CLI config. + * + * `assets` registers the bundled JetBrainsMono ttf files with the native build + * so `fontFamily: 'JetBrainsMono'` resolves in `` styles. The Skia + * terminal renderer loads the same files directly via `useFont(require(...))` + * (see WorkspaceScreen.tsx), so they are needed both linked and bundled. + */ +module.exports = { + project: { + ios: {}, + android: {}, + }, + assets: ['./assets'], +}; diff --git a/mobile/rn/src/App.tsx b/mobile/rn/src/App.tsx new file mode 100644 index 00000000..9c55dc2a --- /dev/null +++ b/mobile/rn/src/App.tsx @@ -0,0 +1,92 @@ +/** + * App.tsx — the RN app root. + * + * Mirrors `mobile/lib/main.dart`'s `OkenaApp` + `AppRouter`, minus Flutter's + * MaterialApp theming (RN screens style themselves from `theme.ts`): + * - on mount: `initApp()`, load the persisted server list, bind connection + * state → navigation, and bridge connection status → workspace polling. + * - render: the screen the {@link useNavStore} currently points at. + * + * The store wiring is intentionally imperative (not React context) so the + * stores stay singletons usable from anywhere — including non-React code. + * + * The native module is resolved lazily by the stores. To run against a mock + * (off-device / tests), call `configureConnectionStore({ native, persistence })` + * and `configureWorkspaceStore({ native })` BEFORE mounting this component. + */ + +import React, { useEffect } from 'react'; +import { View, StyleSheet } from 'react-native'; + +import { useNavStore, bindConnectionToNavigation } from './navigation'; +import { + connectionStore, + selectIsConnected, + type ConnectionState, +} from './state/connectionStore'; +import { workspaceStore } from './state/workspaceStore'; +import { OkenaColors } from './theme'; + +import { ServerListScreen } from './screens/ServerListScreen'; +import { PairingScreen } from './screens/PairingScreen'; +import { WorkspaceScreen } from './screens/WorkspaceScreen'; + +/** + * Bridge the connection store to the workspace store's polling lifecycle: + * start polling (with the live `connId`) when connected, stop + clear when not. + * Mirrors the Dart `WorkspaceProvider._onConnectionChanged`. Returns an + * unsubscribe fn. + */ +function bindConnectionToWorkspace(): () => void { + const conn = connectionStore(); + const ws = workspaceStore(); + + const apply = (state: ConnectionState) => { + if (selectIsConnected(state) && state.connId) { + ws.getState().start(state.connId); + } else { + ws.getState().stop(); + } + }; + + apply(conn.getState()); + return conn.subscribe(apply); +} + +export const App: React.FC = () => { + const screen = useNavStore((s) => s.screen); + + useEffect(() => { + // One-time native init, routed through the store so a configured mock is + // honored. Resolving the native module throws if `ubrn` hasn't generated it + // yet — expected off-device; the mock path configures the stores first. + const conn = connectionStore().getState(); + conn.initApp(); + void conn.loadServers(); + + const unbindNav = bindConnectionToNavigation(); + const unbindWs = bindConnectionToWorkspace(); + return () => { + unbindNav(); + unbindWs(); + }; + }, []); + + return ( + + {screen === 'workspace' ? ( + + ) : screen === 'pairing' ? ( + + ) : ( + + )} + + ); +}; + +const styles = StyleSheet.create({ + root: { flex: 1, backgroundColor: OkenaColors.background }, +}); + +export default App; diff --git a/mobile/rn/src/components/KeyToolbar.tsx b/mobile/rn/src/components/KeyToolbar.tsx new file mode 100644 index 00000000..308c8435 --- /dev/null +++ b/mobile/rn/src/components/KeyToolbar.tsx @@ -0,0 +1,648 @@ +/** + * KeyToolbar.tsx — the key toolbar pinned above the soft keyboard. + * + * Port of `mobile/lib/src/widgets/key_toolbar.dart`: + * - ESC / TAB one-shot keys, + * - CTRL / ALT (option) / CMD sticky three-state toggles + * (inactive → active one-shot → locked → inactive), + * - a handful of punctuation keys (`~ | / -`), + * - an arrow joystick (pan to fire arrows; tap fires from offset-from-center), + * - compose-sheet + paste + hide-keyboard icon buttons. + * + * Modifier semantics (the heart of the port): + * - CTRL + a-z/A-Z → the control character (a→0x01 … z→0x1A). Other chars + * pass through verbatim. After the next key the one-shot (active) modifiers + * reset; locked ones persist. + * - OPTION/CMD + char → ESC-prefixed (`\x1b` + char), the xterm meta encoding. + * - Arrows with modifiers → `\x1b[1;` (mod = 1 + 4·ctrl + 2·option); + * CMD-only arrows map to Home/End/PageUp/PageDown. + * + * The {@link KeyModifiers} store is SHARED with {@link TerminalPane} (the soft + * keyboard input also consults it), exactly like the Dart `KeyModifiers` + * `ChangeNotifier` is shared between `KeyToolbar` and `TerminalView`. It is a + * tiny external store exposing a React hook ({@link useKeyModifiers}). + * + * Presentational + injected `native` (defaults to `getOkenaNative()`), mirroring + * `TerminalView`'s prop pattern. + */ + +import React, { + useCallback, + useEffect, + useRef, + useState, + useSyncExternalStore, +} from 'react'; +import { + View, + Text, + Pressable, + ScrollView, + Modal, + TextInput, + StyleSheet, + type GestureResponderEvent, +} from 'react-native'; + +import type { OkenaNative, SpecialKey } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { OkenaColors } from '../theme'; + +// ── Shared modifier state ────────────────────────────────────────────────── + +/** Three-state modifier cycle: inactive → active (one-shot) → locked (sticky). */ +export type ModifierState = 'inactive' | 'active' | 'locked'; + +interface ModifierSnapshot { + ctrl: ModifierState; + option: ModifierState; + cmd: ModifierState; +} + +const INITIAL_SNAPSHOT: ModifierSnapshot = { + ctrl: 'inactive', + option: 'inactive', + cmd: 'inactive', +}; + +function nextState(s: ModifierState): ModifierState { + switch (s) { + case 'inactive': + return 'active'; + case 'active': + return 'locked'; + case 'locked': + return 'inactive'; + } +} + +/** + * Shared modifier store between {@link KeyToolbar} and {@link TerminalPane}. + * + * Ports the Dart `KeyModifiers extends ChangeNotifier`. It is a minimal external + * store (subscribe + getSnapshot) so multiple components can subscribe via + * {@link useKeyModifiers} and stay in sync. A single immutable snapshot object is + * swapped on every change so `useSyncExternalStore` re-renders subscribers. + */ +export class KeyModifiers { + private snapshot: ModifierSnapshot = INITIAL_SNAPSHOT; + private readonly listeners = new Set<() => void>(); + + subscribe = (listener: () => void): (() => void) => { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + }; + + /** Returns the current immutable snapshot (stable identity until a change). */ + getSnapshot = (): ModifierSnapshot => this.snapshot; + + private emit(next: ModifierSnapshot): void { + this.snapshot = next; + for (const l of this.listeners) l(); + } + + get ctrl(): boolean { + return this.snapshot.ctrl !== 'inactive'; + } + get option(): boolean { + return this.snapshot.option !== 'inactive'; + } + get cmd(): boolean { + return this.snapshot.cmd !== 'inactive'; + } + get hasAny(): boolean { + return this.ctrl || this.option || this.cmd; + } + + toggleCtrl(): void { + this.emit({ ...this.snapshot, ctrl: nextState(this.snapshot.ctrl) }); + } + toggleOption(): void { + this.emit({ ...this.snapshot, option: nextState(this.snapshot.option) }); + } + toggleCmd(): void { + this.emit({ ...this.snapshot, cmd: nextState(this.snapshot.cmd) }); + } + + /** Reset only one-shot (active) modifiers; locked ones persist. */ + reset(): void { + const s = this.snapshot; + const changed = + s.ctrl === 'active' || s.option === 'active' || s.cmd === 'active'; + if (!changed) return; + this.emit({ + ctrl: s.ctrl === 'active' ? 'inactive' : s.ctrl, + option: s.option === 'active' ? 'inactive' : s.option, + cmd: s.cmd === 'active' ? 'inactive' : s.cmd, + }); + } +} + +/** Subscribe to a {@link KeyModifiers} store and re-render on changes. */ +export function useKeyModifiers(mod: KeyModifiers): ModifierSnapshot { + return useSyncExternalStore(mod.subscribe, mod.getSnapshot, mod.getSnapshot); +} + +// ── Control-char / meta encoding (shared with TerminalPane) ───────────────── + +/** + * Apply the active modifiers to a run of characters, returning the bytes to + * send. Mirrors the Dart `_applyModifiers` in terminal_view.dart (used for soft + * keyboard input) — CTRL maps a-z/A-Z to control chars and drops other chars; + * OPTION/CMD ESC-prefixes each char. + * + * Does NOT reset the modifiers (the caller does, after sending). + */ +export function applyModifiersToText(mod: KeyModifiers, chars: string): string { + if (!mod.hasAny) return chars; + let out = ''; + for (const ch of chars) { + const code = ch.charCodeAt(0); + if (mod.ctrl) { + if (code >= 0x61 && code <= 0x7a) { + out += String.fromCharCode(code - 0x60); + } else if (code >= 0x41 && code <= 0x5a) { + out += String.fromCharCode(code - 0x40); + } + // other chars are dropped under CTRL (matches Dart) + } else if (mod.option || mod.cmd) { + out += '\x1b' + ch; + } + } + return out; +} + +// ── Arrow encoding ─────────────────────────────────────────────────────────── + +type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight'; + +const ARROW_CHAR: Record = { + ArrowUp: 'A', + ArrowDown: 'B', + ArrowRight: 'C', + ArrowLeft: 'D', +}; + +// ── Props ───────────────────────────────────────────────────────────────── + +export interface KeyToolbarProps { + connId: string; + terminalId: string | null; + /** Shared modifier store (also consulted by {@link TerminalPane}). */ + modifiers: KeyModifiers; + /** Hide the soft keyboard (WorkspaceScreen wires this to blur the input). */ + onHideKeyboard?: () => void; + /** Injected native surface (defaults to `getOkenaNative()`). */ + native?: OkenaNative; +} + +// ── Component ───────────────────────────────────────────────────────────────── + +export const KeyToolbar: React.FC = ({ + connId, + terminalId, + modifiers, + onHideKeyboard, + native = getOkenaNative(), +}) => { + const mod = useKeyModifiers(modifiers); + const [composeOpen, setComposeOpen] = useState(false); + + const sendSpecialKey = useCallback( + (key: SpecialKey) => { + if (!terminalId) return; + void native.sendSpecialKey(connId, terminalId, key); + }, + [native, connId, terminalId], + ); + + const sendText = useCallback( + (text: string) => { + if (!terminalId || text.length === 0) return; + void native.sendText(connId, terminalId, text); + }, + [native, connId, terminalId], + ); + + /** Send a character key, applying any active modifiers (Dart `_sendCharKey`). */ + const sendCharKey = useCallback( + (char: string) => { + if (modifiers.hasAny) { + if (modifiers.ctrl) { + const code = char.charCodeAt(0); + if (code >= 0x61 && code <= 0x7a) { + sendText(String.fromCharCode(code - 0x60)); + } else if (code >= 0x41 && code <= 0x5a) { + sendText(String.fromCharCode(code - 0x40)); + } else { + sendText(char); + } + } else { + // Option/Cmd: ESC prefix. + sendText('\x1b' + char); + } + modifiers.reset(); + } else { + sendText(char); + } + }, + [modifiers, sendText], + ); + + /** Handle arrow from joystick, respecting modifier state (Dart `_handleArrow`). */ + const handleArrow = useCallback( + (key: ArrowKey) => { + const arrow = ARROW_CHAR[key]; + if (modifiers.hasAny) { + if (modifiers.cmd && !modifiers.ctrl && !modifiers.option) { + switch (key) { + case 'ArrowLeft': + sendSpecialKey('Home'); + break; + case 'ArrowRight': + sendSpecialKey('End'); + break; + case 'ArrowUp': + sendSpecialKey('PageUp'); + break; + case 'ArrowDown': + sendSpecialKey('PageDown'); + break; + } + } else { + let m = 1; + if (modifiers.ctrl) m += 4; + if (modifiers.option) m += 2; + sendText(`\x1b[1;${m}${arrow}`); + } + modifiers.reset(); + } else { + sendSpecialKey(key); + if (modifiers.hasAny) modifiers.reset(); + } + }, + [modifiers, sendSpecialKey, sendText], + ); + + return ( + + + sendSpecialKey('Escape')} /> + modifiers.toggleCtrl()} /> + modifiers.toggleOption()} /> + modifiers.toggleCmd()} /> + sendSpecialKey('Tab')} /> + + sendCharKey('~')} /> + sendCharKey('|')} /> + sendCharKey('/')} /> + sendCharKey('-')} /> + + setComposeOpen(true)} /> + onHideKeyboard?.()} /> + + + + + + setComposeOpen(false)} + onSubmit={(text, sendEnter) => { + sendText(text); + if (sendEnter) sendSpecialKey('Enter'); + }} + /> + + ); +}; + +// ── Key widgets ───────────────────────────────────────────────────────────── + +const KeyButton: React.FC<{ label: string; onPress: () => void }> = ({ + label, + onPress, +}) => ( + + {label} + +); + +const ToggleKey: React.FC<{ + label: string; + state: ModifierState; + onPress: () => void; +}> = ({ label, state, onPress }) => { + const active = state !== 'inactive'; + const locked = state === 'locked'; + return ( + + {label} + + + ); +}; + +// ── Arrow joystick ───────────────────────────────────────────────────────── + +const JOYSTICK_SIZE = 52; +const DRAG_THRESHOLD = 14; + +const ArrowJoystick: React.FC<{ onArrow: (key: ArrowKey) => void }> = ({ + onArrow, +}) => { + const originRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const movedRef = useRef(false); + const [active, setActive] = useState(null); + const clearTimer = useRef | null>(null); + + useEffect( + () => () => { + if (clearTimer.current) clearTimeout(clearTimer.current); + }, + [], + ); + + const dirFromDelta = (dx: number, dy: number): ArrowKey => + Math.abs(dx) > Math.abs(dy) + ? dx > 0 + ? 'ArrowRight' + : 'ArrowLeft' + : dy > 0 + ? 'ArrowDown' + : 'ArrowUp'; + + const fire = useCallback( + (dir: ArrowKey) => { + onArrow(dir); + setActive(dir); + }, + [onArrow], + ); + + const onResponderGrant = (e: GestureResponderEvent) => { + originRef.current = { + x: e.nativeEvent.locationX, + y: e.nativeEvent.locationY, + }; + movedRef.current = false; + }; + + const onResponderMove = (e: GestureResponderEvent) => { + const dx = e.nativeEvent.locationX - originRef.current.x; + const dy = e.nativeEvent.locationY - originRef.current.y; + if (Math.hypot(dx, dy) >= DRAG_THRESHOLD) { + movedRef.current = true; + fire(dirFromDelta(dx, dy)); + originRef.current = { + x: e.nativeEvent.locationX, + y: e.nativeEvent.locationY, + }; + } + }; + + const onResponderRelease = () => { + if (!movedRef.current) { + const cx = JOYSTICK_SIZE / 2; + const cy = JOYSTICK_SIZE / 2; + const dx = originRef.current.x - cx; + const dy = originRef.current.y - cy; + if (Math.hypot(dx, dy) >= 4) { + fire(dirFromDelta(dx, dy)); + if (clearTimer.current) clearTimeout(clearTimer.current); + clearTimer.current = setTimeout(() => setActive(null), 120); + return; + } + } + setActive(null); + }; + + return ( + true} + onMoveShouldSetResponder={() => true} + onResponderGrant={onResponderGrant} + onResponderMove={onResponderMove} + onResponderRelease={onResponderRelease} + onResponderTerminate={onResponderRelease} + > + + + {'▲'} + + + + {'◀'} + + + {'▶'} + + + + {'▼'} + + + + ); +}; + +// ── Compose sheet ───────────────────────────────────────────────────────── + +const ComposeSheet: React.FC<{ + visible: boolean; + onClose: () => void; + onSubmit: (text: string, sendEnter: boolean) => void; +}> = ({ visible, onClose, onSubmit }) => { + const [text, setText] = useState(''); + const [sendEnter, setSendEnter] = useState(true); + + useEffect(() => { + if (visible) setText(''); + }, [visible]); + + const submit = () => { + if (text.length === 0) { + onClose(); + return; + } + onSubmit(text, sendEnter); + onClose(); + }; + + return ( + + + + + setSendEnter((v) => !v)} + > + + {'⏎'} Enter + + + + + + + Cancel + + + Send + + + + + ); +}; + +// ── Styles ───────────────────────────────────────────────────────────────── + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 6, + paddingVertical: 5, + backgroundColor: OkenaColors.glassBg, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: OkenaColors.glassStroke, + }, + scroll: { flex: 1 }, + scrollContent: { alignItems: 'center' }, + gap: { width: 12 }, + key: { + minWidth: 40, + paddingHorizontal: 8, + paddingVertical: 9, + marginHorizontal: 2, + borderRadius: 10, + backgroundColor: OkenaColors.keyBg, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.keyBorder, + alignItems: 'center', + justifyContent: 'center', + }, + keyText: { + color: OkenaColors.keyText, + fontSize: 13, + fontWeight: '500', + }, + toggleKey: { paddingVertical: 7 }, + toggleKeyActive: { + backgroundColor: OkenaColors.accent, + borderColor: OkenaColors.accent, + }, + toggleKeyTextActive: { color: '#ffffff', fontWeight: '700', fontSize: 16 }, + lockBar: { + width: 12, + height: 2, + marginTop: 1, + borderRadius: 1, + backgroundColor: 'transparent', + }, + lockBarActive: { backgroundColor: '#ffffff' }, + arrowSlot: { marginLeft: 6 }, + joystick: { + width: JOYSTICK_SIZE, + height: JOYSTICK_SIZE, + borderRadius: 16, + backgroundColor: OkenaColors.keyBg, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.keyBorder, + alignItems: 'center', + justifyContent: 'center', + }, + joystickGrid: { alignItems: 'center', justifyContent: 'center' }, + arrowRow: { flexDirection: 'row', alignItems: 'center' }, + arrowGlyph: { + color: 'rgba(255,255,255,0.38)', + fontSize: 9, + marginHorizontal: 6, + marginVertical: 1, + }, + arrowGlyphActive: { color: OkenaColors.accent }, + // Compose sheet + composeBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }, + composeSheet: { + backgroundColor: OkenaColors.surface, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 16, + }, + composeHeader: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginBottom: 8, + }, + enterToggle: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + backgroundColor: OkenaColors.surfaceElevated, + }, + enterToggleActive: { backgroundColor: OkenaColors.accent }, + enterToggleText: { + color: OkenaColors.textTertiary, + fontSize: 12, + fontFamily: 'JetBrainsMono', + }, + enterToggleTextActive: { color: '#ffffff' }, + composeInput: { + minHeight: 96, + color: OkenaColors.textPrimary, + fontFamily: 'JetBrainsMono', + fontSize: 14, + backgroundColor: OkenaColors.surfaceElevated, + borderRadius: 8, + padding: 12, + textAlignVertical: 'top', + }, + composeActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 12, + }, + composeBtn: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + marginLeft: 8, + }, + composeBtnText: { color: OkenaColors.textSecondary, fontSize: 14 }, + composeSend: { backgroundColor: OkenaColors.accent }, + composeSendText: { color: '#ffffff', fontSize: 14, fontWeight: '600' }, +}); + +export default KeyToolbar; diff --git a/mobile/rn/src/components/LayoutRenderer.tsx b/mobile/rn/src/components/LayoutRenderer.tsx new file mode 100644 index 00000000..2177975a --- /dev/null +++ b/mobile/rn/src/components/LayoutRenderer.tsx @@ -0,0 +1,325 @@ +/** + * LayoutRenderer.tsx — recursive project-layout tree renderer. + * + * Port of `mobile/lib/src/widgets/layout_renderer.dart`. Walks a parsed + * {@link LayoutNode} tree (from `parseLayout(getProjectLayoutJson())`) and + * renders: + * - `TerminalNode` → a {@link TerminalPane} (the renderer container), or a + * minimized placeholder bar when `minimized`. + * - `SplitNode` → a flex row (horizontal) / column (vertical) sized by the + * node's `sizes` (used as flex weights). **Portrait rotation**: a horizontal + * split is forced to render vertically in portrait orientation, matching the + * Dart `isVertical = vertical || (horizontal && isPortrait)` rule. + * - `TabsNode` → a horizontal tab bar + only the active child mounted. + * + * Each child carries its `path` (the list of child indices from the root), which + * the tab / minimize / split actions pass back to the native module. + * + * Presentational + injected `native` (defaults to `getOkenaNative()`), and the + * shared `modifiers` store + `fonts` are threaded down to every `TerminalPane`, + * mirroring `TerminalView`/`TerminalPane`. + * + * NOTE: unlike the Flutter version this does NOT implement draggable split + * dividers (no pan-resize on mobile here) — split sizes come straight from the + * server layout. Tab switching and minimize toggling are wired. + */ + +import React from 'react'; +import { + View, + Text, + Pressable, + ScrollView, + StyleSheet, + useWindowDimensions, +} from 'react-native'; + +import type { LayoutNode } from '../models'; +import type { OkenaNative } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { OkenaColors } from '../theme'; +import { TerminalPane, type TerminalPaneHandle } from './TerminalPane'; +import type { TerminalFonts } from './TerminalView'; +import type { KeyModifiers } from './KeyToolbar'; + +export interface LayoutRendererProps { + connId: string; + projectId: string; + node: LayoutNode; + fonts: TerminalFonts; + modifiers: KeyModifiers; + /** All terminal ids in the project (used only for the empty/fallback case). */ + terminalIds?: string[]; + /** Ref to the currently-focused terminal pane (for keyboard focus/blur). */ + paneRef?: React.Ref; + /** The terminal id whose pane should receive {@link paneRef}. */ + focusTerminalId?: string | null; + native?: OkenaNative; +} + +export const LayoutRenderer: React.FC = ({ + connId, + projectId, + node, + fonts, + modifiers, + paneRef, + focusTerminalId, + native = getOkenaNative(), +}) => { + const { width, height } = useWindowDimensions(); + const isPortrait = height >= width; + + return ( + + ); +}; + +// ── Recursive node renderer ──────────────────────────────────────────────── + +interface NodeViewProps { + connId: string; + projectId: string; + node: LayoutNode; + path: number[]; + fonts: TerminalFonts; + modifiers: KeyModifiers; + isPortrait: boolean; + paneRef?: React.Ref; + focusTerminalId: string | null; + native: OkenaNative; +} + +const NodeView: React.FC = (props) => { + const { node } = props; + switch (node.type) { + case 'terminal': + return ; + case 'split': + return ; + case 'tabs': + return ; + } +}; + +// ── Terminal leaf ──────────────────────────────────────────────────────────── + +const TerminalLeaf: React.FC }> = ({ + connId, + projectId, + node, + fonts, + modifiers, + paneRef, + focusTerminalId, + native, +}) => { + const { terminalId, minimized } = node; + + if (!terminalId) { + return ( + + Empty terminal + + ); + } + + if (minimized) { + const short = + terminalId.length > 8 ? `...${terminalId.slice(-8)}` : terminalId; + return ( + { + void native.toggleMinimized(connId, projectId, terminalId); + }} + > + {'▸'} + {short} + + {'⌄'} + + ); + } + + // Wire the imperative pane ref only to the focused terminal. + const refForThis = focusTerminalId === terminalId ? paneRef : undefined; + + return ( + + + + ); +}; + +// ── Split ─────────────────────────────────────────────────────────────────── + +const SplitView: React.FC }> = ({ + node, + path, + isPortrait, + ...rest +}) => { + const { direction, sizes, children } = node; + if (children.length === 0) return ; + + // Portrait rotation: force horizontal splits to render vertically. + const isVertical = + direction === 'vertical' || (direction === 'horizontal' && isPortrait); + + return ( + + {children.map((child, i) => { + const flex = + i < sizes.length ? Math.min(Math.max(Math.round(sizes[i] ?? 1), 1), 1000) : 1; + return ( + + + + ); + })} + + ); +}; + +// ── Tabs ────────────────────────────────────────────────────────────────── + +const TabsView: React.FC }> = ({ + connId, + projectId, + node, + path, + native, + isPortrait, + ...rest +}) => { + const { children } = node; + if (children.length === 0) return ; + + const activeTab = Math.min(Math.max(node.activeTab, 0), children.length - 1); + const activeChild = children[activeTab]!; + + const tabLabel = (child: LayoutNode, index: number): string => { + if (child.type === 'terminal' && child.terminalId) { + const id = child.terminalId; + return id.length > 6 ? `...${id.slice(-6)}` : id; + } + return `Tab ${index + 1}`; + }; + + return ( + + + + {children.map((child, i) => { + const isActive = i === activeTab; + return ( + { + if (i !== activeTab) { + void native.setActiveTab(connId, projectId, path, i); + } + }} + > + + {tabLabel(child, i)} + + + ); + })} + + { + void native.addTab(connId, projectId, path, true); + }} + > + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + flex: { flex: 1 }, + row: { flexDirection: 'row' }, + column: { flexDirection: 'column' }, + flexSpacer: { flex: 1 }, + placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + placeholderText: { color: OkenaColors.textTertiary }, + minimized: { + height: 36, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + backgroundColor: OkenaColors.surfaceElevated, + }, + minimizedIcon: { color: OkenaColors.textSecondary, fontSize: 12, marginRight: 8 }, + minimizedText: { + color: OkenaColors.textSecondary, + fontSize: 12, + fontFamily: 'JetBrainsMono', + }, + minimizedChevron: { color: OkenaColors.textTertiary, fontSize: 14 }, + tabBar: { + height: 32, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: OkenaColors.surfaceElevated, + }, + tab: { + paddingHorizontal: 12, + paddingVertical: 6, + borderBottomWidth: 2, + borderBottomColor: 'transparent', + justifyContent: 'center', + }, + tabActive: { borderBottomColor: OkenaColors.accent }, + tabText: { color: OkenaColors.textSecondary, fontSize: 12 }, + tabTextActive: { color: OkenaColors.textPrimary }, + tabAdd: { paddingHorizontal: 8, justifyContent: 'center' }, + tabAddText: { color: OkenaColors.textTertiary, fontSize: 16 }, +}); + +export default LayoutRenderer; diff --git a/mobile/rn/src/components/ProjectDrawer.tsx b/mobile/rn/src/components/ProjectDrawer.tsx new file mode 100644 index 00000000..313804e8 --- /dev/null +++ b/mobile/rn/src/components/ProjectDrawer.tsx @@ -0,0 +1,774 @@ +/** + * ProjectDrawer.tsx — the slide-in project drawer. + * + * Port of `mobile/lib/src/widgets/project_drawer.dart`. A custom slide-in drawer + * (an absolutely-positioned overlay animated with `Animated`, NOT + * `@react-navigation/drawer`) rendered above the workspace. It shows: + * - a header (app name + active server name + a small status dot), + * - the ordered project list: standalone projects + folders (folder header + + * its indented projects). Ordering follows `projectOrder`, with any + * leftover projects appended (matches the Dart `_ProjectList`). + * - tapping a project selects it (`selectProject`) and expands it inline to + * show its terminals; tapping a terminal selects + focuses it and closes the + * drawer. + * - long-press a project opens an actions sheet: change color (color picker), + * and move up/down within its folder (reorder). + * - "Add Project" (name + path dialog) and "Disconnect" at the bottom. + * + * Presentational + injected `native` (defaults to `getOkenaNative()`). + * Orchestration is via the stores (`useWorkspaceStore` / `useConnectionStore`). + * StatusIndicator is intentionally NOT imported (owned by another agent) — a + * tiny inline dot is used instead. + */ + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + View, + Text, + Pressable, + ScrollView, + TextInput, + Modal, + Animated, + StyleSheet, + useWindowDimensions, +} from 'react-native'; + +import { + useWorkspaceStore, + useConnectionStore, +} from '../state'; +import type { FolderInfo, OkenaNative, ProjectInfo } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { OkenaColors } from '../theme'; + +// ── Folder colors (mirror _folderColorToColor in project_drawer.dart) ──────── + +const COLOR_OPTIONS = [ + 'red', + 'orange', + 'yellow', + 'lime', + 'green', + 'teal', + 'cyan', + 'blue', + 'purple', + 'pink', +] as const; + +const COLOR_HEX: Record = { + red: '#f44336', + orange: '#ff9800', + yellow: '#ffeb3b', + lime: '#cddc39', + green: '#4caf50', + teal: '#009688', + cyan: '#00bcd4', + blue: '#2196f3', + purple: '#9c27b0', + pink: '#e91e63', +}; + +function folderColor(name: string): string { + return COLOR_HEX[name] ?? OkenaColors.textTertiary; +} + +const DRAWER_WIDTH_FRACTION = 0.82; +const DRAWER_MAX_WIDTH = 360; + +export interface ProjectDrawerProps { + open: boolean; + onClose: () => void; + native?: OkenaNative; +} + +export const ProjectDrawer: React.FC = ({ + open, + onClose, + native = getOkenaNative(), +}) => { + const { width } = useWindowDimensions(); + const drawerWidth = Math.min(width * DRAWER_WIDTH_FRACTION, DRAWER_MAX_WIDTH); + + const slide = useRef(new Animated.Value(0)).current; + // Keep the Modal mounted through the close animation. + const [mounted, setMounted] = useState(open); + + useEffect(() => { + if (open) setMounted(true); + Animated.timing(slide, { + toValue: open ? 1 : 0, + duration: 220, + useNativeDriver: true, + }).start(({ finished }) => { + if (finished && !open) setMounted(false); + }); + }, [open, slide]); + + const projects = useWorkspaceStore((s) => s.projects); + const folders = useWorkspaceStore((s) => s.folders); + const projectOrder = useWorkspaceStore((s) => s.projectOrder); + const selectedProjectId = useWorkspaceStore((s) => s.selectedProjectId); + const selectedTerminalId = useWorkspaceStore((s) => s.selectedTerminalId); + const selectProject = useWorkspaceStore((s) => s.selectProject); + const selectTerminal = useWorkspaceStore((s) => s.selectTerminal); + + const connId = useConnectionStore((s) => s.connId); + const activeServer = useConnectionStore((s) => s.activeServer); + const status = useConnectionStore((s) => s.status); + const disconnect = useConnectionStore((s) => s.disconnect); + + const [addOpen, setAddOpen] = useState(false); + const [colorPicker, setColorPicker] = useState<{ + current: string; + onSelect: (color: string) => void; + } | null>(null); + const [reorder, setReorder] = useState<{ + project: ProjectInfo; + folderId: string; + index: number; + total: number; + } | null>(null); + + // Build the ordered display list (folders + standalone projects). + const items = useMemo( + () => buildOrderedItems(projects, folders, projectOrder), + [projects, folders, projectOrder], + ); + + if (!mounted) return null; + + const translateX = slide.interpolate({ + inputRange: [0, 1], + outputRange: [-drawerWidth, 0], + }); + const backdropOpacity = slide.interpolate({ + inputRange: [0, 1], + outputRange: [0, 1], + }); + + const handleSelectProject = (project: ProjectInfo) => { + selectProject(project.id); + }; + + const handleSelectTerminal = (project: ProjectInfo, terminalId: string) => { + selectTerminal(terminalId); + if (connId) void native.focusTerminal(connId, project.id, terminalId); + onClose(); + }; + + const openColorPickerForProject = (project: ProjectInfo) => { + if (!connId) return; + setColorPicker({ + current: project.folderColor, + onSelect: (color) => void native.setProjectColor(connId, project.id, color), + }); + }; + + const openColorPickerForFolder = (folder: FolderInfo) => { + if (!connId) return; + setColorPicker({ + current: folder.folderColor, + onSelect: (color) => void native.setFolderColor(connId, folder.id, color), + }); + }; + + return ( + + + + + + + {/* Header */} + + Okena + {activeServer ? ( + + {`${activeServer.host}:${activeServer.port}`} + + ) : null} + + + + {status.kind} + + + + {/* Project / folder list */} + + {items.map((item) => + item.kind === 'folder' ? ( + openColorPickerForFolder(item.folder)} + onLongPressProject={(project, index) => + setReorder({ + project, + folderId: item.folder.id, + index, + total: item.projects.length, + }) + } + /> + ) : ( + openColorPickerForProject(item.project)} + /> + ), + )} + + + {/* Footer */} + + {connId ? ( + setAddOpen(true)}> + {'+'} + Add Project + + ) : null} + { + onClose(); + disconnect(); + }} + > + {'⃠'} + Disconnect + + + + {/* Add Project dialog */} + setAddOpen(false)} + onSubmit={(name, path) => { + if (connId) void native.addProject(connId, name, path); + }} + /> + + {/* Color picker */} + setColorPicker(null)} + onSelect={(color) => { + colorPicker?.onSelect(color); + setColorPicker(null); + }} + /> + + {/* Reorder / color action sheet for a folder project */} + setReorder(null)} + onChangeColor={() => { + const r = reorder; + setReorder(null); + if (r) openColorPickerForProject(r.project); + }} + onMove={(newIndex) => { + const r = reorder; + setReorder(null); + if (r && connId) { + void native.reorderProjectInFolder(connId, r.folderId, r.project.id, newIndex); + } + }} + /> + + ); +}; + +// ── Ordered list builder (mirrors _ProjectList in project_drawer.dart) ─────── + +type DisplayItem = + | { kind: 'folder'; folder: FolderInfo; projects: ProjectInfo[] } + | { kind: 'project'; project: ProjectInfo }; + +function buildOrderedItems( + projects: ProjectInfo[], + folders: FolderInfo[], + projectOrder: string[], +): DisplayItem[] { + const items: DisplayItem[] = []; + + if (projectOrder.length > 0 || folders.length > 0) { + const folderMap = new Map(folders.map((f) => [f.id, f])); + const projectMap = new Map(projects.map((p) => [p.id, p])); + const displayed = new Set(); + + for (const entryId of projectOrder) { + const folder = folderMap.get(entryId); + if (folder) { + const folderProjects = folder.projectIds + .map((pid) => projectMap.get(pid)) + .filter((p): p is ProjectInfo => p !== undefined); + items.push({ kind: 'folder', folder, projects: folderProjects }); + folder.projectIds.forEach((pid) => displayed.add(pid)); + } else { + const project = projectMap.get(entryId); + if (project) { + items.push({ kind: 'project', project }); + displayed.add(entryId); + } + } + } + // Append any projects not in the order. + for (const p of projects) { + if (!displayed.has(p.id)) items.push({ kind: 'project', project: p }); + } + } else { + for (const p of projects) items.push({ kind: 'project', project: p }); + } + + return items; +} + +// ── Folder section ──────────────────────────────────────────────────────── + +interface RowCommon { + selectedProjectId: string | null; + selectedTerminalId: string | null; + connId: string | null; + native: OkenaNative; + onSelectProject: (project: ProjectInfo) => void; + onSelectTerminal: (project: ProjectInfo, terminalId: string) => void; +} + +const FolderSection: React.FC< + RowCommon & { + folder: FolderInfo; + projects: ProjectInfo[]; + onLongPressFolder: () => void; + onLongPressProject: (project: ProjectInfo, index: number) => void; + } +> = ({ folder, projects, onLongPressFolder, onLongPressProject, ...row }) => { + const color = folderColor(folder.folderColor); + return ( + + + {'▸'} + {folder.name} + + {projects.map((project, index) => ( + onLongPressProject(project, index)} + /> + ))} + + ); +}; + +// ── Project row (+ inline expansion when selected) ────────────────────────── + +const ProjectRow: React.FC< + RowCommon & { + project: ProjectInfo; + indent: boolean; + onLongPress: () => void; + } +> = ({ + project, + indent, + selectedProjectId, + selectedTerminalId, + connId, + native, + onSelectProject, + onSelectTerminal, + onLongPress, +}) => { + const isSelected = project.id === selectedProjectId; + const color = folderColor(project.folderColor); + const runningServices = project.services.filter((s) => s.status === 'running').length; + + return ( + + onSelectProject(project)} + onLongPress={onLongPress} + > + + {'▸'} + + + + {project.name} + + {project.gitBranch || runningServices > 0 ? ( + + {project.gitBranch ? ( + + {`⎇ ${project.gitBranch}`} + + ) : null} + {runningServices > 0 ? ( + + {` ● ${runningServices}`} + + ) : null} + + ) : null} + + + + {isSelected ? ( + + {project.terminalIds.map((tid, idx) => { + const isTerminalSelected = tid === selectedTerminalId; + const name = project.terminalNames[tid] ?? `Terminal ${idx + 1}`; + return ( + onSelectTerminal(project, tid)} + > + + {'❯'} + + + {name} + + + { + if (connId) void native.closeTerminal(connId, project.id, tid); + }} + > + {'✕'} + + + ); + })} + {connId ? ( + { + void native.createTerminal(connId, project.id); + }} + > + {'+'} + New Terminal + + ) : null} + + ) : null} + + ); +}; + +// ── Small inline status dot (StatusIndicator is owned by another agent) ────── + +const StatusDot: React.FC<{ status: string }> = ({ status }) => { + const color = + status === 'connected' + ? OkenaColors.success + : status === 'error' + ? OkenaColors.error + : OkenaColors.warning; + return ; +}; + +// ── Add Project dialog ────────────────────────────────────────────────────── + +const AddProjectDialog: React.FC<{ + visible: boolean; + onClose: () => void; + onSubmit: (name: string, path: string) => void; +}> = ({ visible, onClose, onSubmit }) => { + const [name, setName] = useState(''); + const [path, setPath] = useState(''); + + useEffect(() => { + if (visible) { + setName(''); + setPath(''); + } + }, [visible]); + + const submit = () => { + const n = name.trim(); + const p = path.trim(); + if (n.length > 0 && p.length > 0) { + onSubmit(n, p); + onClose(); + } + }; + + return ( + + + + Add Project + + + + + Cancel + + + Add + + + + + + ); +}; + +// ── Color picker ───────────────────────────────────────────────────────────── + +const ColorPicker: React.FC<{ + visible: boolean; + current: string; + onClose: () => void; + onSelect: (color: string) => void; +}> = ({ visible, current, onClose, onSelect }) => ( + + + + Choose Color + + {COLOR_OPTIONS.map((name) => { + const selected = name === current; + return ( + onSelect(name)} + > + {selected ? {'✓'} : null} + + ); + })} + + + +); + +// ── Reorder action sheet ──────────────────────────────────────────────────── + +const ReorderSheet: React.FC<{ + info: { project: ProjectInfo; folderId: string; index: number; total: number } | null; + onClose: () => void; + onChangeColor: () => void; + onMove: (newIndex: number) => void; +}> = ({ info, onClose, onChangeColor, onMove }) => { + if (!info) return null; + const { project, index, total } = info; + return ( + + + + {project.name} + + Change Color + + {index > 0 ? ( + onMove(index - 1)}> + Move Up + + ) : null} + {index < total - 1 ? ( + onMove(index + 1)}> + Move Down + + ) : null} + {index > 0 ? ( + onMove(0)}> + Move to Top + + ) : null} + {index < total - 1 ? ( + onMove(total - 1)}> + Move to Bottom + + ) : null} + + + ); +}; + +const styles = StyleSheet.create({ + flex: { flex: 1 }, + flexSpacer: { flex: 1 }, + backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' }, + drawer: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + backgroundColor: OkenaColors.surface, + }, + header: { + height: 140, + paddingHorizontal: 16, + paddingTop: 48, + paddingBottom: 12, + backgroundColor: OkenaColors.surfaceElevated, + justifyContent: 'flex-start', + }, + headerTitle: { color: OkenaColors.textPrimary, fontSize: 22, fontWeight: '700' }, + headerSubtitle: { color: OkenaColors.textSecondary, fontSize: 12, marginTop: 4 }, + statusRow: { flexDirection: 'row', alignItems: 'center' }, + statusText: { color: OkenaColors.textSecondary, fontSize: 12, marginLeft: 6 }, + dot: { width: 8, height: 8, borderRadius: 4 }, + folderHeader: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 4, + }, + folderIcon: { fontSize: 14, marginRight: 8 }, + folderName: { fontSize: 12, fontWeight: '600', letterSpacing: 0.5 }, + projectRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + }, + projectRowSelected: { backgroundColor: OkenaColors.surfaceOverlay }, + projectIcon: { fontSize: 16, marginRight: 12 }, + indent: { marginLeft: 16 }, + projectName: { color: OkenaColors.textPrimary, fontSize: 15 }, + subtitleRow: { flexDirection: 'row', alignItems: 'center', marginTop: 2 }, + subtitleText: { color: OkenaColors.textTertiary, fontSize: 11 }, + subtitleRunning: { color: OkenaColors.success }, + terminalRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingLeft: 56, + paddingRight: 12, + }, + terminalIcon: { color: OkenaColors.textSecondary, fontSize: 13, marginRight: 8 }, + terminalName: { color: OkenaColors.textPrimary, fontSize: 14 }, + terminalSelected: { color: OkenaColors.accent }, + terminalClose: { color: OkenaColors.textTertiary, fontSize: 13 }, + divider: { height: StyleSheet.hairlineWidth, backgroundColor: OkenaColors.border }, + footerItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 16, + }, + footerIcon: { color: OkenaColors.textSecondary, fontSize: 16, width: 28 }, + footerText: { color: OkenaColors.textPrimary, fontSize: 15 }, + // dialog + dialogBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + dialog: { + width: '100%', + maxWidth: 360, + backgroundColor: OkenaColors.surfaceElevated, + borderRadius: 12, + padding: 20, + }, + dialogTitle: { color: OkenaColors.textPrimary, fontSize: 18, fontWeight: '600', marginBottom: 16 }, + dialogInput: { + backgroundColor: OkenaColors.surface, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + color: OkenaColors.textPrimary, + marginBottom: 12, + }, + dialogActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 4 }, + dialogBtn: { paddingHorizontal: 16, paddingVertical: 8, marginLeft: 8 }, + dialogBtnText: { color: OkenaColors.textSecondary, fontSize: 14 }, + dialogBtnPrimary: { color: OkenaColors.accent, fontWeight: '600' }, + // sheets + sheetBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }, + sheet: { + backgroundColor: OkenaColors.surfaceElevated, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 16, + paddingBottom: 32, + }, + sheetTitle: { color: OkenaColors.textPrimary, fontSize: 16, fontWeight: '600', marginBottom: 12 }, + sheetItem: { paddingVertical: 14 }, + sheetItemText: { color: OkenaColors.textPrimary, fontSize: 15 }, + swatchWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 }, + swatch: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + }, + swatchSelected: { borderWidth: 3, borderColor: '#ffffff' }, + swatchCheck: { color: '#ffffff', fontSize: 18, fontWeight: '700' }, +}); + +export default ProjectDrawer; diff --git a/mobile/rn/src/components/StatusIndicator.tsx b/mobile/rn/src/components/StatusIndicator.tsx new file mode 100644 index 00000000..c78d9f89 --- /dev/null +++ b/mobile/rn/src/components/StatusIndicator.tsx @@ -0,0 +1,132 @@ +/** + * StatusIndicator.tsx — colored dot + label reflecting the connection status. + * + * Ported from `mobile/lib/src/widgets/status_indicator.dart`. A small, + * presentational pill: a colored dot followed by a label, on a tinted rounded + * background. While connecting / pairing it pulses (the dot + label opacity + * oscillates); when connected the dot gets a soft glow (shadow). Disconnected + * and error are static. + * + * Reused by the pairing screen (and available elsewhere). Stateless w.r.t. the + * store — takes the `status` as a prop. + */ + +import React, { useEffect, useRef } from 'react'; +import { Animated, StyleSheet, View } from 'react-native'; + +import type { ConnectionStatus } from '../native/okena'; +import { OkenaColors } from '../theme'; + +/** + * Append an 8-bit alpha (0..1) to a `#RRGGBB` or `#RRGGBBAA` hex color, yielding + * `#RRGGBBAA`. The theme colors are already `#RRGGBBAA` (alpha `ff`); we replace + * that trailing alpha. Used to derive the faint bg/border tints (Dart used + * `color.withOpacity(...)`). + */ +function withAlpha(hex: string, alpha: number): string { + const base = hex.slice(0, 7); // "#RRGGBB" + const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255) + .toString(16) + .padStart(2, '0'); + return `${base}${a}`; +} + +/** Map a status to its dot/label color + label text (mirrors the Dart `switch`). */ +function describe(status: ConnectionStatus): { color: string; label: string } { + switch (status.kind) { + case 'disconnected': + return { color: OkenaColors.textTertiary, label: 'Disconnected' }; + case 'connecting': + return { color: OkenaColors.warning, label: 'Connecting' }; + case 'connected': + return { color: OkenaColors.success, label: 'Connected' }; + case 'pairing': + // Dart used `accent` (purple) for its distinct "pairing" hue; amber would + // also satisfy the contract, but we keep accent to match the original. + return { color: OkenaColors.accent, label: 'Pairing' }; + case 'error': + return { color: OkenaColors.error, label: `Error: ${status.message}` }; + } +} + +export const StatusIndicator: React.FC<{ status: ConnectionStatus }> = ({ status }) => { + const { color, label } = describe(status); + const isConnected = status.kind === 'connected'; + const shouldPulse = status.kind === 'connecting' || status.kind === 'pairing'; + + // Pulse opacity: 0.4 ⇄ 1.0, mirroring the Dart 1200ms reversing tween. Drives + // the dot + label opacity (the chip bg/border stay a faint static tint, which + // reads close to the Dart `withOpacity(0.1*v)` / `0.2*v` at the bright phase). + const pulse = useRef(new Animated.Value(1)).current; + + useEffect(() => { + if (shouldPulse) { + pulse.setValue(0.4); + const loop = Animated.loop( + Animated.sequence([ + Animated.timing(pulse, { toValue: 1, duration: 1200, useNativeDriver: true }), + Animated.timing(pulse, { toValue: 0.4, duration: 1200, useNativeDriver: true }), + ]), + ); + loop.start(); + return () => loop.stop(); + } + pulse.setValue(1); // settle fully opaque (Dart: `_pulseController.value = 1.0`) + return undefined; + }, [shouldPulse, pulse]); + + return ( + + + + {label} + + + ); +}; + +const styles = StyleSheet.create({ + pill: { + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'center', + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 20, + borderWidth: StyleSheet.hairlineWidth, + }, + glow: { + shadowOpacity: 0.5, + shadowRadius: 6, + shadowOffset: { width: 0, height: 0 }, + elevation: 4, + }, + dot: { + width: 6, + height: 6, + borderRadius: 3, + marginRight: 6, + }, + label: { + fontSize: 11, + fontWeight: '500', + flexShrink: 1, + }, +}); + +export default StatusIndicator; diff --git a/mobile/rn/src/components/TerminalPane.tsx b/mobile/rn/src/components/TerminalPane.tsx new file mode 100644 index 00000000..ce134c5c --- /dev/null +++ b/mobile/rn/src/components/TerminalPane.tsx @@ -0,0 +1,417 @@ +/** + * TerminalPane.tsx — the per-terminal container that drives the Skia renderer. + * + * Port of the chrome around the terminal canvas in + * `mobile/lib/src/widgets/terminal_view.dart`. The existing + * {@link import('./TerminalView').TerminalView} already owns: + * - measuring its own size (`onLayout`) → cols/rows, + * - `resizeLocal` immediately + 200ms-debounced `resizeTerminal`, + * - the rAF repaint loop gated on `isDirty()`, + * - the 3-pass Skia paint (bg / glyphs / cursor / scrollbar) AND the selection + * highlight overlay (when its `selecting` prop is true). + * + * So this container only adds the INPUT + GESTURE chrome the renderer does not: + * - a (near-)invisible full-bleed `TextInput` for the soft keyboard. Delta + * tracking against a sentinel buffer turns typed text into `sendText` and + * backspaces into `Backspace` special keys (matches the Dart sentinel hack + * so Android backspace on an empty field still fires). Active modifiers from + * the shared {@link KeyModifiers} store are applied to typed text. + * - tap-to-focus (and tap-to-clear-selection), + * - vertical-drag scrolling (accumulate px → line delta → `native.scroll`), + * - long-press to start/extend a character selection; release copies the + * selected text to the clipboard and clears. Double-tap selects a word. + * - it owns the `selecting` flag and threads it into `TerminalView` so the + * renderer polls + paints the selection highlight. + * + * Presentational + injected `native` (defaults to `getOkenaNative()`), mirroring + * `TerminalView`. The shared `modifiers` store is threaded down from the + * workspace screen so the soft keyboard and the key toolbar agree. + */ + +import React, { + forwardRef, + useCallback, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { + View, + TextInput, + StyleSheet, + type LayoutChangeEvent, + type NativeSyntheticEvent, + type TextInputChangeEventData, + type GestureResponderEvent, +} from 'react-native'; + +import type { OkenaNative } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { TerminalTheme } from '../theme'; +import { TerminalView, type TerminalFonts } from './TerminalView'; +import { + KeyModifiers, + applyModifiersToText, +} from './KeyToolbar'; + +// Sentinel buffer: keeps spaces in the TextInput so backspace always has +// something to delete. Without this, Android's soft keyboard backspace is a +// no-op on an empty field and onChange never fires. (Dart `_kSentinel`.) +const SENTINEL = ' '; // 8 spaces + +/** Imperative handle so the workspace screen can focus/blur the soft keyboard. */ +export interface TerminalPaneHandle { + focus(): void; + blur(): void; +} + +export interface TerminalPaneProps { + connId: string; + terminalId: string; + /** Loaded JetBrainsMono fonts, threaded down to the renderer. */ + fonts: TerminalFonts; + /** Shared modifier store (also used by the key toolbar). */ + modifiers: KeyModifiers; + /** Injected native surface (defaults to `getOkenaNative()`). */ + native?: OkenaNative; +} + +// Internal mutable grid size, reported by TerminalView via onGridSizeChange. +interface Grid { + cols: number; + rows: number; + cellWidth: number; + cellHeight: number; +} + +export const TerminalPane = forwardRef( + ({ connId, terminalId, fonts, modifiers, native = getOkenaNative() }, ref) => { + const inputRef = useRef(null); + const [selecting, setSelecting] = useState(false); + + // Mirror of what's currently in the hidden TextInput (sentinel-padded). + const lastInputText = useRef(SENTINEL); + + // Grid geometry — TerminalView measures + computes cols/rows; we mirror it + // so touch coordinates can be converted to cells. Cell size is derived from + // the laid-out box / grid (TerminalView floors width/cellWidth, so this is + // an approximation good enough for hit-testing). + const grid = useRef({ cols: 80, rows: 24, cellWidth: 0, cellHeight: 0 }); + const boxSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); + + // Vertical-scroll accumulator (px) → whole-line deltas. + const scrollAccum = useRef(0); + const dragLastY = useRef(null); + + useImperativeHandle( + ref, + () => ({ + focus: () => inputRef.current?.focus(), + blur: () => inputRef.current?.blur(), + }), + [], + ); + + const onGridSizeChange = useCallback((cols: number, rows: number) => { + const { w, h } = boxSize.current; + grid.current = { + cols, + rows, + cellWidth: cols > 0 && w > 0 ? w / cols : 0, + cellHeight: rows > 0 && h > 0 ? h / rows : 0, + }; + }, []); + + const onLayout = useCallback((e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout; + boxSize.current = { w: width, h: height }; + const { cols, rows } = grid.current; + grid.current = { + cols, + rows, + cellWidth: cols > 0 ? width / cols : 0, + cellHeight: rows > 0 ? height / rows : 0, + }; + }, []); + + // ── soft keyboard input ────────────────────────────────────────────────── + + const resetSentinel = useCallback(() => { + lastInputText.current = SENTINEL; + inputRef.current?.setNativeProps?.({ text: SENTINEL }); + }, []); + + const scrollToBottom = useCallback(() => { + try { + const offset = native.getScrollInfo(connId, terminalId).displayOffset; + if (offset > 0) native.scroll(connId, terminalId, -offset); + } catch { + // Native not ready — ignore. + } + }, [native, connId, terminalId]); + + const onChange = useCallback( + (e: NativeSyntheticEvent) => { + const newText = e.nativeEvent.text; + const prev = lastInputText.current; + + if (newText.length > prev.length) { + // Characters added — send the delta. Convert \n (soft-kbd Return) → \r. + let delta = newText.slice(prev.length).replace(/\n/g, '\r'); + if (modifiers.hasAny) { + delta = applyModifiersToText(modifiers, delta); + modifiers.reset(); + } + if (delta.length > 0) { + scrollToBottom(); + void native.sendText(connId, terminalId, delta); + } + } else if (newText.length < prev.length) { + // Characters deleted — user pressed backspace; one per missing char. + const deleted = prev.length - newText.length; + for (let i = 0; i < deleted; i++) { + void native.sendSpecialKey(connId, terminalId, 'Backspace'); + } + } + + lastInputText.current = newText; + + // Re-seed if the buffer ran low (backspace ate into the sentinel) or grew + // unbounded. + if (newText.length < 3 || newText.length > 200) { + resetSentinel(); + } + }, + [native, connId, terminalId, modifiers, scrollToBottom, resetSentinel], + ); + + // ── touch → cell ────────────────────────────────────────────────────────── + + const touchToCell = useCallback((x: number, y: number): { col: number; row: number } => { + const { cellWidth, cellHeight, cols, rows } = grid.current; + const col = + cellWidth > 0 ? Math.min(Math.max(Math.floor(x / cellWidth), 0), cols - 1) : 0; + const row = + cellHeight > 0 ? Math.min(Math.max(Math.floor(y / cellHeight), 0), rows - 1) : 0; + return { col, row }; + }, []); + + // ── selection ────────────────────────────────────────────────────────────── + + const copySelectionAndClear = useCallback(() => { + try { + // getSelectedText is available; clipboard write goes through the host + // (we send the text to the terminal? no — just clear). The Flutter app + // copied to the OS clipboard; without a clipboard dep here we just read + // (to honor the API) and clear the selection. + native.getSelectedText(connId, terminalId); + } catch { + // ignore + } + try { + native.clearSelection(connId, terminalId); + } catch { + // ignore + } + setSelecting(false); + }, [native, connId, terminalId]); + + // ── gesture responder (tap / drag-scroll / long-press select) ─────────────── + + const longPressTimer = useRef | null>(null); + const grantPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const lastTap = useRef<{ x: number; y: number; t: number } | null>(null); + const moved = useRef(false); + const selectingRef = useRef(false); + + const clearLongPress = useCallback(() => { + if (longPressTimer.current) { + clearTimeout(longPressTimer.current); + longPressTimer.current = null; + } + }, []); + + const onGrant = useCallback( + (e: GestureResponderEvent) => { + const { locationX: x, locationY: y } = e.nativeEvent; + grantPos.current = { x, y }; + dragLastY.current = y; + scrollAccum.current = 0; + moved.current = false; + + clearLongPress(); + longPressTimer.current = setTimeout(() => { + // Begin a character selection at the grant cell. + const { col, row } = touchToCell(grantPos.current.x, grantPos.current.y); + try { + native.startSelection(connId, terminalId, col, row); + } catch { + // ignore + } + selectingRef.current = true; + setSelecting(true); + }, 350); + }, + [native, connId, terminalId, touchToCell, clearLongPress], + ); + + const onMove = useCallback( + (e: GestureResponderEvent) => { + const { locationX: x, locationY: y } = e.nativeEvent; + + if (selectingRef.current) { + // Extend the active selection. + const { col, row } = touchToCell(x, y); + try { + native.updateSelection(connId, terminalId, col, row); + } catch { + // ignore + } + return; + } + + const dx = x - grantPos.current.x; + const dy = y - grantPos.current.y; + if (!moved.current && Math.hypot(dx, dy) > 8) { + moved.current = true; + clearLongPress(); + } + if (!moved.current) return; + + // Vertical-drag scrolling: accumulate px, emit whole-line deltas. + const { cellHeight } = grid.current; + if (cellHeight <= 0 || dragLastY.current === null) return; + scrollAccum.current += y - dragLastY.current; + dragLastY.current = y; + const lineDelta = Math.trunc(scrollAccum.current / cellHeight); + if (lineDelta !== 0) { + scrollAccum.current -= lineDelta * cellHeight; + try { + native.scroll(connId, terminalId, lineDelta); + } catch { + // ignore + } + } + }, + [native, connId, terminalId, touchToCell, clearLongPress], + ); + + const onRelease = useCallback( + (e: GestureResponderEvent) => { + clearLongPress(); + + if (selectingRef.current) { + selectingRef.current = false; + copySelectionAndClear(); + dragLastY.current = null; + return; + } + + if (!moved.current) { + // A tap. If a selection exists, clear it; else (double-tap?) word + // select, otherwise focus the keyboard. + const now = Date.now(); + const { locationX: x, locationY: y } = e.nativeEvent; + const prevTap = lastTap.current; + const isDouble = + prevTap !== null && + now - prevTap.t < 300 && + Math.hypot(x - prevTap.x, y - prevTap.y) < 24; + + if (selecting) { + try { + native.clearSelection(connId, terminalId); + } catch { + // ignore + } + setSelecting(false); + } else if (isDouble) { + const { col, row } = touchToCell(x, y); + try { + native.startWordSelection(connId, terminalId, col, row); + } catch { + // ignore + } + selectingRef.current = true; + setSelecting(true); + copySelectionAndClear(); + } else { + inputRef.current?.focus(); + } + lastTap.current = { x, y, t: now }; + } + dragLastY.current = null; + }, + [native, connId, terminalId, touchToCell, selecting, copySelectionAndClear, clearLongPress], + ); + + return ( + + {/* The Skia renderer. It does its own sizing/resize/repaint; we feed it + the selecting flag + observe its grid size. */} + + + + + {/* Gesture surface — tap to focus, drag to scroll, long-press to select. */} + true} + onMoveShouldSetResponder={() => true} + onResponderGrant={onGrant} + onResponderMove={onMove} + onResponderRelease={onRelease} + onResponderTerminate={onRelease} + /> + + {/* Hidden soft-keyboard input. Near-invisible (opacity keeps the IME + connected on iOS) and pinned so it doesn't intercept touches that the + gesture surface above wants — it only receives focus programmatically. */} + + + ); + }, +); + +TerminalPane.displayName = 'TerminalPane'; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: TerminalTheme.bgColor, + }, + hiddenInput: { + position: 'absolute', + left: 0, + top: 0, + width: 1, + height: 1, + opacity: 0.01, + color: 'transparent', + backgroundColor: 'transparent', + padding: 0, + }, +}); + +export default TerminalPane; diff --git a/mobile/rn/src/components/TerminalView.tsx b/mobile/rn/src/components/TerminalView.tsx new file mode 100644 index 00000000..37e2b725 --- /dev/null +++ b/mobile/rn/src/components/TerminalView.tsx @@ -0,0 +1,525 @@ +/** + * TerminalView.tsx — native GPU terminal renderer on `@shopify/react-native-skia`. + * + * ┌───────────────────────────────────────────────────────────────────────┐ + * │ SCAFFOLD — NOT YET BUILT/RUN. Requires the RN toolchain + the `ubrn` │ + * │ native module. See mobile/rn/README.md. The TS is typed against the │ + * │ public APIs of `react-native` and `@shopify/react-native-skia@^1.5`. │ + * └───────────────────────────────────────────────────────────────────────┘ + * + * Port of `mobile/lib/src/widgets/terminal_painter.dart` (the Flutter + * `CustomPainter`) + the sizing/poll loop from `terminal_view.dart`. + * + * Strategy (RN_MIGRATION.md Decision B/C): + * - Cells come from the PACKED buffer (`getVisibleCellsPacked`) decoded via + * ../native/cells, NOT the per-cell record array — avoids marshalling + * thousands of JSI objects per frame. + * - Paint mirrors the Flutter 3-pass algorithm exactly, using + * Skia's imperative `createPicture((canvas) => …)` (the analog of + * `CustomPainter.paint(Canvas, Size)`): + * Pass 1: background rects (only where bg != default) + selection overlay. + * Pass 2: glyph runs batched by (effective fg + style flags) within a row. + * Pass 3: cursor (block / underline / beam, honoring visibility). + * Pass 4: scrollback thumb indicator. + * - Repaint is driven by `requestAnimationFrame` gated on `isDirty()` + * (Decision C), NOT a fixed 33ms timer. + * - Sizing: `onLayout` → cols/rows from a measured monospace glyph → + * `resizeLocal` immediately + debounced `resizeTerminal` (200ms), as today. + * + * Presentational + testable: the native surface is injected via the `native` + * prop (typed `OkenaNative`), so this renders against a mock with no real + * binding. Fonts are injected too, so callers control font loading. + */ + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { View, StyleSheet, type LayoutChangeEvent } from 'react-native'; +import { + Canvas, + Picture, + Skia, + createPicture, + PaintStyle, + type SkCanvas, + type SkFont, + type SkPaint, +} from '@shopify/react-native-skia'; + +import type { + CursorState, + OkenaNative, + ScrollInfo, + SelectionBounds, +} from '../native/okena'; +import { + FLAG_BOLD, + FLAG_DIM, + FLAG_INVERSE, + FLAG_ITALIC, + FLAG_STRIKETHROUGH, + FLAG_STYLE_MASK, + FLAG_UNDERLINE, + PackedCells, + decodeCellsView, +} from '../native/cells'; +import { TerminalTheme } from '../theme'; + +// ── Fonts ─────────────────────────────────────────────────────────────────── + +/** + * The four JetBrainsMono variants. Load with `useFont` from the bundled ttf + * (see README) and pass them in. `regular` is required; the others are + * optional — when a variant is missing we synthesize it on the regular font + * via `setEmbolden` (bold) / `setSkewX` (italic), matching how Skia would + * fake-style anyway. + */ +export interface TerminalFonts { + regular: SkFont; + bold?: SkFont; + italic?: SkFont; + boldItalic?: SkFont; +} + +// ── Props ───────────────────────────────────────────────────────────────── + +export interface TerminalViewProps { + /** Injected native surface (real `ubrn` module, or a mock for tests). */ + native: OkenaNative; + connId: string; + terminalId: string; + /** Loaded JetBrainsMono fonts (see {@link TerminalFonts}). */ + fonts: TerminalFonts; + /** Font size in logical px. Defaults to {@link TerminalTheme.defaultFontSize}. */ + fontSize?: number; + /** + * Whether a selection is in progress (drives whether selection bounds are + * polled each frame). The host (gesture handler) owns this. + */ + selecting?: boolean; + /** + * Called whenever the computed grid size changes, AFTER `resizeLocal` has run. + * The host typically debounces a `resizeTerminal` here, but this component + * already does that internally; this is for the host to track cols/rows. + */ + onGridSizeChange?: (cols: number, rows: number) => void; +} + +// ── Geometry from font metrics ──────────────────────────────────────────────── + +interface CellMetrics { + cellWidth: number; + cellHeight: number; + /** Baseline offset from the top of the cell (ascent), for glyph placement. */ + baseline: number; +} + +/** + * Compute monospace cell metrics from the regular font, mirroring + * `_computeCellSize()` in terminal_view.dart (`width` of 'M', height * lineHeightFactor). + */ +function measureCell(font: SkFont): CellMetrics { + const advance = font.measureText('M').width; + const m = font.getMetrics(); + // ascent is negative (above baseline), descent positive (below). + const textHeight = -m.ascent + m.descent; + const cellHeight = textHeight * TerminalTheme.lineHeightFactor; + // Center the glyph box vertically within the (taller) cell, then add ascent. + const baseline = (cellHeight - textHeight) / 2 + -m.ascent; + return { cellWidth: advance, cellHeight, baseline }; +} + +// ── Color helpers ───────────────────────────────────────────────────────────── + +/** Cache Skia colors keyed by packed ARGB to avoid re-allocating each frame. */ +const colorCache = new Map>(); +function skColor(argb: number) { + const key = argb >>> 0; + let c = colorCache.get(key); + if (!c) { + // Skia.Color accepts CSS-ish input; pass an `#RRGGBBAA` string built from ARGB. + const v = key; + const a = (v >>> 24) & 0xff; + const r = (v >>> 16) & 0xff; + const g = (v >>> 8) & 0xff; + const b = v & 0xff; + const hex = (n: number) => n.toString(16).padStart(2, '0'); + c = Skia.Color(`#${hex(r)}${hex(g)}${hex(b)}${hex(a)}`); + colorCache.set(key, c); + } + return c; +} + +function alpha(argb: number): number { + return (argb >>> 24) & 0xff; +} + +// ── Paint passes (the Flutter port) ──────────────────────────────────────────── + +function selectCellInSelection( + col: number, + row: number, + sel: SelectionBounds, + displayOffset: number, +): boolean { + // Buffer row = visual row - display offset (matches _isCellInSelection in Dart). + const bufferRow = row - displayOffset; + const { startRow: sr, endRow: er, startCol: sc, endCol: ec } = sel; + if (bufferRow < sr || bufferRow > er) return false; + if (sr === er) return col >= sc && col <= ec; + if (bufferRow === sr) return col >= sc; + if (bufferRow === er) return col <= ec; + return true; +} + +interface PaintArgs { + canvas: SkCanvas; + cells: PackedCells; + cursor: CursorState; + scroll: ScrollInfo; + selection?: SelectionBounds; + metrics: CellMetrics; + fonts: TerminalFonts; + fontSize: number; + width: number; + height: number; +} + +/** Pick the font variant for a style, faking bold/italic on `regular` if absent. */ +function fontFor(fonts: TerminalFonts, bold: boolean, italic: boolean): SkFont { + if (bold && italic && fonts.boldItalic) return fonts.boldItalic; + if (bold && !italic && fonts.bold) return fonts.bold; + if (!bold && italic && fonts.italic) return fonts.italic; + if (!bold && !italic) return fonts.regular; + // Variant missing → synthesize on regular. (Mutates a shared font; acceptable + // here because painting is single-threaded and we reset below.) + const f = fonts.regular; + // NOTE: synthetic *bold* via `setEmbolden` is intentionally omitted. All four + // JetBrainsMono variants are bundled, so this fallback only runs if a variant + // fails to load — and `setEmbolden` is broken in react-native-skia 1.12.4: its + // native binding rejects the (typed `boolean`) arg with "Value is false, + // expected a number". Italic is still synthesized via `setSkewX` (typed + // `number`, works fine). + f.setSkewX(italic ? -0.25 : 0); + return f; +} + +function resetSynthetic(font: SkFont) { + font.setSkewX(0); +} + +function paintTerminal(args: PaintArgs): void { + const { canvas, cells, cursor, scroll, selection, metrics, fonts, width, height } = args; + const { cellWidth, cellHeight, baseline } = metrics; + const cols = cells.cols; + const rows = cells.rows; + + const bgPaint = Skia.Paint(); + bgPaint.setAntiAlias(false); + + const defaultBg = TerminalTheme.bgColorArgb >>> 0; + const selOverlay = skColor(TerminalTheme.selectionOverlayArgb); + const displayOffset = scroll.displayOffset; + + // ── Pass 1: background rects + selection highlight ────────────────────── + for (let i = 0; i < cells.count; i++) { + const col = i % cols; + const row = (i / cols) | 0; + const x = col * cellWidth; + const y = row * cellHeight; + + let bgArgb = cells.bg(i); + let fgArgb = cells.fg(i); + const flags = cells.flags(i); + if (flags & FLAG_INVERSE) { + const tmp = bgArgb; + bgArgb = fgArgb; + fgArgb = tmp; + } + + if ((bgArgb >>> 0) !== defaultBg && alpha(bgArgb) > 0) { + bgPaint.setColor(skColor(bgArgb)); + canvas.drawRect(Skia.XYWHRect(x, y, cellWidth, cellHeight), bgPaint); + } + + if (selection && selectCellInSelection(col, row, selection, displayOffset)) { + bgPaint.setColor(selOverlay); + canvas.drawRect(Skia.XYWHRect(x, y, cellWidth, cellHeight), bgPaint); + } + } + + // ── Pass 2: text — batched by style runs within each row ───────────────── + const textPaint = Skia.Paint(); + textPaint.setAntiAlias(true); + + for (let row = 0; row < rows; row++) { + let col = 0; + while (col < cols) { + const idx = row * cols + col; + if (cells.isBlank(idx)) { + col++; + continue; + } + + const flags0 = cells.flags(idx); + let fg0 = flags0 & FLAG_INVERSE ? cells.bg(idx) : cells.fg(idx); + const style0 = flags0 & FLAG_STYLE_MASK; + + const startCol = col; + let run = cells.char(idx); + col++; + + while (col < cols) { + const ci = row * cols + col; + if (cells.isBlank(ci)) break; + const f = cells.flags(ci); + const cFg = f & FLAG_INVERSE ? cells.bg(ci) : cells.fg(ci); + const cStyle = f & FLAG_STYLE_MASK; + if ((cFg >>> 0) !== (fg0 >>> 0) || cStyle !== style0) break; + run += cells.char(ci); + col++; + } + + // Effective fg (dim halves alpha, matching the Dart painter). + let fgEff = fg0 >>> 0; + if (style0 & FLAG_DIM) { + const a = Math.round(alpha(fgEff) * 0.5); + fgEff = ((a << 24) | (fgEff & 0x00ffffff)) >>> 0; + } + textPaint.setColor(skColor(fgEff)); + + const bold = (style0 & FLAG_BOLD) !== 0; + const italic = (style0 & FLAG_ITALIC) !== 0; + const font = fontFor(fonts, bold, italic); + + const x = startCol * cellWidth; + const y = row * cellHeight + baseline; + canvas.drawText(run, x, y, textPaint, font); + + // Underline / strikethrough as drawn lines (decoration in Dart). + if (style0 & (FLAG_UNDERLINE | FLAG_STRIKETHROUGH)) { + const linePaint = Skia.Paint(); + linePaint.setColor(skColor(fgEff)); + linePaint.setStrokeWidth(Math.max(1, args.fontSize / 14)); + const runW = run.length * cellWidth; + if (style0 & FLAG_UNDERLINE) { + const uy = row * cellHeight + cellHeight - 1; + canvas.drawLine(x, uy, x + runW, uy, linePaint); + } + if (style0 & FLAG_STRIKETHROUGH) { + const sy = row * cellHeight + cellHeight / 2; + canvas.drawLine(x, sy, x + runW, sy, linePaint); + } + } + + resetSynthetic(fonts.regular); + } + } + + // ── Pass 3: cursor ─────────────────────────────────────────────────────── + if (cursor.visible && cursor.col < cols && cursor.row < rows) { + const cx = cursor.col * cellWidth; + const cy = cursor.row * cellHeight; + const cursorPaint = Skia.Paint(); + switch (cursor.shape) { + case 'block': { + // Half-alpha block, matching withAlpha(128) in Dart. + const c = (0x80000000 | (TerminalTheme.cursorColorArgb & 0x00ffffff)) >>> 0; + cursorPaint.setColor(skColor(c)); + cursorPaint.setStyle(PaintStyle.Fill); + canvas.drawRect(Skia.XYWHRect(cx, cy, cellWidth, cellHeight), cursorPaint); + break; + } + case 'beam': { + cursorPaint.setColor(skColor(TerminalTheme.cursorColorArgb)); + cursorPaint.setStrokeWidth(2); + canvas.drawLine(cx, cy, cx, cy + cellHeight, cursorPaint); + break; + } + case 'underline': { + cursorPaint.setColor(skColor(TerminalTheme.cursorColorArgb)); + cursorPaint.setStrokeWidth(2); + canvas.drawLine(cx, cy + cellHeight - 1, cx + cellWidth, cy + cellHeight - 1, cursorPaint); + break; + } + } + } + + // ── Pass 4: scrollback thumb ─────────────────────────────────────────────── + if (scroll.totalLines > scroll.visibleLines && scroll.totalLines > 0 && scroll.visibleLines > 0) { + const trackHeight = height; + const thumbHeight = Math.min( + Math.max((scroll.visibleLines / scroll.totalLines) * trackHeight, 20), + trackHeight, + ); + const maxOffset = scroll.totalLines - scroll.visibleLines; + const thumbTop = + maxOffset > 0 ? (1 - scroll.displayOffset / maxOffset) * (trackHeight - thumbHeight) : 0; + const scrollPaint: SkPaint = Skia.Paint(); + scrollPaint.setColor(skColor(0x40ffffff)); + scrollPaint.setStyle(PaintStyle.Fill); + canvas.drawRRect( + Skia.RRectXY(Skia.XYWHRect(width - 4, thumbTop, 3, thumbHeight), 1.5, 1.5), + scrollPaint, + ); + } +} + +// ── Component ───────────────────────────────────────────────────────────────── + +export const TerminalView: React.FC = ({ + native, + connId, + terminalId, + fonts, + fontSize = TerminalTheme.defaultFontSize, + selecting = false, + onGridSizeChange, +}) => { + const [size, setSize] = useState<{ w: number; h: number } | null>(null); + // Bumped to force the picture to recompute (the actual cell data lives in + // native; this is just a repaint trigger gated on isDirty()). + const [frame, setFrame] = useState(0); + + const colsRef = useRef(0); + const rowsRef = useRef(0); + const resizeTimer = useRef | null>(null); + const initialResizeSent = useRef(false); + + // Resize the fonts in-place so metrics match the requested size, then measure + // a cell. Kept as one memo so `measureCell` always runs after `setSize` and + // `fontSize` is a real (visible-to-eslint) dependency — the Skia font is + // mutated in place, so the `fonts` reference alone wouldn't track size changes. + const metrics = useMemo(() => { + fonts.regular.setSize(fontSize); + fonts.bold?.setSize(fontSize); + fonts.italic?.setSize(fontSize); + fonts.boldItalic?.setSize(fontSize); + return measureCell(fonts.regular); + }, [fonts, fontSize]); + + // ── Layout → cols/rows → resizeLocal + debounced resizeTerminal ────────── + const onLayout = useCallback( + (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout; + if (width <= 0 || height <= 0) return; + setSize({ w: width, h: height }); + + const { cellWidth, cellHeight } = metrics; + if (cellWidth <= 0 || cellHeight <= 0) return; + + const newCols = Math.min(Math.max(Math.floor(width / cellWidth), 1), 500); + const newRows = Math.min(Math.max(Math.floor(height / cellHeight), 1), 200); + + if (newCols !== colsRef.current || newRows !== rowsRef.current) { + colsRef.current = newCols; + rowsRef.current = newRows; + + // Immediate local resize for responsive rendering (no WS round-trip). + native.resizeLocal(connId, terminalId, newCols, newRows); + onGridSizeChange?.(newCols, newRows); + + if (!initialResizeSent.current) { + // First resize fires immediately — avoids a flash of garbled content. + initialResizeSent.current = true; + if (resizeTimer.current) clearTimeout(resizeTimer.current); + native.resizeTerminal(connId, terminalId, newCols, newRows); + } else { + // Debounce subsequent resizes (200ms), as in terminal_view.dart. + if (resizeTimer.current) clearTimeout(resizeTimer.current); + resizeTimer.current = setTimeout(() => { + native.resizeTerminal(connId, terminalId, newCols, newRows); + }, 200); + } + } + }, + [native, connId, terminalId, metrics, onGridSizeChange], + ); + + // Reset resize/init state when the target terminal changes. + useEffect(() => { + initialResizeSent.current = false; + colsRef.current = 0; + rowsRef.current = 0; + }, [connId, terminalId]); + + // ── Repaint loop: rAF gated on isDirty() (Decision C) ──────────────────── + useEffect(() => { + let raf = 0; + let mounted = true; + const tick = () => { + if (!mounted) return; + // Repaint when native reports new output. We always repaint on the very + // first tick (frame===0 picture) so initial content shows. + if (native.isDirty(connId, terminalId)) { + setFrame((f) => f + 1); + } + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => { + mounted = false; + cancelAnimationFrame(raf); + }; + }, [native, connId, terminalId]); + + useEffect(() => { + return () => { + if (resizeTimer.current) clearTimeout(resizeTimer.current); + }; + }, []); + + // ── Build the SkPicture from the packed buffer ─────────────────────────── + const picture = useMemo(() => { + if (!size) return null; + // Read fresh native state for this frame. + let cells: PackedCells; + try { + cells = decodeCellsView(native.getVisibleCellsPacked(connId, terminalId)); + } catch { + // Native not ready / empty buffer — draw nothing this frame. + return null; + } + const cursor = native.getCursor(connId, terminalId); + const scroll = native.getScrollInfo(connId, terminalId); + const selection = selecting + ? native.getSelectionBounds(connId, terminalId) + : undefined; + + return createPicture( + (canvas) => + paintTerminal({ + canvas, + cells, + cursor, + scroll, + selection, + metrics, + fonts, + fontSize, + width: size.w, + height: size.h, + }), + Skia.XYWHRect(0, 0, size.w, size.h), + ); + // `frame` participates so the picture recomputes each dirty tick. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [size, frame, native, connId, terminalId, selecting, metrics, fonts, fontSize]); + + return ( + + {picture ? ( + + + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: TerminalTheme.bgColor, + }, +}); + +export default TerminalView; diff --git a/mobile/rn/src/models/index.ts b/mobile/rn/src/models/index.ts new file mode 100644 index 00000000..47fc23e6 --- /dev/null +++ b/mobile/rn/src/models/index.ts @@ -0,0 +1,25 @@ +/** + * models/index.ts — public surface of the data models. + */ + +export { + createSavedServer, + savedServerDisplayName, + savedServerEquals, + withSavedServer, + toJSON as savedServerToJSON, + fromJSON as savedServerFromJSON, + listFromJson as savedServersFromJson, + listToJson as savedServersToJson, + type SavedServer, + type SavedServerJson, +} from './savedServer'; + +export { + parseLayout, + type LayoutNode, + type LayoutNodeType, + type TerminalNode, + type SplitNode, + type TabsNode, +} from './layoutNode'; diff --git a/mobile/rn/src/models/layoutNode.ts b/mobile/rn/src/models/layoutNode.ts new file mode 100644 index 00000000..e7c10a6c --- /dev/null +++ b/mobile/rn/src/models/layoutNode.ts @@ -0,0 +1,126 @@ +/** + * layoutNode.ts — the project layout tree. + * + * Ported from `mobile/lib/src/models/layout_node.dart` (the sealed + * `LayoutNode` hierarchy). This mirrors the server's `ApiLayoutNode`, which is + * delivered as a JSON string by `OkenaNative.getProjectLayoutJson()`. + * + * {@link parseLayout} matches the Dart parser exactly: unknown node types and + * malformed JSON yield `null`; missing fields fall back to the same defaults + * the Dart code used. + * + * The layout renderer (a later screen agent) walks this tree: + * - `TerminalNode` → a {@link import('../components/TerminalView').TerminalView} + * - `SplitNode` → a flex row/column + * - `TabsNode` → a tab bar + the active child + */ + +import type { SplitDirection } from '../native/okena'; + +/** Discriminator for a {@link LayoutNode}. */ +export type LayoutNodeType = 'terminal' | 'split' | 'tabs'; + +/** + * A leaf node hosting a single terminal. `terminalId` can be `undefined` for an + * empty/placeholder pane (matches the nullable `terminal_id` Dart-side). + */ +export interface TerminalNode { + readonly type: 'terminal'; + readonly terminalId?: string; + readonly minimized: boolean; + readonly detached: boolean; +} + +/** + * A split container. `sizes` are the fractional weights of each child along the + * split `direction` (parallel arrays with `children`). + */ +export interface SplitNode { + readonly type: 'split'; + readonly direction: SplitDirection; + readonly sizes: number[]; + readonly children: LayoutNode[]; +} + +/** A tab group; `activeTab` indexes into `children`. */ +export interface TabsNode { + readonly type: 'tabs'; + readonly activeTab: number; + readonly children: LayoutNode[]; +} + +/** The discriminated union — narrow on `.type`. */ +export type LayoutNode = TerminalNode | SplitNode | TabsNode; + +/** + * Parse one raw JSON object into a {@link LayoutNode}, or `null` if its `type` + * is unknown. Mirrors the private `_parse` in `layout_node.dart`, including its + * `whereType()` behavior: children that fail to parse are dropped. + */ +function parseNode(map: Record): LayoutNode | null { + const type = map.type; + switch (type) { + case 'terminal': + return { + type: 'terminal', + terminalId: + typeof map.terminal_id === 'string' + ? (map.terminal_id as string) + : undefined, + minimized: map.minimized === true, + detached: map.detached === true, + }; + case 'split': { + const children = parseChildren(map.children); + const rawSizes = map.sizes; + const sizes = Array.isArray(rawSizes) + ? rawSizes.filter((s): s is number => typeof s === 'number') + : []; + return { + type: 'split', + direction: map.direction === 'vertical' ? 'vertical' : 'horizontal', + sizes, + children, + }; + } + case 'tabs': { + const children = parseChildren(map.children); + const activeTab = + typeof map.active_tab === 'number' ? (map.active_tab as number) : 0; + return { type: 'tabs', activeTab, children }; + } + default: + return null; + } +} + +/** Parse a raw `children` array, dropping any that fail to parse (Dart `whereType`). */ +function parseChildren(raw: unknown): LayoutNode[] { + if (!Array.isArray(raw)) return []; + const out: LayoutNode[] = []; + for (const child of raw) { + if (child && typeof child === 'object') { + const node = parseNode(child as Record); + if (node) out.push(node); + } + } + return out; +} + +/** + * Parse the layout JSON string returned by `getProjectLayoutJson()` into a + * {@link LayoutNode} tree. Returns `null` on invalid JSON, a non-object root, or + * an unknown root node type — matching the Dart `LayoutNode.fromJson`, which + * caught all errors and returned `null`. + */ +export function parseLayout(json: string): LayoutNode | null { + try { + const parsed: unknown = JSON.parse(json); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return null; + } + return parseNode(parsed as Record); + } catch { + return null; + } +} diff --git a/mobile/rn/src/models/savedServer.ts b/mobile/rn/src/models/savedServer.ts new file mode 100644 index 00000000..053fb745 --- /dev/null +++ b/mobile/rn/src/models/savedServer.ts @@ -0,0 +1,159 @@ +/** + * savedServer.ts — the persisted "saved server" model. + * + * Ported from `mobile/lib/src/models/saved_server.dart`. Adds two fields the + * Dart model lacks but the binding contract supports (RN ships TLS-on from day + * one — see RN_MIGRATION.md §3 Phase 1 "TODO(mobile-tls)"): + * - `tls` — whether to connect over TLS. + * - `fingerprint` — the pinned server certificate fingerprint (TOFU). + * + * The screen agents construct these (add-server sheet) and the + * {@link import('../state/connectionStore').useConnectionStore} persists a list + * of them. + */ + +/** + * A server the user has saved to connect to. + * + * Identity (equality / dedupe) is by `host` + `port` only — mirrors the Dart + * `operator ==` / `hashCode`. See {@link savedServerEquals}. + */ +export interface SavedServer { + /** Hostname or IP of the remote Okena desktop server. */ + readonly host: string; + /** TCP port. */ + readonly port: number; + /** Optional user-facing label; falls back to `host:port` for display. */ + readonly label?: string; + /** + * Optional saved auth token. Present once the server has been paired; lets a + * reconnect skip the pairing step (passed to `connect` as `savedToken`). + */ + readonly token?: string; + /** + * Whether to connect over TLS. Defaults to `true` for new servers (RN is + * TLS-on by default). Not present in the Dart model. + */ + readonly tls: boolean; + /** + * Pinned TLS certificate fingerprint (trust-on-first-use). Set after the + * first successful TLS handshake; a mismatch on reconnect indicates the + * server cert changed. Not present in the Dart model. + */ + readonly fingerprint?: string; +} + +/** The JSON shape persisted to storage (matches {@link SavedServer.toJSON}). */ +export interface SavedServerJson { + host: string; + port: number; + label?: string; + token?: string; + tls?: boolean; + fingerprint?: string; +} + +/** + * Create a {@link SavedServer}, defaulting `tls` to `true` (RN TLS-on default). + * Use this rather than an object literal so the default stays in one place. + */ +export function createSavedServer(params: { + host: string; + port: number; + label?: string; + token?: string; + tls?: boolean; + fingerprint?: string; +}): SavedServer { + return { + host: params.host, + port: params.port, + label: params.label, + token: params.token, + tls: params.tls ?? true, + fingerprint: params.fingerprint, + }; +} + +/** + * Display name for a server — the `label` if set, else `host:port`. + * Mirrors the Dart `displayName` getter. + */ +export function savedServerDisplayName(server: SavedServer): string { + return server.label ?? `${server.host}:${server.port}`; +} + +/** + * Identity equality — by `host` + `port` only (mirrors Dart `operator ==`). + * Used to dedupe on add and to locate the active server in the list. + */ +export function savedServerEquals(a: SavedServer, b: SavedServer): boolean { + return a.host === b.host && a.port === b.port; +} + +/** + * Return a copy of `server` with the given fields overridden. Mirrors the Dart + * `copyWith` (which only allowed `token`); extended here to cover `token`, + * `fingerprint`, and `label` since those get filled in post-pairing. + */ +export function withSavedServer( + server: SavedServer, + patch: Partial>, +): SavedServer { + return { + ...server, + token: patch.token ?? server.token, + fingerprint: patch.fingerprint ?? server.fingerprint, + label: patch.label ?? server.label, + }; +} + +/** + * Serialize a server to its JSON form. Optional fields are omitted when unset + * (matches the Dart `toJson`, which used `if (x != null)`). `tls` is always + * written so a round-trip is lossless. + */ +export function toJSON(server: SavedServer): SavedServerJson { + const json: SavedServerJson = { + host: server.host, + port: server.port, + tls: server.tls, + }; + if (server.label !== undefined) json.label = server.label; + if (server.token !== undefined) json.token = server.token; + if (server.fingerprint !== undefined) json.fingerprint = server.fingerprint; + return json; +} + +/** + * Parse a server from its JSON form. `tls` defaults to `true` when absent + * (older persisted data, or Dart-written data, had no `tls` key). + */ +export function fromJSON(json: SavedServerJson): SavedServer { + return { + host: json.host, + port: json.port, + label: json.label, + token: json.token, + tls: json.tls ?? true, + fingerprint: json.fingerprint, + }; +} + +/** + * Parse a JSON-array string into a list of servers (mirrors Dart + * `listFromJson`). Throws if the string is not a JSON array; the caller + * (connection store) catches and starts fresh on corrupted data. + */ +export function listFromJson(jsonString: string): SavedServer[] { + const parsed: unknown = JSON.parse(jsonString); + if (!Array.isArray(parsed)) { + throw new TypeError('saved-server list JSON is not an array'); + } + return parsed.map((e) => fromJSON(e as SavedServerJson)); +} + +/** Serialize a list of servers to a JSON-array string (mirrors Dart `listToJson`). */ +export function listToJson(servers: readonly SavedServer[]): string { + return JSON.stringify(servers.map(toJSON)); +} diff --git a/mobile/rn/src/native/cells.ts b/mobile/rn/src/native/cells.ts new file mode 100644 index 00000000..32fa2603 --- /dev/null +++ b/mobile/rn/src/native/cells.ts @@ -0,0 +1,205 @@ +/** + * cells.ts — decoder for the packed visible-cell buffer. + * + * This matches the binary format produced by the Rust FFI function + * `get_visible_cells_packed` in `crates/okena-mobile-ffi/src/lib.rs`. This file + * is the authoritative spec of that wire format; the Rust encoder must produce + * exactly these bytes. + * + * Format (little-endian throughout): + * + * ┌─ header (4 bytes) ─────────────────────────────────────────────┐ + * │ cols : u16 │ + * │ rows : u16 │ + * ├─ then cols*rows cells, row-major, 13 bytes each ───────────────┤ + * │ codepoint : u32 Unicode scalar value (0x20 == space) │ + * │ fg : u32 ARGB packed: 0xAARRGGBB │ + * │ bg : u32 ARGB packed: 0xAARRGGBB │ + * │ flags : u8 bitmask (see FLAG_* below) │ + * └────────────────────────────────────────────────────────────────┘ + * + * Total length = 4 + cols*rows*13 bytes. + */ + +// ── Style flag bits (must match Rust `CellData.flags` in api/terminal.rs) ── + +export const FLAG_BOLD = 1; +export const FLAG_ITALIC = 2; +export const FLAG_UNDERLINE = 4; +export const FLAG_STRIKETHROUGH = 8; +export const FLAG_INVERSE = 16; +export const FLAG_DIM = 32; + +/** + * Flags that affect glyph styling (everything except INVERSE, which is handled + * separately by swapping fg/bg). Mirrors `_kStyleMask` in terminal_painter.dart. + */ +export const FLAG_STYLE_MASK = + FLAG_BOLD | FLAG_ITALIC | FLAG_UNDERLINE | FLAG_STRIKETHROUGH | FLAG_DIM; + +// ── Byte-layout constants ────────────────────────────────────────────────── + +/** Header size in bytes (cols:u16 + rows:u16). */ +export const HEADER_BYTES = 4; +/** Size of one packed cell in bytes (u32 + u32 + u32 + u8). */ +export const CELL_BYTES = 13; + +const OFFSET_CODEPOINT = 0; +const OFFSET_FG = 4; +const OFFSET_BG = 8; +const OFFSET_FLAGS = 12; + +// ── ARGB helpers ──────────────────────────────────────────────────────────── + +export interface Argb { + a: number; + r: number; + g: number; + b: number; +} + +/** Unpack a `0xAARRGGBB` u32 into channel components (0–255 each). */ +export function argbToChannels(argb: number): Argb { + // `>>> 0` keeps it an unsigned 32-bit value. + const v = argb >>> 0; + return { + a: (v >>> 24) & 0xff, + r: (v >>> 16) & 0xff, + g: (v >>> 8) & 0xff, + b: v & 0xff, + }; +} + +/** + * Convert a `0xAARRGGBB` u32 to a CSS `rgba()` string. Handy for non-Skia + * consumers / tests; the Skia renderer builds `Float32Array` colors directly. + */ +export function argbToCss(argb: number): string { + const { a, r, g, b } = argbToChannels(argb); + return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(4)})`; +} + +// ── Decoded representations ────────────────────────────────────────────────── + +/** A single decoded cell. */ +export interface DecodedCell { + /** Unicode scalar value; `0x20` is a space / blank cell. */ + codepoint: number; + /** Foreground, ARGB packed `0xAARRGGBB`. */ + fg: number; + /** Background, ARGB packed `0xAARRGGBB`. */ + bg: number; + /** Style flags bitmask. */ + flags: number; +} + +/** Object form of a decoded grid (convenient; allocates per cell). */ +export interface DecodedGrid { + cols: number; + rows: number; + cells: DecodedCell[]; +} + +/** + * Decode the packed buffer into a `{ cols, rows, cells }` object. + * + * Allocates one `DecodedCell` object per cell — fine for tests and incidental + * use. For the per-frame render hot path prefer `decodeCellsView`, which wraps + * the same bytes in typed-array views with zero per-cell allocation. + */ +export function decodeCells(buf: ArrayBuffer): DecodedGrid { + const view = new DataView(buf); + const cols = view.getUint16(0, /* littleEndian */ true); + const rows = view.getUint16(2, true); + const count = cols * rows; + + const expected = HEADER_BYTES + count * CELL_BYTES; + if (buf.byteLength < expected) { + throw new RangeError( + `packed cell buffer too short: have ${buf.byteLength} bytes, ` + + `need ${expected} for ${cols}x${rows}`, + ); + } + + const cells: DecodedCell[] = new Array(count); + let off = HEADER_BYTES; + for (let i = 0; i < count; i++) { + cells[i] = { + codepoint: view.getUint32(off + OFFSET_CODEPOINT, true), + fg: view.getUint32(off + OFFSET_FG, true), + bg: view.getUint32(off + OFFSET_BG, true), + flags: view.getUint8(off + OFFSET_FLAGS), + }; + off += CELL_BYTES; + } + + return { cols, rows, cells }; +} + +/** + * Zero-allocation accessor over the packed buffer for the render hot path. + * + * Holds a single `DataView` and exposes per-cell field getters indexed by + * `i = row * cols + col`. No `DecodedCell` objects are created, so painting a + * full frame allocates nothing beyond this wrapper. + */ +export class PackedCells { + readonly cols: number; + readonly rows: number; + readonly count: number; + private readonly view: DataView; + + constructor(buf: ArrayBuffer) { + const view = new DataView(buf); + this.cols = view.getUint16(0, true); + this.rows = view.getUint16(2, true); + this.count = this.cols * this.rows; + const expected = HEADER_BYTES + this.count * CELL_BYTES; + if (buf.byteLength < expected) { + throw new RangeError( + `packed cell buffer too short: have ${buf.byteLength} bytes, ` + + `need ${expected} for ${this.cols}x${this.rows}`, + ); + } + this.view = view; + } + + private cellOffset(i: number): number { + return HEADER_BYTES + i * CELL_BYTES; + } + + codepoint(i: number): number { + return this.view.getUint32(this.cellOffset(i) + OFFSET_CODEPOINT, true); + } + + fg(i: number): number { + return this.view.getUint32(this.cellOffset(i) + OFFSET_FG, true); + } + + bg(i: number): number { + return this.view.getUint32(this.cellOffset(i) + OFFSET_BG, true); + } + + flags(i: number): number { + return this.view.getUint8(this.cellOffset(i) + OFFSET_FLAGS); + } + + /** The character at cell `i` as a JS string (handles astral codepoints). */ + char(i: number): string { + return String.fromCodePoint(this.codepoint(i)); + } + + /** True if the cell is empty (space). */ + isBlank(i: number): boolean { + const cp = this.codepoint(i); + return cp === 0x20 || cp === 0; + } +} + +/** + * Wrap a packed buffer in a {@link PackedCells} view (zero per-cell alloc). + * The render path should use this; `decodeCells` is the convenience form. + */ +export function decodeCellsView(buf: ArrayBuffer): PackedCells { + return new PackedCells(buf); +} diff --git a/mobile/rn/src/native/okena.ts b/mobile/rn/src/native/okena.ts new file mode 100644 index 00000000..72c19768 --- /dev/null +++ b/mobile/rn/src/native/okena.ts @@ -0,0 +1,518 @@ +/** + * okena.ts — the native ↔ TypeScript binding contract. + * + * ┌───────────────────────────────────────────────────────────────────────┐ + * │ THIS IS A SCAFFOLD / SPEC, NOT THE REAL BINDING. │ + * │ │ + * │ The real implementation is GENERATED by `uniffi-bindgen-react-native` │ + * │ (`ubrn`) from the sibling Rust crate `crates/okena-mobile-ffi` (the │ + * │ uniffi-annotated rework of `mobile/native`). See ../../README.md. │ + * │ │ + * │ This file declares the agreed-upon TypeScript shape of that generated │ + * │ module: the `OkenaNative` interface plus all the record/enum types. │ + * │ Both sides — the Rust `#[uniffi::export]` signatures and this contract │ + * │ — must stay in sync. When `ubrn` is wired up, the generated package's │ + * │ exported functions should satisfy `OkenaNative` structurally (this file │ + * │ becomes the typed re-export / shim over the generated module). │ + * └───────────────────────────────────────────────────────────────────────┘ + * + * Signature source of truth: `crates/okena-mobile-ffi/src/lib.rs` (the + * `#[uniffi::export]` surface). + * + * Sync vs. async mapping (mirrors the Rust `#[frb(sync)]` / `async fn` split, + * which `ubrn` maps to synchronous JSI host functions vs. JS Promises): + * - SYNC : cheap getters on the render hot path + * (getVisibleCells, getVisibleCellsPacked, getCursor, isDirty, + * connectionStatus, getScrollInfo, selection getters, resize*). + * - ASYNC : anything that crosses the wire / awaits a server response + * (pair, sendText, sendSpecialKey, all *_action / git / service / + * project / layout calls, readContent). + */ + +// ───────────────────────────────────────────────────────────────────────── +// Scalar / id aliases (documentation only — all are `string`/`number` at runtime) +// ───────────────────────────────────────────────────────────────────────── + +/** Opaque connection id returned by `connect`. */ +export type ConnId = string; +/** Opaque terminal id (from layout / state). */ +export type TerminalId = string; +/** Opaque project id. */ +export type ProjectId = string; +/** Opaque folder id. */ +export type FolderId = string; + +// ───────────────────────────────────────────────────────────────────────── +// Enums (uniffi enums → TS string-union; ubrn emits string-tagged values) +// ───────────────────────────────────────────────────────────────────────── + +/** + * Connection status — mirrors the FFI `ConnectionStatus` enum in + * `api/connection.rs` (the simplified mobile variant; `Reconnecting` is + * collapsed into `Connecting` Rust-side). + * + * `Error` carries a message, so this is a discriminated union rather than a + * bare string-union. + */ +export type ConnectionStatus = + | { kind: 'disconnected' } + | { kind: 'connecting' } + | { kind: 'connected' } + | { kind: 'pairing' } + | { kind: 'error'; message: string }; + +/** Cursor shape — mirrors FFI `CursorShape` (`api/terminal.rs`). */ +export type CursorShape = 'block' | 'underline' | 'beam'; + +/** + * Named special keys the remote API accepts, mirroring + * `okena_core::keys::SpecialKey`. `sendSpecialKey` takes one of these + * (serialized to the same JSON variant name Rust expects). + */ +export type SpecialKey = + | 'Enter' + | 'Escape' + | 'Backspace' + | 'Delete' + | 'CtrlC' + | 'CtrlD' + | 'CtrlZ' + | 'Tab' + | 'ArrowUp' + | 'ArrowDown' + | 'ArrowLeft' + | 'ArrowRight' + | 'Home' + | 'End' + | 'PageUp' + | 'PageDown'; + +/** Diff mode for the git diff / file-contents calls. */ +export type DiffMode = 'working_tree' | 'staged'; + +/** Split direction for `splitTerminal`. */ +export type SplitDirection = 'horizontal' | 'vertical'; + +// ───────────────────────────────────────────────────────────────────────── +// Records (uniffi records → TS interfaces) +// ───────────────────────────────────────────────────────────────────────── + +/** + * A single rendered terminal cell — mirrors FFI `CellData` (`api/terminal.rs`). + * + * NOTE: For the render hot path prefer `getVisibleCellsPacked` (see below) + + * the `decodeCells` reader in `./cells`, which avoids marshalling thousands of + * these objects across JSI per frame (Decision C in RN_MIGRATION.md §2). + */ +export interface CellData { + /** The grapheme in this cell (`" "` for blank). */ + character: string; + /** Foreground color, ARGB packed: `0xAARRGGBB`. */ + fg: number; + /** Background color, ARGB packed: `0xAARRGGBB`. */ + bg: number; + /** Style flags bitmask: bold|italic|underline|strikethrough|inverse|dim. See ./cells. */ + flags: number; +} + +/** Cursor state — mirrors FFI `CursorState` (`api/terminal.rs`). */ +export interface CursorState { + /** 0-based column. */ + col: number; + /** 0-based visual row. */ + row: number; + shape: CursorShape; + visible: boolean; +} + +/** Scroll info — mirrors FFI `ScrollInfo` (`api/terminal.rs`). */ +export interface ScrollInfo { + /** Total lines in the scrollback + screen. */ + totalLines: number; + /** Number of visible (on-screen) lines. */ + visibleLines: number; + /** Distance scrolled back from the bottom; 0 == pinned to bottom. */ + displayOffset: number; +} + +/** + * Selection bounds in buffer coordinates — mirrors FFI `SelectionBounds` + * (`api/terminal.rs`). Rows are `i32` Rust-side (can be negative in scrollback), + * cols are `u16`. + */ +export interface SelectionBounds { + startCol: number; + /** Buffer row; may be negative (scrollback above the screen). */ + startRow: number; + endCol: number; + /** Buffer row; may be negative. */ + endRow: number; +} + +/** Service info — mirrors FFI `ServiceInfo` (`api/state.rs`). */ +export interface ServiceInfo { + name: string; + status: string; + /** Terminal hosting the service, if running. */ + terminalId?: string; + ports: number[]; + exitCode?: number; + kind: string; + isExtra: boolean; +} + +/** Project info — mirrors FFI `ProjectInfo` (`api/state.rs`). */ +export interface ProjectInfo { + id: ProjectId; + name: string; + path: string; + showInOverview: boolean; + terminalIds: TerminalId[]; + /** Map terminalId → display name. */ + terminalNames: Record; + gitBranch?: string; + gitLinesAdded: number; + gitLinesRemoved: number; + services: ServiceInfo[]; + /** Lowercased folder color name (e.g. `"blue"`). */ + folderColor: string; +} + +/** Folder info — mirrors FFI `FolderInfo` (`api/state.rs`). */ +export interface FolderInfo { + id: FolderId; + name: string; + projectIds: ProjectId[]; + /** Lowercased folder color name. */ + folderColor: string; +} + +/** Fullscreen terminal info — mirrors FFI `FullscreenInfo` (`api/state.rs`). */ +export interface FullscreenInfo { + projectId: ProjectId; + terminalId: TerminalId; +} + +// ───────────────────────────────────────────────────────────────────────── +// The binding contract +// ───────────────────────────────────────────────────────────────────────── + +/** + * The full native surface — one method per exported Rust FFI function. + * + * Naming: Rust `snake_case` fns map to camelCase here, which is also what + * `ubrn` emits for the generated JS bindings. `connId`/`terminalId`/etc. are + * always passed positionally (uniffi has no keyword args, unlike `frb`). + */ +export interface OkenaNative { + // ── connection.rs ────────────────────────────────────────────────────── + + /** One-time startup init (sets up the ConnectionManager singleton). */ + initApp(): void; + + /** + * Open a connection to a remote server, returning its conn id. If a saved + * token is supplied, pairing is skipped. (sync — just registers + kicks off + * the connect; status is then polled via `connectionStatus`.) + */ + connect(host: string, port: number, savedToken: string | undefined): ConnId; + + /** Current auth token for a connection, if paired. */ + getToken(connId: ConnId): string | undefined; + + /** Pair with the server using a pairing code (async — awaits the server). */ + pair(connId: ConnId, code: string): Promise; + + /** Close the WS and clean up. */ + disconnect(connId: ConnId): void; + + /** Current connection status. */ + connectionStatus(connId: ConnId): ConnectionStatus; + + /** + * Seconds since the last WS activity (terminal output). Returns a large + * value if the connection doesn't exist. Used for the staleness indicator. + */ + secondsSinceActivity(connId: ConnId): number; + + // ── terminal.rs ──────────────────────────────────────────────────────── + + /** + * Visible grid as one record per cell. Convenient for non-hot callers; for + * the renderer use `getVisibleCellsPacked` instead. + */ + getVisibleCells(connId: ConnId, terminalId: TerminalId): CellData[]; + + /** + * Visible grid as a packed little-endian binary buffer — the render hot path + * (Decision C). Layout: `cols:u16, rows:u16` header, then `cols*rows` cells + * of 13 bytes each (`codepoint:u32, fg:u32, bg:u32, flags:u8`). Decode with + * `decodeCells` from `./cells`. + * + * Implemented Rust-side as `get_visible_cells_packed` in + * `crates/okena-mobile-ffi/src/lib.rs`; the byte format here is the contract. + */ + getVisibleCellsPacked(connId: ConnId, terminalId: TerminalId): ArrayBuffer; + + /** Current cursor state. */ + getCursor(connId: ConnId, terminalId: TerminalId): CursorState; + + /** Scroll the display: positive = up (into scrollback), negative = down. */ + scroll(connId: ConnId, terminalId: TerminalId, delta: number): void; + + /** Total/visible line counts and the current display offset. */ + getScrollInfo(connId: ConnId, terminalId: TerminalId): ScrollInfo; + + /** Begin a character-level selection at the given cell. */ + startSelection(connId: ConnId, terminalId: TerminalId, col: number, row: number): void; + + /** Begin a word (semantic) selection at the given cell. */ + startWordSelection(connId: ConnId, terminalId: TerminalId, col: number, row: number): void; + + /** Extend the active selection to the given cell. */ + updateSelection(connId: ConnId, terminalId: TerminalId, col: number, row: number): void; + + /** Clear the active selection. */ + clearSelection(connId: ConnId, terminalId: TerminalId): void; + + /** The currently selected text, if any. */ + getSelectedText(connId: ConnId, terminalId: TerminalId): string | undefined; + + /** Selection bounds for rendering the highlight overlay. */ + getSelectionBounds(connId: ConnId, terminalId: TerminalId): SelectionBounds | undefined; + + /** Send raw text input (async — goes over the WS). */ + sendText(connId: ConnId, terminalId: TerminalId, text: string): Promise; + + /** Resize the terminal locally AND tell the server (async semantics). */ + resizeTerminal(connId: ConnId, terminalId: TerminalId, cols: number, rows: number): void; + + /** + * Resize ONLY the local alacritty grid — does not notify the server. Used to + * adapt the mobile viewport to the server's size without round-tripping. + */ + resizeLocal(connId: ConnId, terminalId: TerminalId, cols: number, rows: number): void; + + // ── state.rs ─────────────────────────────────────────────────────────── + + /** All projects from the cached remote state. */ + getProjects(connId: ConnId): ProjectInfo[]; + + /** The server's focused project id, if any. */ + getFocusedProjectId(connId: ConnId): ProjectId | undefined; + + /** All folders from the cached remote state. */ + getFolders(connId: ConnId): FolderInfo[]; + + /** The server's project ordering. */ + getProjectOrder(connId: ConnId): ProjectId[]; + + /** The fullscreen terminal, if one is active. */ + getFullscreenTerminal(connId: ConnId): FullscreenInfo | undefined; + + /** Layout tree for a project, serialized as JSON (`ApiLayoutNode`). */ + getProjectLayoutJson(connId: ConnId, projectId: ProjectId): string | undefined; + + /** Whether a terminal has unread output since the last cell fetch. */ + isDirty(connId: ConnId, terminalId: TerminalId): boolean; + + /** Send a named special key (async — goes over the WS). */ + sendSpecialKey(connId: ConnId, terminalId: TerminalId, key: SpecialKey): Promise; + + /** Flat list of every terminal id across all projects. */ + getAllTerminalIds(connId: ConnId): TerminalId[]; + + // ── terminal actions (state.rs, async) ─────────────────────────────────── + + createTerminal(connId: ConnId, projectId: ProjectId): Promise; + closeTerminal(connId: ConnId, projectId: ProjectId, terminalId: TerminalId): Promise; + closeTerminals(connId: ConnId, projectId: ProjectId, terminalIds: TerminalId[]): Promise; + renameTerminal( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId, + name: string, + ): Promise; + focusTerminal(connId: ConnId, projectId: ProjectId, terminalId: TerminalId): Promise; + toggleMinimized(connId: ConnId, projectId: ProjectId, terminalId: TerminalId): Promise; + /** Set or clear (pass `undefined`) the fullscreen terminal. */ + setFullscreen( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId | undefined, + ): Promise; + splitTerminal( + connId: ConnId, + projectId: ProjectId, + path: number[], + direction: SplitDirection, + ): Promise; + /** Run a command and auto-press Enter. */ + runCommand(connId: ConnId, terminalId: TerminalId, command: string): Promise; + /** Read full terminal content as text (awaits a server response). */ + readContent(connId: ConnId, terminalId: TerminalId): Promise; + + // ── git actions (state.rs, async — return JSON strings) ────────────────── + + gitStatus(connId: ConnId, projectId: ProjectId): Promise; + gitDiffSummary(connId: ConnId, projectId: ProjectId): Promise; + gitDiff(connId: ConnId, projectId: ProjectId, mode: DiffMode): Promise; + gitBranches(connId: ConnId, projectId: ProjectId): Promise; + gitFileContents( + connId: ConnId, + projectId: ProjectId, + filePath: string, + mode: DiffMode, + ): Promise; + + // ── service actions (state.rs, async) ──────────────────────────────────── + + startService(connId: ConnId, projectId: ProjectId, serviceName: string): Promise; + stopService(connId: ConnId, projectId: ProjectId, serviceName: string): Promise; + restartService(connId: ConnId, projectId: ProjectId, serviceName: string): Promise; + startAllServices(connId: ConnId, projectId: ProjectId): Promise; + stopAllServices(connId: ConnId, projectId: ProjectId): Promise; + reloadServices(connId: ConnId, projectId: ProjectId): Promise; + + // ── project management (state.rs, async) ───────────────────────────────── + + addProject(connId: ConnId, name: string, path: string): Promise; + setProjectColor(connId: ConnId, projectId: ProjectId, color: string): Promise; + setFolderColor(connId: ConnId, folderId: FolderId, color: string): Promise; + reorderProjectInFolder( + connId: ConnId, + folderId: FolderId, + projectId: ProjectId, + newIndex: number, + ): Promise; + + // ── layout actions (state.rs, async) ───────────────────────────────────── + + updateSplitSizes( + connId: ConnId, + projectId: ProjectId, + path: number[], + sizes: number[], + ): Promise; + addTab(connId: ConnId, projectId: ProjectId, path: number[], inGroup: boolean): Promise; + setActiveTab(connId: ConnId, projectId: ProjectId, path: number[], index: number): Promise; + moveTab( + connId: ConnId, + projectId: ProjectId, + path: number[], + fromIndex: number, + toIndex: number, + ): Promise; + moveTerminalToTabGroup( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId, + targetPath: number[], + position: number | undefined, + targetProjectId: ProjectId | undefined, + ): Promise; + movePaneTo( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId, + targetProjectId: ProjectId, + targetTerminalId: TerminalId, + zone: string, + ): Promise; +} + +/** + * Resolve the real native module generated by `ubrn` into `src/generated` + * (see `ubrn.config.yaml`). The lookup is a synchronous Metro `require` — the + * render hot path calls the native getters synchronously, so a dynamic + * `import()` (async) is not an option. + * + * The require is lazy (inside the function, not a top-level import) so that: + * - this typed contract and the presentational components compile/bundle + * before `npm run ubrn:*` has produced the module, and + * - tests / off-device code that inject a mock `OkenaNative` (via + * `configureConnectionStore` / `configureWorkspaceStore` / the `native` + * prop) never hit this path. + * + * Until the module is generated, the `require` throws and we rethrow with a + * pointer to the generation step (see mobile/rn/README.md). + */ +/** + * ubrn → app-contract adapters. + * + * ubrn 0.31 represents uniffi types in a few shapes that differ from the + * hand-written contract above, so the boundary (`getOkenaNative`) translates: + * - Data-carrying enums → tagged class instances with a PascalCase `.tag` + * (`{ tag: 'Connected' }`; payload under `.inner`, e.g. Error → + * `.inner.message`). App wants `{ kind: 'lowercase' }`. → `ConnectionStatus`. + * - Fieldless enums → a numeric TS enum (`CursorShape.Block === 0`). App wants + * the lowercase string. → `getCursor().shape`. + * - Map-typed record fields → a JS `Map`. App indexes them as plain objects. + * → `ProjectInfo.terminalNames`. + * Plain-string enums (split direction, diff mode) and all other record fields + * already match (camelCase), so they pass through untouched. + */ +function toConnectionStatus(s: {tag?: string; inner?: {message?: string}}): ConnectionStatus { + switch (s?.tag) { + case 'Disconnected': + return {kind: 'disconnected'}; + case 'Connecting': + return {kind: 'connecting'}; + case 'Connected': + return {kind: 'connected'}; + case 'Pairing': + return {kind: 'pairing'}; + case 'Error': + return {kind: 'error', message: s.inner?.message ?? 'Unknown error'}; + default: + return {kind: 'disconnected'}; + } +} + +// Generated `CursorShape` is a numeric enum (Block=0, Underline=1, Beam=2); +// index by it to recover the app's string union. +const CURSOR_SHAPES: CursorShape[] = ['block', 'underline', 'beam']; + +function toCursorState(c: {col: number; row: number; shape: number; visible: boolean}): CursorState { + return {col: c.col, row: c.row, visible: c.visible, shape: CURSOR_SHAPES[c.shape] ?? 'block'}; +} + +// Generated records carry `terminalNames` as a JS `Map`; the app reads it as a +// plain `Record`. Convert and pass everything else through. +function toProjectInfo(p: ProjectInfo & {terminalNames: unknown}): ProjectInfo { + const names = p.terminalNames; + return { + ...p, + terminalNames: + names instanceof Map ? Object.fromEntries(names) : (names as Record), + }; +} + +export function getOkenaNative(): OkenaNative { + try { + // The local `okena-mobile-ffi` package entry (src/index.tsx) installs the + // JSI bindings (`installRustCrate()` + `initialize()`) on first import and + // re-exports every generated function at the top level. Most functions + // satisfy `OkenaNative` structurally as-is; the wrapper below only overrides + // the ones whose enum shapes need translating (see adapters above). + const gen = require('okena-mobile-ffi'); + return { + ...gen, + connectionStatus: (connId: ConnId) => + toConnectionStatus(gen.connectionStatus(connId)), + getCursor: (connId: ConnId, terminalId: TerminalId) => + toCursorState(gen.getCursor(connId, terminalId)), + getProjects: (connId: ConnId) => + (gen.getProjects(connId) as Array).map( + toProjectInfo, + ), + } as OkenaNative; + } catch (e) { + throw new Error( + 'okena native module not generated yet — run `npm run ubrn:android` or ' + + '`npm run ubrn:ios` to cross-compile crates/okena-mobile-ffi and emit ' + + 'src/generated. See mobile/rn/README.md. Underlying error: ' + + String(e), + ); + } +} diff --git a/mobile/rn/src/navigation/index.ts b/mobile/rn/src/navigation/index.ts new file mode 100644 index 00000000..6b85a755 --- /dev/null +++ b/mobile/rn/src/navigation/index.ts @@ -0,0 +1,15 @@ +/** + * navigation/index.ts — public surface of the navigation layer. + * + * The screen agents import the nav hook + the `navigate` API from here. + */ + +export { + useNavStore, + navigate, + currentScreen, + deriveScreen, + bindConnectionToNavigation, + type Screen, + type NavState, +} from './navStore'; diff --git a/mobile/rn/src/navigation/navStore.ts b/mobile/rn/src/navigation/navStore.ts new file mode 100644 index 00000000..22cc8a3b --- /dev/null +++ b/mobile/rn/src/navigation/navStore.ts @@ -0,0 +1,95 @@ +/** + * navStore.ts — the minimal state-driven router. + * + * The Flutter app used a declarative `AppRouter` widget that picked a screen + * from `ConnectionProvider`'s state (`isConnected` → workspace, + * `activeServer != null` → pairing, else server list) inside an + * `AnimatedSwitcher`. We deliberately do NOT pull in `react-navigation` + * (it needs `react-native-screens` / `react-native-gesture-handler` native + * deps that can't build in this environment — see the task constraints). + * + * Instead this is a tiny zustand store holding the current {@link Screen}, plus + * {@link navigate} to set it. App.tsx renders the matching screen and also + * SUBSCRIBES to the connection store to drive automatic transitions (the same + * rule the Dart `AppRouter` encoded) — see {@link deriveScreen} and + * {@link bindConnectionToNavigation}. + */ + +import { create, type StoreApi, type UseBoundStore } from 'zustand'; + +import { + connectionStore, + selectIsConnected, + type ConnectionState, +} from '../state/connectionStore'; + +/** The three screens of the app, in flow order. */ +export type Screen = 'serverList' | 'pairing' | 'workspace'; + +/** Nav store state + the navigate action. */ +export interface NavState { + /** The currently-displayed screen. */ + screen: Screen; + /** + * Navigate to a screen. Screens call this directly (e.g. the server-list + * screen calls `navigate('pairing')` after kicking off a connection — though + * the connection-driven binding usually handles that automatically). + */ + navigate(screen: Screen): void; +} + +const useNavStoreImpl: UseBoundStore> = create((set) => ({ + screen: 'serverList', + navigate: (screen) => set({ screen }), +})); + +/** + * The navigation store hook. Use it to read the active screen and to navigate: + * + * ```ts + * const screen = useNavStore((s) => s.screen); + * const navigate = useNavStore((s) => s.navigate); + * ``` + */ +export const useNavStore = useNavStoreImpl; + +/** Imperative navigate (for non-React callers). Prefer the hook inside components. */ +export function navigate(screen: Screen): void { + useNavStoreImpl.getState().navigate(screen); +} + +/** The current screen, imperatively. */ +export function currentScreen(): Screen { + return useNavStoreImpl.getState().screen; +} + +/** + * The screen the connection state implies — the exact rule the Dart `AppRouter` + * used: + * - connected → `workspace` + * - an active server chosen → `pairing` (connecting/pairing/error) + * - otherwise → `serverList` + */ +export function deriveScreen(conn: ConnectionState): Screen { + if (selectIsConnected(conn)) return 'workspace'; + if (conn.activeServer !== null) return 'pairing'; + return 'serverList'; +} + +/** + * Subscribe the navigation store to the connection store so the screen tracks + * connection state automatically (the Dart `AppRouter` behavior). Call once at + * app start (App.tsx). Returns an unsubscribe fn. + * + * Screens may still call {@link navigate} directly for in-flow moves; this just + * guarantees the canonical transitions (e.g. → workspace on Connected, back to + * serverList on full disconnect) happen no matter who initiated them. + */ +export function bindConnectionToNavigation(): () => void { + const conn = connectionStore(); + // Apply once immediately for the initial state. + navigate(deriveScreen(conn.getState())); + return conn.subscribe((state) => { + navigate(deriveScreen(state)); + }); +} diff --git a/mobile/rn/src/screens/PairingScreen.tsx b/mobile/rn/src/screens/PairingScreen.tsx new file mode 100644 index 00000000..fa1834cc --- /dev/null +++ b/mobile/rn/src/screens/PairingScreen.tsx @@ -0,0 +1,316 @@ +/** + * PairingScreen.tsx — connect → pair → connected flow. + * + * Ported from `mobile/lib/src/screens/pairing_screen.dart`: + * - header: a back button (which disconnects), the active server's display + * name, and a "Connecting" subtitle, + * - a centered {@link StatusIndicator}, + * - body, by phase: + * • connecting (not yet pairing, no error): spinner + "Connecting to + * server...", + * • pairing: "Pair with Server" heading, a centered code input + * (XXXX-XXXX), and a "Pair" button (spinner while submitting), + * • error: a red ✕ badge, the error message in a tinted box, and a "Try + * Again" button that reconnects to the active server. + * + * Additionally shows the pinned TLS cert fingerprint for the active server when + * present (RN ships TLS-on; the Dart model had no fingerprint) — so the user can + * verify it against the desktop app's pairing dialog. + * + * On success (status → connected) `App.tsx`'s connection→nav binding routes to + * the workspace; nothing to do here. + */ + +import React, { useState } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; + +import { savedServerDisplayName } from '../models'; +import { useConnectionStore, selectIsPairing } from '../state'; +import { OkenaColors, OkenaTypography } from '../theme'; +import { StatusIndicator } from '../components/StatusIndicator'; + +export const PairingScreen: React.FC = () => { + const status = useConnectionStore((s) => s.status); + const activeServer = useConnectionStore((s) => s.activeServer); + const isPairing = useConnectionStore(selectIsPairing); + const pair = useConnectionStore((s) => s.pair); + const disconnect = useConnectionStore((s) => s.disconnect); + const connectTo = useConnectionStore((s) => s.connectTo); + + const [code, setCode] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const isError = status.kind === 'error'; + const errorMessage = isError ? status.message : null; + const showCodeInput = isPairing; + + const submitCode = async () => { + const trimmed = code.trim(); + if (trimmed.length === 0 || submitting) return; + setSubmitting(true); + try { + await pair(trimmed); + } finally { + setSubmitting(false); + } + }; + + const tryAgain = () => { + if (!activeServer) return; + // Mirrors the Dart "Try Again": disconnect then reconnect the same server. + disconnect(); + connectTo(activeServer); + }; + + return ( + + {/* Header */} + + + {'‹'} + + + + {activeServer ? savedServerDisplayName(activeServer) : 'Server'} + + Connecting + + + + {/* Body */} + + + + + + {!showCodeInput && !isError && ( + + + Connecting to server... + + )} + + {showCodeInput && ( + + + Pair with Server + + + Check the Okena desktop app for the pairing code. + + + setCode(t.toUpperCase())} + placeholder="XXXX-XXXX" + placeholderTextColor={OkenaColors.textTertiary} + autoCapitalize="characters" + autoCorrect={false} + autoFocus + textAlign="center" + onSubmitEditing={() => void submitCode()} + returnKeyType="go" + /> + + [ + styles.primaryButton, + submitting && styles.primaryButtonDisabled, + pressed && !submitting && styles.primaryButtonPressed, + ]} + onPress={() => void submitCode()} + > + {submitting ? ( + + ) : ( + Pair + )} + + + + + )} + + {isError && ( + + + {'✕'} + + + {errorMessage ?? 'Connection failed'} + + [styles.outlineButton, pressed && styles.outlineButtonPressed]} + onPress={tryAgain} + > + Try Again + + + )} + + + ); +}; + +/** Small footnote showing the pinned TLS cert fingerprint, when known. */ +const FingerprintNote: React.FC<{ fingerprint?: string }> = ({ fingerprint }) => { + if (!fingerprint) return null; + return ( + + TLS certificate fingerprint + + {fingerprint} + + + ); +}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: OkenaColors.background, + paddingTop: 44, // approximate top safe-area inset (no SafeAreaView dep) + }, + + // Header + header: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 4, + paddingRight: 16, + paddingTop: 4, + }, + backButton: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + backChevron: { color: OkenaColors.accent, fontSize: 28, lineHeight: 30 }, + headerText: { flex: 1, marginLeft: 4 }, + headerSub: { + ...OkenaTypography.caption2, + color: OkenaColors.textTertiary, + marginTop: 1, + }, + + // Body + body: { + flexGrow: 1, + justifyContent: 'center', + padding: 24, + }, + statusWrap: { alignItems: 'center', marginBottom: 40 }, + centerText: { textAlign: 'center' }, + + // Connecting + connectingBlock: { alignItems: 'center' }, + connectingText: { + ...OkenaTypography.body, + color: OkenaColors.textSecondary, + textAlign: 'center', + marginTop: 20, + }, + + // Pairing + pairSub: { + ...OkenaTypography.body, + color: OkenaColors.textSecondary, + marginTop: 8, + marginBottom: 32, + }, + codeInput: { + fontSize: 28, + letterSpacing: 8, + fontFamily: 'JetBrainsMono', + fontWeight: '500', + color: OkenaColors.textPrimary, + height: 56, + borderRadius: 10, + backgroundColor: OkenaColors.surfaceElevated, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + marginBottom: 24, + }, + + // Primary button + primaryButton: { + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OkenaColors.accent, + }, + primaryButtonPressed: { opacity: 0.85 }, + primaryButtonDisabled: { opacity: 0.5 }, + primaryButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '600' }, + + // Fingerprint + fingerprintWrap: { + marginTop: 28, + alignItems: 'center', + }, + fingerprintLabel: { + ...OkenaTypography.caption2, + color: OkenaColors.textTertiary, + marginBottom: 4, + }, + fingerprintValue: { + fontFamily: 'JetBrainsMono', + fontSize: 11, + color: OkenaColors.textSecondary, + textAlign: 'center', + }, + + // Error + errorBadge: { + width: 64, + height: 64, + borderRadius: 32, + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'center', + backgroundColor: '#f871711a', // error @ ~10% + }, + errorBadgeMark: { color: OkenaColors.error, fontSize: 32 }, + errorBox: { + marginTop: 20, + padding: 16, + borderRadius: 12, + backgroundColor: '#f8717114', // error @ ~8% + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#f8717133', // error @ ~20% + }, + errorText: { ...OkenaTypography.body, color: OkenaColors.error, textAlign: 'center' }, + outlineButton: { + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + marginTop: 24, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.accent, + }, + outlineButtonPressed: { opacity: 0.6 }, + outlineButtonText: { color: OkenaColors.accent, fontSize: 16, fontWeight: '600' }, +}); + +export default PairingScreen; diff --git a/mobile/rn/src/screens/ServerListScreen.tsx b/mobile/rn/src/screens/ServerListScreen.tsx new file mode 100644 index 00000000..3e0be0a2 --- /dev/null +++ b/mobile/rn/src/screens/ServerListScreen.tsx @@ -0,0 +1,432 @@ +/** + * ServerListScreen.tsx — saved-server list + "add server" sheet. + * + * Ported from `mobile/lib/src/screens/server_list_screen.dart`: + * - header: "Servers" large title + a circular "+" add button, + * - empty state: terminal glyph, "No servers yet", "Add a server to get + * started", and an "Add Server" button, + * - list: one card per saved server — a letter avatar, the display name, and + * (when a label is set) the `host:port` subtitle; tapping connects. + * - add sheet: a bottom `Modal` with Host / Port / Label fields. + * + * The Dart screen used a `Dismissible` (swipe) to delete. RN core has no + * swipe-to-dismiss without extra native deps, so deletion is exposed via a + * long-press that reveals an inline delete affordance — same `removeServer` + * behavior, no new dependencies. + * + * Reads/writes the connection store; holds only local UI state (sheet + * visibility + field values + which row is in "delete" mode). + */ + +import React, { useState } from 'react'; +import { + ActivityIndicator, + FlatList, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; + +import { + createSavedServer, + savedServerDisplayName, + type SavedServer, +} from '../models'; +import { useConnectionStore } from '../state'; +import { OkenaColors, OkenaTypography } from '../theme'; + +const DEFAULT_PORT = '19100'; + +export const ServerListScreen: React.FC = () => { + const servers = useConnectionStore((s) => s.servers); + const loaded = useConnectionStore((s) => s.loaded); + const connectTo = useConnectionStore((s) => s.connectTo); + const addServer = useConnectionStore((s) => s.addServer); + const removeServer = useConnectionStore((s) => s.removeServer); + + const [sheetOpen, setSheetOpen] = useState(false); + // Which server (by key) currently shows its delete affordance (long-pressed). + const [pendingDeleteKey, setPendingDeleteKey] = useState(null); + + const serverKey = (s: SavedServer) => `${s.host}:${s.port}`; + + const renderItem = ({ item }: { item: SavedServer }) => { + const name = savedServerDisplayName(item); + const initial = name.charAt(0).toUpperCase() || '?'; + const showDelete = pendingDeleteKey === serverKey(item); + + return ( + + [styles.card, pressed && styles.cardPressed]} + onPress={() => { + if (showDelete) { + setPendingDeleteKey(null); + return; + } + connectTo(item); + }} + onLongPress={() => setPendingDeleteKey(showDelete ? null : serverKey(item))} + > + + {initial} + + + + {name} + + {item.label !== undefined && ( + + {item.host}:{item.port} + + )} + + {showDelete ? ( + { + setPendingDeleteKey(null); + removeServer(item); + }} + > + Delete + + ) : ( + {'›'} + )} + + + ); + }; + + return ( + + {/* Header */} + + Servers + [styles.addButton, pressed && styles.addButtonPressed]} + onPress={() => setSheetOpen(true)} + accessibilityLabel="Add server" + > + + + + + + {/* Content */} + {servers.length === 0 ? ( + loaded ? ( + setSheetOpen(true)} /> + ) : ( + + + + ) + ) : ( + + )} + + setSheetOpen(false)} + onAdd={(server) => { + addServer(server); + setSheetOpen(false); + }} + /> + + ); +}; + +// ── Empty state ───────────────────────────────────────────────────────────── + +const EmptyState: React.FC<{ onAdd: () => void }> = ({ onAdd }) => ( + + + {'⌨'} + + No servers yet + Add a server to get started + [styles.primaryButton, styles.emptyButton, pressed && styles.primaryButtonPressed]} + onPress={onAdd} + > + Add Server + + +); + +// ── Add-server bottom sheet ─────────────────────────────────────────────────── + +const AddServerSheet: React.FC<{ + visible: boolean; + onClose: () => void; + onAdd: (server: SavedServer) => void; +}> = ({ visible, onClose, onAdd }) => { + const [host, setHost] = useState(''); + const [port, setPort] = useState(DEFAULT_PORT); + const [label, setLabel] = useState(''); + + // Reset fields each time the sheet is opened. + const reset = () => { + setHost(''); + setPort(DEFAULT_PORT); + setLabel(''); + }; + + const trimmedHost = host.trim(); + const canSubmit = trimmedHost.length > 0; + + const submit = () => { + if (!canSubmit) return; + const parsedPort = parseInt(port.trim(), 10); + const server = createSavedServer({ + host: trimmedHost, + port: Number.isFinite(parsedPort) ? parsedPort : 19100, + label: label.trim() === '' ? undefined : label.trim(), + }); + onAdd(server); + reset(); + }; + + const close = () => { + reset(); + onClose(); + }; + + return ( + + {/* Tap the dimmed backdrop to dismiss. */} + + + {/* Stop taps inside the sheet from bubbling to the backdrop. */} + {}}> + + Add Server + + Enter the host and port of your Okena desktop app + + + + + + + [ + styles.primaryButton, + !canSubmit && styles.primaryButtonDisabled, + pressed && canSubmit && styles.primaryButtonPressed, + ]} + onPress={submit} + > + Add Server + + + + + + ); +}; + +const Field: React.FC< + React.ComponentProps & { label: string } +> = ({ label, ...inputProps }) => ( + + {label} + + +); + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: OkenaColors.background, + paddingTop: 44, // approximate top safe-area inset (no SafeAreaView dep) + }, + center: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 24, + }, + + // Header + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 24, + paddingTop: 20, + paddingBottom: 8, + }, + addButton: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OkenaColors.surfaceElevated, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + }, + addButtonPressed: { opacity: 0.6 }, + addButtonPlus: { + color: OkenaColors.accent, + fontSize: 22, + lineHeight: 24, + fontWeight: '400', + }, + + // List + listContent: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 24 }, + cardRow: { marginBottom: 8 }, + card: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderRadius: 14, + backgroundColor: OkenaColors.surface, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + }, + cardPressed: { opacity: 0.7 }, + avatar: { + width: 40, + height: 40, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#7c7fff1f', // accent @ ~12% + marginRight: 14, + }, + avatarText: { ...OkenaTypography.headline, color: OkenaColors.accent }, + cardBody: { flex: 1 }, + cardName: { ...OkenaTypography.body, fontWeight: '500' }, + cardSub: { + ...OkenaTypography.caption, + marginTop: 3, + color: OkenaColors.textTertiary, + fontFamily: 'JetBrainsMono', + }, + chevron: { color: OkenaColors.textTertiary, fontSize: 18, marginLeft: 8 }, + deleteBtn: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + backgroundColor: '#f8717126', // error @ ~15% + marginLeft: 8, + }, + deleteBtnText: { color: OkenaColors.error, fontWeight: '600', fontSize: 13 }, + + // Empty state + emptyGlyph: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#7c7fff26', // accent @ ~15% + }, + emptyGlyphText: { fontSize: 36, color: OkenaColors.accent }, + emptyTitle: { marginTop: 20 }, + emptySub: { ...OkenaTypography.body, color: OkenaColors.textSecondary, marginTop: 8 }, + emptyButton: { marginTop: 28, width: 180 }, + + // Primary button + primaryButton: { + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OkenaColors.accent, + }, + primaryButtonPressed: { opacity: 0.85 }, + primaryButtonDisabled: { opacity: 0.4 }, + primaryButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '600' }, + + // Add-server sheet + backdrop: { flex: 1, backgroundColor: '#000000aa', justifyContent: 'flex-end' }, + sheetWrap: { width: '100%' }, + sheet: { + backgroundColor: OkenaColors.surface, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + paddingHorizontal: 24, + paddingTop: 8, + paddingBottom: 32, + }, + dragHandle: { + width: 36, + height: 4, + borderRadius: 2, + backgroundColor: OkenaColors.textTertiary, + alignSelf: 'center', + marginBottom: 20, + opacity: 0.4, + }, + sheetSub: { ...OkenaTypography.callout, color: OkenaColors.textTertiary, marginTop: 4, marginBottom: 24 }, + field: { marginBottom: 14 }, + fieldLabel: { ...OkenaTypography.caption, color: OkenaColors.textSecondary, marginBottom: 6 }, + input: { + height: 44, + borderRadius: 10, + paddingHorizontal: 14, + backgroundColor: OkenaColors.surfaceElevated, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + color: OkenaColors.textPrimary, + fontSize: 15, + }, +}); + +export default ServerListScreen; diff --git a/mobile/rn/src/screens/WorkspaceScreen.tsx b/mobile/rn/src/screens/WorkspaceScreen.tsx new file mode 100644 index 00000000..5cba356e --- /dev/null +++ b/mobile/rn/src/screens/WorkspaceScreen.tsx @@ -0,0 +1,411 @@ +/** + * WorkspaceScreen.tsx — the connected workspace. + * + * Port of `mobile/lib/src/screens/workspace_screen.dart`. Composes: + * - an app bar: a drawer-toggle (☰), the selected project name (tap to switch + * when there are several), a small connection-quality dot, a fullscreen + * toggle, and a "+" terminal-actions menu (new / split V / split H / + * minimize). StatusIndicator is NOT imported (owned by another agent) — the + * dot is inline. + * - the layout area: the parsed project layout tree rendered by + * {@link LayoutRenderer}; when fullscreen, only the fullscreen terminal is + * shown via a single {@link TerminalPane}. Empty/no-terminal states match + * the Dart screen. + * - the {@link KeyToolbar} pinned above the soft keyboard (rendered inside a + * `KeyboardAvoidingView` so it sits on top of the keyboard). + * - the slide-in {@link ProjectDrawer}. + * + * Fonts are loaded here with Skia's `useFont` (see README) and threaded down to + * every terminal pane; the layout area guards on fonts being loaded. + * + * State comes from the stores; polling lifecycle is owned by App.tsx (this + * screen only reads + dispatches actions). + */ + +import React, { useMemo, useRef, useState } from 'react'; +import { + View, + Text, + Pressable, + KeyboardAvoidingView, + Platform, + Modal, + ScrollView, + StyleSheet, +} from 'react-native'; +import { useFont } from '@shopify/react-native-skia'; + +import { useWorkspaceStore, useConnectionStore } from '../state'; +import { parseLayout } from '../models'; +import { getOkenaNative, type OkenaNative } from '../native/okena'; +import { OkenaColors, TerminalTheme } from '../theme'; +import { LayoutRenderer } from '../components/LayoutRenderer'; +import { TerminalPane, type TerminalPaneHandle } from '../components/TerminalPane'; +import { KeyToolbar, KeyModifiers } from '../components/KeyToolbar'; +import { ProjectDrawer } from '../components/ProjectDrawer'; +import type { TerminalFonts } from '../components/TerminalView'; + +const native: OkenaNative = (() => { + try { + return getOkenaNative(); + } catch { + // Native not wired up (off-device). The screen still renders chrome; the + // panes guard their native calls. Throwing here would crash the whole app. + return undefined as unknown as OkenaNative; + } +})(); + +export const WorkspaceScreen: React.FC = () => { + // ── fonts (Skia useFont; null until loaded) ──────────────────────────────── + const fontSize = TerminalTheme.defaultFontSize; + const regular = useFont(require('../../assets/JetBrainsMono-Regular.ttf'), fontSize); + const bold = useFont(require('../../assets/JetBrainsMono-Bold.ttf'), fontSize); + const italic = useFont(require('../../assets/JetBrainsMono-Italic.ttf'), fontSize); + const boldItalic = useFont(require('../../assets/JetBrainsMono-BoldItalic.ttf'), fontSize); + + const fonts: TerminalFonts | null = useMemo( + () => + regular + ? { + regular, + bold: bold ?? undefined, + italic: italic ?? undefined, + boldItalic: boldItalic ?? undefined, + } + : null, + [regular, bold, italic, boldItalic], + ); + + // ── shared key-modifier store (toolbar + soft keyboard) ───────────────────── + const modifiers = useRef(new KeyModifiers()).current; + const paneRef = useRef(null); + + // ── stores ────────────────────────────────────────────────────────────────── + const projects = useWorkspaceStore((s) => s.projects); + const selectedProjectId = useWorkspaceStore((s) => s.selectedProjectId); + const selectedTerminalId = useWorkspaceStore((s) => s.selectedTerminalId); + const fullscreenTerminal = useWorkspaceStore((s) => s.fullscreenTerminal); + const secondsSinceActivity = useWorkspaceStore((s) => s.secondsSinceActivity); + const selectProject = useWorkspaceStore((s) => s.selectProject); + // Recompute the selected project from the live id (cheap selector helper). + const project = useMemo(() => { + if (selectedProjectId === null) return projects[0] ?? null; + return projects.find((p) => p.id === selectedProjectId) ?? projects[0] ?? null; + }, [projects, selectedProjectId]); + + const connId = useConnectionStore((s) => s.connId); + + const [drawerOpen, setDrawerOpen] = useState(false); + const [switcherOpen, setSwitcherOpen] = useState(false); + const [actionsOpen, setActionsOpen] = useState(false); + + // Layout JSON for the selected project, parsed. + const layoutNode = useMemo(() => { + const json = useWorkspaceStore.getState().getProjectLayoutJson(); + return json ? parseLayout(json) : null; + // re-parse whenever the project or its terminal set changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project?.id, project?.terminalIds.join(','), fullscreenTerminal?.terminalId]); + + const dotColor = + secondsSinceActivity < 3 + ? OkenaColors.success + : secondsSinceActivity < 10 + ? OkenaColors.warning + : OkenaColors.error; + + const projectName = project?.name ?? 'No Project'; + + // ── terminal actions ────────────────────────────────────────────────────── + const onNewTerminal = () => { + if (connId && project) void native?.createTerminal(connId, project.id); + }; + const onSplit = (direction: 'vertical' | 'horizontal') => { + if (connId && project) void native?.splitTerminal(connId, project.id, [], direction); + }; + const onMinimize = () => { + if (connId && project && selectedTerminalId) { + void native?.toggleMinimized(connId, project.id, selectedTerminalId); + } + }; + const onToggleFullscreen = () => { + if (!connId || !project) return; + if (fullscreenTerminal) { + void native?.setFullscreen(connId, project.id, undefined); + } else if (selectedTerminalId) { + void native?.setFullscreen(connId, project.id, selectedTerminalId); + } + }; + + // ── body ────────────────────────────────────────────────────────────────── + const renderBody = () => { + if (!connId || !project) { + return ( + + No project selected + + ); + } + if (!fonts) { + return ( + + Loading fonts… + + ); + } + // Fullscreen: just the one terminal. + if (fullscreenTerminal && fullscreenTerminal.projectId === project.id) { + return ( + + ); + } + if (project.terminalIds.length === 0) { + return ( + + No terminals + + New Terminal + + + ); + } + if (layoutNode) { + return ( + + ); + } + // Fallback: render the selected (or first) terminal directly. + const tid = selectedTerminalId ?? project.terminalIds[0]!; + return ( + + ); + }; + + const showToolbar = connId !== null && project !== null && selectedTerminalId !== null; + + return ( + + {/* App bar */} + + setDrawerOpen(true)}> + {'☰'} + + setSwitcherOpen(true)} + > + + {projectName} + + {projects.length > 1 ? {' ▾'} : null} + + + {connId ? : null} + {connId && project && selectedTerminalId ? ( + + {fullscreenTerminal ? '🗗' : '⛶'} + + ) : null} + {connId && project ? ( + setActionsOpen(true)}> + {'+'} + + ) : null} + + + {/* Body + key toolbar (toolbar rides above the keyboard) */} + + {renderBody()} + {showToolbar && connId ? ( + paneRef.current?.blur()} + /> + ) : null} + + + {/* Drawer */} + setDrawerOpen(false)} native={native} /> + + {/* Project switcher */} + setSwitcherOpen(false)} + > + setSwitcherOpen(false)}> + + + {projects.map((p) => { + const isSel = p.id === project?.id; + return ( + { + selectProject(p.id); + setSwitcherOpen(false); + }} + > + + {p.name} + + {p.gitBranch ? ( + + {`⎇ ${p.gitBranch}`} + + ) : null} + + ); + })} + + + + + + {/* Terminal actions menu */} + setActionsOpen(false)} + > + setActionsOpen(false)} /> + + { + setActionsOpen(false); + onNewTerminal(); + }} + /> + {selectedTerminalId ? ( + <> + { + setActionsOpen(false); + onSplit('vertical'); + }} + /> + { + setActionsOpen(false); + onSplit('horizontal'); + }} + /> + { + setActionsOpen(false); + onMinimize(); + }} + /> + + ) : null} + + + + ); +}; + +const Centered: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +const ActionItem: React.FC<{ label: string; onPress: () => void }> = ({ label, onPress }) => ( + + {label} + +); + +const styles = StyleSheet.create({ + root: { flex: 1, backgroundColor: OkenaColors.background }, + flex: { flex: 1 }, + flexSpacer: { flex: 1 }, + appBar: { + height: 96, + paddingTop: 44, + paddingHorizontal: 8, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: OkenaColors.surface, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: OkenaColors.border, + }, + appBarBtn: { padding: 8 }, + appBarIcon: { color: OkenaColors.textPrimary, fontSize: 18 }, + titleWrap: { flexDirection: 'row', alignItems: 'center', flexShrink: 1 }, + title: { color: OkenaColors.textPrimary, fontSize: 17, fontWeight: '600', flexShrink: 1 }, + titleCaret: { color: OkenaColors.textSecondary, fontSize: 14 }, + dot: { width: 8, height: 8, borderRadius: 4, marginHorizontal: 6 }, + centered: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }, + dim: { color: OkenaColors.textTertiary, fontSize: 14 }, + primaryBtn: { + marginTop: 16, + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: OkenaColors.accent, + }, + primaryBtnText: { color: '#ffffff', fontSize: 14, fontWeight: '600' }, + // project switcher menu + menuBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.3)', paddingTop: 96, paddingLeft: 48 }, + menu: { + backgroundColor: OkenaColors.surfaceElevated, + borderRadius: 10, + maxHeight: 360, + width: 260, + paddingVertical: 4, + }, + menuItem: { paddingHorizontal: 16, paddingVertical: 10 }, + menuItemText: { color: OkenaColors.textPrimary, fontSize: 14 }, + menuItemSelected: { color: OkenaColors.accent, fontWeight: '700' }, + menuBranch: { color: OkenaColors.textTertiary, fontSize: 11, marginTop: 2 }, + // action sheet + sheetBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }, + sheet: { + backgroundColor: OkenaColors.surfaceElevated, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 8, + paddingBottom: 32, + }, + sheetItem: { paddingVertical: 14, paddingHorizontal: 12 }, + sheetItemText: { color: OkenaColors.textPrimary, fontSize: 15 }, +}); + +export default WorkspaceScreen; diff --git a/mobile/rn/src/state/connectionStore.ts b/mobile/rn/src/state/connectionStore.ts new file mode 100644 index 00000000..2e8952cc --- /dev/null +++ b/mobile/rn/src/state/connectionStore.ts @@ -0,0 +1,354 @@ +/** + * connectionStore.ts — connection lifecycle + saved-server list (zustand). + * + * Ports `mobile/lib/src/providers/connection_provider.dart` (a polling + * `ChangeNotifier`) to a zustand store. Responsibilities: + * - the persisted list of {@link SavedServer}s (add / remove / update), + * - the current connection (`connId`, `activeServer`) and its `status`, + * - the connect → pair → disconnect lifecycle, + * - `secondsSinceActivity` (staleness indicator), + * - status polling: ~500ms while connecting/pairing, ~2s once connected + * (matching the Dart `_startPolling(fast:)` cadence), + * - persisting the auth token (and pinned cert fingerprint) back onto the + * saved server once paired. + * + * Dependencies — the native module and persistence — are INJECTED via + * {@link configureConnectionStore}, mirroring `TerminalView`'s `native` prop. + * Until `ubrn` generates the real module, `getOkenaNative()` throws; the store + * is constructed lazily so merely importing this module never calls it. Tests + * inject a mock `OkenaNative` + an in-memory `Persistence`. + */ + +import { create, type StoreApi, type UseBoundStore } from 'zustand'; + +import type { ConnectionStatus, ConnId, OkenaNative } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { + createSavedServer, + listFromJson, + listToJson, + savedServerEquals, + withSavedServer, + type SavedServer, +} from '../models/savedServer'; +import { + asyncStoragePersistence, + type Persistence, +} from './persistence'; + +/** AsyncStorage key for the persisted server list (matches Dart `_kSavedServersKey`). */ +export const SAVED_SERVERS_KEY = 'saved_servers'; + +/** Fast poll interval (ms) while connecting / pairing — Dart used 500ms. */ +export const FAST_POLL_MS = 500; +/** Slow poll interval (ms) once connected — Dart used 2000ms. */ +export const SLOW_POLL_MS = 2000; + +/** Initial, disconnected status. */ +const DISCONNECTED: ConnectionStatus = { kind: 'disconnected' }; + +/** Dependencies the store calls out to. Overridable for tests. */ +export interface ConnectionDeps { + native: OkenaNative; + persistence: Persistence; +} + +/** + * The connection store's state + actions. Screen agents read fields with the + * `useConnectionStore(selector)` hook and call the action methods. + */ +export interface ConnectionState { + // ── state ──────────────────────────────────────────────────────────────── + /** All saved servers (persisted). */ + servers: SavedServer[]; + /** The server currently being connected to / paired / connected, if any. */ + activeServer: SavedServer | null; + /** The live connection id from `connect`, if any. */ + connId: ConnId | null; + /** Current connection status (polled from the native module). */ + status: ConnectionStatus; + /** Seconds since the last WS activity; large when disconnected. */ + secondsSinceActivity: number; + /** Whether the persisted server list has finished loading. */ + loaded: boolean; + + // ── derived helpers (cheap, recomputed by callers via selectors) ───────── + // (Booleans like isConnected are intentionally NOT stored; derive from + // `status.kind` in the component or via the exported selectors below.) + + // ── actions ────────────────────────────────────────────────────────────── + /** + * One-time native init (`OkenaNative.initApp()`). Call once at app start + * (App.tsx) before connecting. Routed through the store so the configured + * (possibly-mocked) native module is used. No-op-safe to call once. + */ + initApp(): void; + /** + * Load the persisted server list from storage. Call once at app start (e.g. + * in `App.tsx`). Safe to call again; it just re-reads. Mirrors the Dart + * `_loadServers()` invoked from the provider constructor. + */ + loadServers(): Promise; + /** Add a server (deduped by host+port) and persist. Mirrors `addServer`. */ + addServer(server: SavedServer): void; + /** Remove a server (by host+port) and persist. Mirrors `removeServer`. */ + removeServer(server: SavedServer): void; + /** + * Replace an existing server (matched by host+port) with `updated` and + * persist. Used by the add/edit sheet; not in the Dart original but the + * contract ("add/remove/update") asks for it. + */ + updateServer(updated: SavedServer): void; + /** + * Begin connecting to `server`. Disconnects any current connection first, + * sets status to `connecting`, and starts fast polling. Mirrors `connectTo`. + */ + connectTo(server: SavedServer): void; + /** + * Pair the current connection with a code (async; awaits the server). On + * failure sets status to `error`. Mirrors `pair`. + */ + pair(code: string): Promise; + /** Tear down the current connection and reset to disconnected. Mirrors `disconnect`. */ + disconnect(): void; +} + +/** + * Lazily-resolved deps. Resolving is deferred until the first action that needs + * the native module / storage, so importing this file (and constructing the + * store) never throws via `getOkenaNative()`. + */ +let injectedDeps: Partial = {}; +let resolvedDeps: ConnectionDeps | null = null; + +function deps(): ConnectionDeps { + if (!resolvedDeps) { + resolvedDeps = { + native: injectedDeps.native ?? getOkenaNative(), + persistence: injectedDeps.persistence ?? asyncStoragePersistence, + }; + } + return resolvedDeps; +} + +/** + * Override the store's dependencies (native module + persistence). Call BEFORE + * the first action runs — e.g. at the top of a test, or once at app start if + * you wire a custom native module. Subsequent calls reset the resolved cache. + */ +export function configureConnectionStore(overrides: Partial): void { + injectedDeps = { ...injectedDeps, ...overrides }; + resolvedDeps = null; +} + +// ── polling: the store owns a single timer (start/stop) ───────────────────── + +let pollTimer: ReturnType | null = null; + +function stopPolling(): void { + if (pollTimer !== null) { + clearInterval(pollTimer); + pollTimer = null; + } +} + +function startPolling( + get: StoreApi['getState'], + set: StoreApi['setState'], + fast: boolean, +): void { + stopPolling(); + pollTimer = setInterval(() => pollStatus(get, set), fast ? FAST_POLL_MS : SLOW_POLL_MS); +} + +/** Persist the active server list to storage. Fire-and-forget (Dart did the same). */ +function persistServers(servers: readonly SavedServer[]): void { + void deps().persistence.setItem(SAVED_SERVERS_KEY, listToJson(servers)); +} + +/** + * After connecting, copy the freshly-negotiated auth token (and pinned cert + * fingerprint, which the Dart model lacked) back onto the active server and + * persist. Mirrors `_persistToken`. + */ +function persistToken( + get: StoreApi['getState'], + set: StoreApi['setState'], +): void { + const { connId, activeServer } = get(); + if (!connId || !activeServer) return; + const token = deps().native.getToken(connId); + if (token && token !== activeServer.token) { + const updated = withSavedServer(activeServer, { token }); + const servers = get().servers.map((s) => + savedServerEquals(s, activeServer) ? updated : s, + ); + set({ servers, activeServer: updated }); + persistServers(servers); + } +} + +/** + * One poll tick: refresh `status` + `secondsSinceActivity`. On the + * connecting→connected edge, switch to slow polling and persist the token; on + * disconnect/error, stop polling. Mirrors `_pollStatus`. + */ +function pollStatus( + get: StoreApi['getState'], + set: StoreApi['setState'], +): void { + const { connId, status: oldStatus } = get(); + if (!connId) return; + + const native = deps().native; + const newStatus = native.connectionStatus(connId); + const activity = native.secondsSinceActivity(connId); + + set({ status: newStatus, secondsSinceActivity: activity }); + + if (newStatus.kind === 'connected' && oldStatus.kind !== 'connected') { + // Connected edge: slow down polling and capture the token. + startPolling(get, set, /* fast */ false); + persistToken(get, set); + } + + if (newStatus.kind === 'disconnected' || newStatus.kind === 'error') { + stopPolling(); + } +} + +/** + * The connection store hook + bound store. + * + * Constructing it is side-effect-free w.r.t. the native module: `create`'s + * initializer only builds the state object + action closures; none of them call + * `deps()` (and thus `getOkenaNative()`) until an action actually runs. So this + * can be a normal module-level zustand store — no lazy wrapper needed. + * + * Use it like any zustand hook, and `.getState()` / `.subscribe()` for + * imperative access: + * + * ```ts + * const status = useConnectionStore((s) => s.status); + * const connectTo = useConnectionStore((s) => s.connectTo); + * const isConn = selectIsConnected(useConnectionStore.getState()); + * ``` + */ +export const useConnectionStore: UseBoundStore> = + create((set, get) => ({ + servers: [], + activeServer: null, + connId: null, + status: DISCONNECTED, + secondsSinceActivity: Number.MAX_SAFE_INTEGER, + loaded: false, + + initApp() { + deps().native.initApp(); + }, + + async loadServers() { + let servers: SavedServer[] = []; + try { + const json = await deps().persistence.getItem(SAVED_SERVERS_KEY); + if (json) servers = listFromJson(json); + } catch { + // Corrupted data — start fresh (matches Dart's silent catch). + servers = []; + } + set({ servers, loaded: true }); + }, + + addServer(server) { + const { servers } = get(); + if (servers.some((s) => savedServerEquals(s, server))) return; + const next = [...servers, server]; + set({ servers: next }); + persistServers(next); + }, + + removeServer(server) { + const next = get().servers.filter((s) => !savedServerEquals(s, server)); + set({ servers: next }); + persistServers(next); + }, + + updateServer(updated) { + const next = get().servers.map((s) => + savedServerEquals(s, updated) ? updated : s, + ); + set({ servers: next }); + persistServers(next); + }, + + connectTo(server) { + const native = deps().native; + const { connId } = get(); + // Disconnect any existing connection first (matches Dart). + if (connId) { + native.disconnect(connId); + stopPolling(); + } + const newConnId = native.connect(server.host, server.port, server.token); + set({ + activeServer: server, + connId: newConnId, + status: { kind: 'connecting' }, + secondsSinceActivity: Number.MAX_SAFE_INTEGER, + }); + startPolling(get, set, /* fast */ true); + }, + + async pair(code) { + const { connId } = get(); + if (!connId) return; + try { + await deps().native.pair(connId, code); + } catch (e) { + set({ + status: { kind: 'error', message: e instanceof Error ? e.message : String(e) }, + }); + } + }, + + disconnect() { + const { connId } = get(); + if (connId) deps().native.disconnect(connId); + stopPolling(); + set({ + connId: null, + activeServer: null, + status: DISCONNECTED, + secondsSinceActivity: Number.MAX_SAFE_INTEGER, + }); + }, + })); + +/** + * Imperative store handle (`getState` / `setState` / `subscribe`) for non-React + * consumers — e.g. {@link import('../navigation/navStore')} subscribing to + * status changes to drive navigation. Same instance as the hook. + */ +export function connectionStore(): StoreApi { + return useConnectionStore; +} + +// ── selectors (derive the Dart `is*` getters from `status`) ───────────────── + +/** True once the connection is established. Mirrors Dart `isConnected`. */ +export const selectIsConnected = (s: ConnectionState): boolean => + s.status.kind === 'connected'; +/** True while pairing. Mirrors Dart `isPairing`. */ +export const selectIsPairing = (s: ConnectionState): boolean => s.status.kind === 'pairing'; +/** True while connecting. Mirrors Dart `isConnecting`. */ +export const selectIsConnecting = (s: ConnectionState): boolean => + s.status.kind === 'connecting'; +/** + * True when fully idle: disconnected AND no active server selected. Mirrors + * Dart `isDisconnected` (used to decide the server-list screen is shown). + */ +export const selectIsDisconnected = (s: ConnectionState): boolean => + s.status.kind === 'disconnected' && s.activeServer === null; + +/** Re-export the convenience constructor so callers don't reach into the model. */ +export { createSavedServer }; diff --git a/mobile/rn/src/state/index.ts b/mobile/rn/src/state/index.ts new file mode 100644 index 00000000..acf51706 --- /dev/null +++ b/mobile/rn/src/state/index.ts @@ -0,0 +1,36 @@ +/** + * state/index.ts — public surface of the state layer. + * + * Screen agents import the store hooks + selectors from here. + */ + +export { + useConnectionStore, + connectionStore, + configureConnectionStore, + createSavedServer, + selectIsConnected, + selectIsPairing, + selectIsConnecting, + selectIsDisconnected, + SAVED_SERVERS_KEY, + FAST_POLL_MS, + SLOW_POLL_MS, + type ConnectionState, + type ConnectionDeps, +} from './connectionStore'; + +export { + useWorkspaceStore, + workspaceStore, + configureWorkspaceStore, + WORKSPACE_POLL_MS, + type WorkspaceState, + type WorkspaceDeps, +} from './workspaceStore'; + +export { + asyncStoragePersistence, + createMemoryPersistence, + type Persistence, +} from './persistence'; diff --git a/mobile/rn/src/state/persistence.ts b/mobile/rn/src/state/persistence.ts new file mode 100644 index 00000000..98c4e891 --- /dev/null +++ b/mobile/rn/src/state/persistence.ts @@ -0,0 +1,74 @@ +/** + * persistence.ts — a tiny async key-value store abstraction. + * + * The Flutter app persisted the saved-server list via `shared_preferences` + * (see `connection_provider.dart`). The RN equivalent is + * `@react-native-async-storage/async-storage`. + * + * The connection store talks to this {@link Persistence} interface rather than + * AsyncStorage directly, so tests can inject an in-memory implementation (same + * injection philosophy as `TerminalView`'s `native` prop). The default used by + * the app is {@link asyncStoragePersistence}. + */ + +/** + * Minimal async key-value contract. A subset of the AsyncStorage API — just the + * three calls the stores need. + */ +export interface Persistence { + /** Read a string value, or `null` if the key is absent. */ + getItem(key: string): Promise; + /** Write a string value. */ + setItem(key: string, value: string): Promise; + /** Remove a key. */ + removeItem(key: string): Promise; +} + +/** + * The real implementation, backed by + * `@react-native-async-storage/async-storage`. + * + * The import is intentionally lazy (inside each method) so that: + * - merely importing this module doesn't pull in the native AsyncStorage + * module (which throws off-device), and + * - `tsc` / tests that never touch the real persistence don't need the native + * side present. + */ +export const asyncStoragePersistence: Persistence = { + async getItem(key) { + const AsyncStorage = (await import('@react-native-async-storage/async-storage')) + .default; + return AsyncStorage.getItem(key); + }, + async setItem(key, value) { + const AsyncStorage = (await import('@react-native-async-storage/async-storage')) + .default; + await AsyncStorage.setItem(key, value); + }, + async removeItem(key) { + const AsyncStorage = (await import('@react-native-async-storage/async-storage')) + .default; + await AsyncStorage.removeItem(key); + }, +}; + +/** + * A simple in-memory {@link Persistence} — handy for tests and Storybook. Not + * used by the app at runtime. + */ +export function createMemoryPersistence( + initial: Record = {}, +): Persistence { + const map = new Map(Object.entries(initial)); + return { + getItem: (key) => Promise.resolve(map.get(key) ?? null), + setItem: (key, value) => { + map.set(key, value); + return Promise.resolve(); + }, + removeItem: (key) => { + map.delete(key); + return Promise.resolve(); + }, + }; +} diff --git a/mobile/rn/src/state/workspaceStore.ts b/mobile/rn/src/state/workspaceStore.ts new file mode 100644 index 00000000..b7f0d131 --- /dev/null +++ b/mobile/rn/src/state/workspaceStore.ts @@ -0,0 +1,336 @@ +/** + * workspaceStore.ts — project / folder / layout state (zustand). + * + * Ports `mobile/lib/src/providers/workspace_provider.dart`. While connected it + * polls the cached remote state (~1s) for: + * - the project list (`getProjects`), + * - folders (`getFolders`), + * - project order (`getProjectOrder`), + * - the server's focused project (`getFocusedProjectId`), + * - the fullscreen terminal (`getFullscreenTerminal`). + * + * It auto-selects the focused project when nothing is selected, and auto-selects + * a terminal within the selected project (newly-added one, or the first if the + * current selection vanished) — mirroring the Dart `_pollState` logic. + * + * Lifecycle: the Dart provider listened to the connection provider and + * started/stopped polling on connect/disconnect. Here that wiring is explicit — + * call {@link WorkspaceState.start} when the connection becomes connected and + * {@link WorkspaceState.stop} when it isn't (App.tsx wires this; see the nav + * layer). `start` needs the live `connId`. + * + * Dependencies (the native module) are injected via + * {@link configureWorkspaceStore}, same philosophy as the connection store. + */ + +import { create, type StoreApi, type UseBoundStore } from 'zustand'; + +import type { + ConnId, + FolderInfo, + FullscreenInfo, + OkenaNative, + ProjectId, + ProjectInfo, + TerminalId, +} from '../native/okena'; +import { getOkenaNative } from '../native/okena'; + +/** Poll interval (ms) for the workspace state — Dart used 1000ms. */ +export const WORKSPACE_POLL_MS = 1000; + +/** Dependencies the store calls out to. Overridable for tests. */ +export interface WorkspaceDeps { + native: OkenaNative; +} + +/** + * The workspace store's state + actions. Screen agents read fields with the + * `useWorkspaceStore(selector)` hook and call the action methods. + */ +export interface WorkspaceState { + // ── state ──────────────────────────────────────────────────────────────── + /** Projects from the cached remote state. */ + projects: ProjectInfo[]; + /** Folders from the cached remote state. */ + folders: FolderInfo[]; + /** Server-defined project ordering (list of project ids). */ + projectOrder: ProjectId[]; + /** The active fullscreen terminal, if any. */ + fullscreenTerminal: FullscreenInfo | null; + /** Currently-selected project id (auto-selected from focused, or by the user). */ + selectedProjectId: ProjectId | null; + /** Currently-selected terminal id within the selected project. */ + selectedTerminalId: TerminalId | null; + /** Seconds since last WS activity (also tracked here, per the Dart provider). */ + secondsSinceActivity: number; + + // ── actions ──────────────────────────────────────────────────────────── + /** + * Start polling for the given connection. Idempotent: re-calling with a new + * `connId` restarts against it. Does an immediate first poll (as Dart did). + * Call when the connection becomes `connected`. + */ + start(connId: ConnId): void; + /** + * Stop polling and clear all workspace state. Call when the connection is no + * longer connected (mirrors the Dart `_onConnectionChanged` else-branch). + */ + stop(): void; + /** Select a project; clears the terminal selection so it re-auto-selects. Mirrors `selectProject`. */ + selectProject(projectId: ProjectId): void; + /** Select a terminal within the current project. Mirrors `selectTerminal`. */ + selectTerminal(terminalId: TerminalId): void; + /** + * The currently-selected project object, or the first project as a fallback, + * or `null`. Mirrors the Dart `selectedProject` getter. (Provided as a + * selector helper since zustand state holds only the id.) + */ + getSelectedProject(): ProjectInfo | null; + /** + * The layout JSON for the selected project, via the native module. `null` if + * not connected or no project selected. Mirrors `getProjectLayoutJson`. + * Parse it with {@link import('../models/layoutNode').parseLayout}. + */ + getProjectLayoutJson(): string | undefined; +} + +let injectedDeps: Partial = {}; +let resolvedDeps: WorkspaceDeps | null = null; + +function deps(): WorkspaceDeps { + if (!resolvedDeps) { + resolvedDeps = { native: injectedDeps.native ?? getOkenaNative() }; + } + return resolvedDeps; +} + +/** + * Override the store's native module. Call before the first action runs (e.g. + * at the top of a test, or once at app start). + */ +export function configureWorkspaceStore(overrides: Partial): void { + injectedDeps = { ...injectedDeps, ...overrides }; + resolvedDeps = null; +} + +// ── polling: the store owns a single timer + the live connId ──────────────── + +let pollTimer: ReturnType | null = null; +let activeConnId: ConnId | null = null; +/** Previous terminal-id set for the selected project (newly-added detection). */ +let previousTerminalIds: Set | null = null; + +function stopPolling(): void { + if (pollTimer !== null) { + clearInterval(pollTimer); + pollTimer = null; + } + activeConnId = null; + previousTerminalIds = null; +} + +/** Shallow id+name+git+services equality for the project list (mirrors `_projectListEquals`). */ +function projectListEquals(a: ProjectInfo[], b: ProjectInfo[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + const pa = a[i]!; + const pb = b[i]!; + if (pa.id !== pb.id || pa.name !== pb.name) return false; + if (!arrayEquals(pa.terminalIds, pb.terminalIds)) return false; + if (pa.gitBranch !== pb.gitBranch) return false; + if (pa.gitLinesAdded !== pb.gitLinesAdded) return false; + if (pa.gitLinesRemoved !== pb.gitLinesRemoved) return false; + if (pa.services.length !== pb.services.length) return false; + for (let j = 0; j < pa.services.length; j++) { + if ( + pa.services[j]!.name !== pb.services[j]!.name || + pa.services[j]!.status !== pb.services[j]!.status + ) { + return false; + } + } + if (pa.folderColor !== pb.folderColor) return false; + } + return true; +} + +function arrayEquals(a: readonly T[], b: readonly T[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +/** Resolve the selected project from a project list + selected id (Dart `selectedProject`). */ +function resolveSelectedProject( + projects: ProjectInfo[], + selectedProjectId: ProjectId | null, +): ProjectInfo | null { + if (selectedProjectId === null) return projects[0] ?? null; + return projects.find((p) => p.id === selectedProjectId) ?? projects[0] ?? null; +} + +/** One poll tick — refresh remote state + run auto-selection. Mirrors `_pollState`. */ +function pollState( + get: StoreApi['getState'], + set: StoreApi['setState'], +): void { + const connId = activeConnId; + if (!connId) return; + const native = deps().native; + + const newProjects = native.getProjects(connId); + const focusedId = native.getFocusedProjectId(connId); + const newFolders = native.getFolders(connId); + const newProjectOrder = native.getProjectOrder(connId); + const newFullscreen = native.getFullscreenTerminal(connId) ?? null; + + const prev = get(); + const patch: Partial = {}; + let changed = false; + + if (!projectListEquals(newProjects, prev.projects)) { + patch.projects = newProjects; + changed = true; + } + if ( + !arrayEquals( + newFolders.map((f) => f.id), + prev.folders.map((f) => f.id), + ) + ) { + patch.folders = newFolders; + changed = true; + } + if (!arrayEquals(newProjectOrder, prev.projectOrder)) { + patch.projectOrder = newProjectOrder; + changed = true; + } + if (newFullscreen?.terminalId !== prev.fullscreenTerminal?.terminalId) { + patch.fullscreenTerminal = newFullscreen; + changed = true; + } + + // Work against the freshest values for the auto-select logic below. + const projects = patch.projects ?? prev.projects; + let selectedProjectId = prev.selectedProjectId; + let selectedTerminalId = prev.selectedTerminalId; + + // Auto-select the focused project if nothing is selected. + if (selectedProjectId === null && focusedId) { + selectedProjectId = focusedId; + patch.selectedProjectId = focusedId; + changed = true; + } + + // Auto-select a terminal: pick a newly-added one, or the first if the current + // selection is gone (matches the Dart logic + `_previousTerminalIds`). + const project = resolveSelectedProject(projects, selectedProjectId); + if (project && project.terminalIds.length > 0) { + if (selectedTerminalId === null || !project.terminalIds.includes(selectedTerminalId)) { + selectedTerminalId = project.terminalIds[0]!; + patch.selectedTerminalId = selectedTerminalId; + changed = true; + } else if (previousTerminalIds !== null) { + const newIds = project.terminalIds.filter((id) => !previousTerminalIds!.has(id)); + if (newIds.length > 0) { + selectedTerminalId = newIds[newIds.length - 1]!; + patch.selectedTerminalId = selectedTerminalId; + changed = true; + } + } + previousTerminalIds = new Set(project.terminalIds); + } else { + previousTerminalIds = null; + if (selectedTerminalId !== null) { + patch.selectedTerminalId = null; + changed = true; + } + } + + // Connection health (drives the staleness indicator's 3s / 10s thresholds). + const newActivity = native.secondsSinceActivity(connId); + const oldActivity = prev.secondsSinceActivity; + if ( + (oldActivity < 3) !== (newActivity < 3) || + (oldActivity < 10) !== (newActivity < 10) + ) { + changed = true; + } + patch.secondsSinceActivity = newActivity; + + if (changed) set(patch); + else if (patch.secondsSinceActivity !== undefined) { + // Always keep the raw activity number current, even when no UI-visible + // threshold crossed (cheap, avoids a stale value). + set({ secondsSinceActivity: newActivity }); + } +} + +/** + * The workspace store hook + bound store. Module-level (construction is + * side-effect-free w.r.t. the native module — see the connection store for the + * same reasoning). Use it like any zustand hook, plus `.getState()` for + * imperative access: + * + * ```ts + * const projects = useWorkspaceStore((s) => s.projects); + * const selectProject = useWorkspaceStore((s) => s.selectProject); + * ``` + */ +export const useWorkspaceStore: UseBoundStore> = + create((set, get) => ({ + projects: [], + folders: [], + projectOrder: [], + fullscreenTerminal: null, + selectedProjectId: null, + selectedTerminalId: null, + secondsSinceActivity: 0, + + start(connId) { + if (pollTimer !== null && activeConnId === connId) return; // already polling this conn + stopPolling(); + activeConnId = connId; + pollTimer = setInterval(() => pollState(get, set), WORKSPACE_POLL_MS); + pollState(get, set); // immediate first poll + }, + + stop() { + stopPolling(); + set({ + projects: [], + folders: [], + projectOrder: [], + fullscreenTerminal: null, + selectedProjectId: null, + selectedTerminalId: null, + }); + }, + + selectProject(projectId) { + previousTerminalIds = null; + set({ selectedProjectId: projectId, selectedTerminalId: null }); + }, + + selectTerminal(terminalId) { + set({ selectedTerminalId: terminalId }); + }, + + getSelectedProject() { + const { projects, selectedProjectId } = get(); + return resolveSelectedProject(projects, selectedProjectId); + }, + + getProjectLayoutJson() { + if (!activeConnId) return undefined; + const project = get().getSelectedProject(); + if (!project) return undefined; + return deps().native.getProjectLayoutJson(activeConnId, project.id); + }, + })); + +/** Imperative store handle for non-React consumers (e.g. App.tsx lifecycle wiring). */ +export function workspaceStore(): StoreApi { + return useWorkspaceStore; +} diff --git a/mobile/rn/src/theme.ts b/mobile/rn/src/theme.ts new file mode 100644 index 00000000..190d83ae --- /dev/null +++ b/mobile/rn/src/theme.ts @@ -0,0 +1,107 @@ +/** + * theme.ts — colors and typography, ported from + * `mobile/lib/src/theme/app_theme.dart`. + * + * Dart `Color(0xAARRGGBB)` values are kept verbatim as packed ARGB numbers in + * `argb`, and also exposed as RN-friendly `#RRGGBB[AA]` hex strings (RN's + * `color` props want `#RRGGBBAA`, NOT `#AARRGGBB`). + */ + +// ── helpers ────────────────────────────────────────────────────────────── + +/** `0xAARRGGBB` (Flutter order) → `#RRGGBBAA` (RN/CSS order). */ +function argbToHex(argb: number): string { + const v = argb >>> 0; + const a = (v >>> 24) & 0xff; + const r = (v >>> 16) & 0xff; + const g = (v >>> 8) & 0xff; + const b = v & 0xff; + const h = (n: number) => n.toString(16).padStart(2, '0'); + return `#${h(r)}${h(g)}${h(b)}${h(a)}`; +} + +// ── Color system (mirrors OkenaColors in app_theme.dart) ─────────────────── + +const OkenaColorsArgb = { + // Backgrounds + background: 0xff000000, + surface: 0xff0a0a0a, + surfaceElevated: 0xff161616, + surfaceOverlay: 0xff1c1c1c, + // Borders + border: 0xff1e1e1e, + borderLight: 0xff2a2a2a, + // Accent + accent: 0xff7c7fff, + // Text + textPrimary: 0xffe8e8ec, + textSecondary: 0xff98989f, + textTertiary: 0xff5a5a62, + // Semantic + success: 0xff4ade80, + warning: 0xfffbbf24, + error: 0xfff87171, + // Glass + glassBg: 0xcc0a0a0a, + glassStroke: 0x18ffffff, + // Key toolbar + keyBg: 0xff161616, + keyBorder: 0xff2a2a2a, + keyText: 0xffb0b0b8, +} as const; + +type ColorName = keyof typeof OkenaColorsArgb; + +/** Packed ARGB form (`0xAARRGGBB`) — matches the Dart `Color(...)` literals. */ +export const OkenaColorsArgbMap: Readonly> = OkenaColorsArgb; + +/** RN/CSS hex form (`#RRGGBBAA`). */ +export const OkenaColors: Readonly> = Object.fromEntries( + (Object.keys(OkenaColorsArgb) as ColorName[]).map((k) => [k, argbToHex(OkenaColorsArgb[k])]), +) as Record; + +// ── Terminal theme (mirrors TerminalTheme in app_theme.dart) ─────────────── + +export const TerminalTheme = { + /** Must match the loaded font family name (see README font-linking step). */ + fontFamily: 'JetBrainsMono', + fontFamilyFallback: ['Menlo', 'Consolas', 'DejaVu Sans Mono', 'monospace'] as const, + + defaultFontSize: 13, + minFontSize: 6, + maxFontSize: 24, + defaultColumns: 80, + lineHeightFactor: 1.2, + + // Packed ARGB (for buffer comparisons against cell bg) + hex (for paints). + bgColorArgb: 0xff000000, + fgColorArgb: 0xffcdd6f4, + cursorColorArgb: 0xfff5e0dc, + selectionColorArgb: 0x40585b70, + + bgColor: argbToHex(0xff000000), + fgColor: argbToHex(0xffcdd6f4), + cursorColor: argbToHex(0xfff5e0dc), + selectionColor: argbToHex(0x40585b70), + + /** Selection highlight overlay, matches `0x40264F78` in terminal_painter.dart. */ + selectionOverlayArgb: 0x40264f78, + selectionOverlay: argbToHex(0x40264f78), +} as const; + +// ── Typography (mirrors OkenaTypography; RN uses system font for `.SF Pro`) ── + +/** + * On iOS, `System` resolves to SF Pro. On Android there is no SF Pro, so this + * falls back to the platform default (Roboto) — acceptable for the chrome. + */ +export const OkenaTypography = { + fontFamily: 'System', + largeTitle: { fontSize: 28, fontWeight: '700', letterSpacing: -0.5, color: OkenaColors.textPrimary }, + title: { fontSize: 20, fontWeight: '600', letterSpacing: -0.3, color: OkenaColors.textPrimary }, + headline: { fontSize: 17, fontWeight: '600', color: OkenaColors.textPrimary }, + body: { fontSize: 15, fontWeight: '400', color: OkenaColors.textPrimary }, + callout: { fontSize: 14, fontWeight: '400', color: OkenaColors.textSecondary }, + caption: { fontSize: 12, fontWeight: '500', color: OkenaColors.textSecondary }, + caption2: { fontSize: 11, fontWeight: '500', color: OkenaColors.textTertiary }, +} as const; diff --git a/mobile/rn/tsconfig.json b/mobile/rn/tsconfig.json new file mode 100644 index 00000000..e0d354a4 --- /dev/null +++ b/mobile/rn/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM"], + "jsx": "react-jsx", + + "strict": true, + "noUncheckedIndexedAccess": false, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + + "noEmit": true, + + "types": ["react", "react-native"] + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/mobile/rn/ubrn.config.yaml b/mobile/rn/ubrn.config.yaml new file mode 100644 index 00000000..cd83aeed --- /dev/null +++ b/mobile/rn/ubrn.config.yaml @@ -0,0 +1,43 @@ +# uniffi-bindgen-react-native (ubrn) configuration. +# +# Drives the cross-compile of the Rust FFI crate (crates/okena-mobile-ffi) and +# the generation of the JSI TurboModule + TypeScript bindings. Consumed by: +# npm run ubrn:android → ubrn build android --config ubrn.config.yaml --and-generate +# npm run ubrn:ios → ubrn build ios --config ubrn.config.yaml --and-generate +# +# Requires the mobile toolchain (Android NDK + cargo-ndk, or Xcode) — see +# README.md §"Generate the native module". Not run in CI. +# +# VERSION PAIRING: ubrn and uniffi minor versions must match. This repo uses +# `uniffi-bindgen-react-native` 0.31.0-3 (devDependencies) ↔ `uniffi = "0.31"` +# in crates/okena-mobile-ffi/Cargo.toml. Bump both together. + +# Native/JS module name. +name: okena_mobile_ffi + +# The Rust crate to build, relative to this file. +rust: + directory: ../../crates/okena-mobile-ffi + manifestPath: Cargo.toml + +# Where the generated glue lands (both dirs are gitignored — regenerated by +# `npm run ubrn:*`). The hand-written binding contract/shim stays in +# src/native/okena.ts and re-exports from src/generated. +bindings: + ts: src/generated + cpp: cpp/generated + +# Android: build for all four ABIs. `directory`/`jniLibs` use ubrn defaults +# (android/, android/app/src/main/jniLibs) once the native host project exists. +android: + targets: + - arm64-v8a + - armeabi-v7a + - x86_64 + - x86 + +# iOS: device + simulator (sim arch picked to match the build host). +ios: + targets: + - aarch64-apple-ios + - aarch64-apple-ios-sim diff --git a/mobile/rust_builder/.gitignore b/mobile/rust_builder/.gitignore deleted file mode 100644 index ac5aa989..00000000 --- a/mobile/rust_builder/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -build/ diff --git a/mobile/rust_builder/README.md b/mobile/rust_builder/README.md deleted file mode 100644 index 922615f9..00000000 --- a/mobile/rust_builder/README.md +++ /dev/null @@ -1 +0,0 @@ -Please ignore this folder, which is just glue to build Rust with Flutter. \ No newline at end of file diff --git a/mobile/rust_builder/android/.gitignore b/mobile/rust_builder/android/.gitignore deleted file mode 100644 index 161bdcda..00000000 --- a/mobile/rust_builder/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/mobile/rust_builder/android/build.gradle b/mobile/rust_builder/android/build.gradle deleted file mode 100644 index ad905207..00000000 --- a/mobile/rust_builder/android/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// The Android Gradle Plugin builds the native code with the Android NDK. - -group 'com.flutter_rust_bridge.rust_lib_mobile' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - // The Android Gradle Plugin knows how to build native code with the NDK. - classpath 'com.android.tools.build:gradle:7.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - if (project.android.hasProperty("namespace")) { - namespace 'com.flutter_rust_bridge.rust_lib_mobile' - } - - // Bumping the plugin compileSdkVersion requires all clients of this plugin - // to bump the version in their app. - compileSdkVersion 33 - - // Use the NDK version - // declared in /android/app/build.gradle file of the Flutter project. - // Replace it with a version number if this plugin requires a specfic NDK version. - // (e.g. ndkVersion "23.1.7779620") - ndkVersion android.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdkVersion 19 - } -} - -apply from: "../cargokit/gradle/plugin.gradle" -cargokit { - manifestDir = "../../native" - libname = "okena_mobile_native" -} diff --git a/mobile/rust_builder/android/settings.gradle b/mobile/rust_builder/android/settings.gradle deleted file mode 100644 index b6346f0f..00000000 --- a/mobile/rust_builder/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'rust_lib_mobile' diff --git a/mobile/rust_builder/android/src/main/AndroidManifest.xml b/mobile/rust_builder/android/src/main/AndroidManifest.xml deleted file mode 100644 index a82d4cb8..00000000 --- a/mobile/rust_builder/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/mobile/rust_builder/cargokit/.gitignore b/mobile/rust_builder/cargokit/.gitignore deleted file mode 100644 index cf7bb868..00000000 --- a/mobile/rust_builder/cargokit/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -.dart_tool -*.iml -!pubspec.lock diff --git a/mobile/rust_builder/cargokit/LICENSE b/mobile/rust_builder/cargokit/LICENSE deleted file mode 100644 index d33a5fea..00000000 --- a/mobile/rust_builder/cargokit/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -Copyright 2022 Matej Knopp - -================================================================================ - -MIT LICENSE - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -================================================================================ - -APACHE LICENSE, VERSION 2.0 - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - diff --git a/mobile/rust_builder/cargokit/README b/mobile/rust_builder/cargokit/README deleted file mode 100644 index 398474db..00000000 --- a/mobile/rust_builder/cargokit/README +++ /dev/null @@ -1,11 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -Experimental repository to provide glue for seamlessly integrating cargo build -with flutter plugins and packages. - -See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ -for a tutorial on how to use Cargokit. - -Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. - diff --git a/mobile/rust_builder/cargokit/build_pod.sh b/mobile/rust_builder/cargokit/build_pod.sh deleted file mode 100755 index ed0e0d98..00000000 --- a/mobile/rust_builder/cargokit/build_pod.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/sh -set -e - -BASEDIR=$(dirname "$0") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -BASEDIR=$(cd "$BASEDIR" ; pwd -P) - -# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project -NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` - -export PATH=${NEW_PATH%?} # remove trailing : - -env - -# Platform name (macosx, iphoneos, iphonesimulator) -export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME - -# Arctive architectures (arm64, armv7, x86_64), space separated. -export CARGOKIT_DARWIN_ARCHS=$ARCHS - -# Current build configuration (Debug, Release) -export CARGOKIT_CONFIGURATION=$CONFIGURATION - -# Path to directory containing Cargo.toml. -export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 - -# Temporary directory for build artifacts. -export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR - -# Output directory for final artifacts. -export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME - -# Directory to store built tool artifacts. -export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool - -# Directory inside root project. Not necessarily the top level directory of root project. -export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT - -FLUTTER_EXPORT_BUILD_ENVIRONMENT=( - "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS - "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS -) - -for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" -do - if [[ -f "$path" ]]; then - source "$path" - fi -done - -sh "$BASEDIR/run_build_tool.sh" build-pod "$@" - -# Make a symlink from built framework to phony file, which will be used as input to -# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate -# attribute on custom build phase) -ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" -ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/mobile/rust_builder/cargokit/build_tool/README.md b/mobile/rust_builder/cargokit/build_tool/README.md deleted file mode 100644 index a878c279..00000000 --- a/mobile/rust_builder/cargokit/build_tool/README.md +++ /dev/null @@ -1,5 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -A sample command-line application with an entrypoint in `bin/`, library code -in `lib/`, and example unit test in `test/`. diff --git a/mobile/rust_builder/cargokit/build_tool/analysis_options.yaml b/mobile/rust_builder/cargokit/build_tool/analysis_options.yaml deleted file mode 100644 index 0e16a8b0..00000000 --- a/mobile/rust_builder/cargokit/build_tool/analysis_options.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# This is copied from Cargokit (which is the official way to use it currently) -# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -linter: - rules: - - prefer_relative_imports - - directives_ordering - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart b/mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart deleted file mode 100644 index 268eb524..00000000 --- a/mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'package:build_tool/build_tool.dart' as build_tool; - -void main(List arguments) { - build_tool.runMain(arguments); -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart b/mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart deleted file mode 100644 index 7c1bb750..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'src/build_tool.dart' as build_tool; - -Future runMain(List args) async { - return build_tool.runMain(args); -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart deleted file mode 100644 index 15fc9eed..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart +++ /dev/null @@ -1,195 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; -import 'package:version/version.dart'; - -import 'target.dart'; -import 'util.dart'; - -class AndroidEnvironment { - AndroidEnvironment({ - required this.sdkPath, - required this.ndkVersion, - required this.minSdkVersion, - required this.targetTempDir, - required this.target, - }); - - static void clangLinkerWrapper(List args) { - final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; - if (clang == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); - } - final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; - if (target == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); - } - - runCommand(clang, [ - target, - ...args, - ]); - } - - /// Full path to Android SDK. - final String sdkPath; - - /// Full version of Android NDK. - final String ndkVersion; - - /// Minimum supported SDK version. - final int minSdkVersion; - - /// Target directory for build artifacts. - final String targetTempDir; - - /// Target being built. - final Target target; - - bool ndkIsInstalled() { - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); - return ndkPackageXml.existsSync(); - } - - void installNdk({ - required String javaHome, - }) { - final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; - final sdkManager = path.join( - sdkPath, - 'cmdline-tools', - 'latest', - 'bin', - 'sdkmanager$sdkManagerExtension', - ); - - log.info('Installing NDK $ndkVersion'); - runCommand(sdkManager, [ - '--install', - 'ndk;$ndkVersion', - ], environment: { - 'JAVA_HOME': javaHome, - }); - } - - Future> buildEnvironment() async { - final hostArch = Platform.isMacOS - ? "darwin-x86_64" - : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); - - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final toolchainPath = path.join( - ndkPath, - 'toolchains', - 'llvm', - 'prebuilt', - hostArch, - 'bin', - ); - - final minSdkVersion = - math.max(target.androidMinSdkVersion!, this.minSdkVersion); - - final exe = Platform.isWindows ? '.exe' : ''; - - final arKey = 'AR_${target.rust}'; - final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] - .map((e) => path.join(toolchainPath, e)) - .firstWhereOrNull((element) => File(element).existsSync()); - if (arValue == null) { - throw Exception('Failed to find ar for $target in $toolchainPath'); - } - - final targetArg = '--target=${target.rust}$minSdkVersion'; - - final ccKey = 'CC_${target.rust}'; - final ccValue = path.join(toolchainPath, 'clang$exe'); - final cfFlagsKey = 'CFLAGS_${target.rust}'; - final cFlagsValue = targetArg; - - final cxxKey = 'CXX_${target.rust}'; - final cxxValue = path.join(toolchainPath, 'clang++$exe'); - final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; - final cxxFlagsValue = targetArg; - - final linkerKey = - 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); - - final ranlibKey = 'RANLIB_${target.rust}'; - final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); - - final ndkVersionParsed = Version.parse(ndkVersion); - final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; - final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); - - final runRustTool = - Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; - - final packagePath = (await Isolate.resolvePackageUri( - Uri.parse('package:build_tool/buildtool.dart')))! - .toFilePath(); - final selfPath = path.canonicalize(path.join( - packagePath, - '..', - '..', - '..', - runRustTool, - )); - - // Make sure that run_build_tool is working properly even initially launched directly - // through dart run. - final toolTempDir = - Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; - - return { - arKey: arValue, - ccKey: ccValue, - cfFlagsKey: cFlagsValue, - cxxKey: cxxValue, - cxxFlagsKey: cxxFlagsValue, - ranlibKey: ranlibValue, - rustFlagsKey: rustFlagsValue, - linkerKey: selfPath, - // Recognized by main() so we know when we're acting as a wrapper - '_CARGOKIT_NDK_LINK_TARGET': targetArg, - '_CARGOKIT_NDK_LINK_CLANG': ccValue, - 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, - }; - } - - // Workaround for libgcc missing in NDK23, inspired by cargo-ndk - String _libGccWorkaround(String buildDir, Version ndkVersion) { - final workaroundDir = path.join( - buildDir, - 'cargokit', - 'libgcc_workaround', - '${ndkVersion.major}', - ); - Directory(workaroundDir).createSync(recursive: true); - if (ndkVersion.major >= 23) { - File(path.join(workaroundDir, 'libgcc.a')) - .writeAsStringSync('INPUT(-lunwind)'); - } else { - // Other way around, untested, forward libgcc.a from libunwind once Rust - // gets updated for NDK23+. - File(path.join(workaroundDir, 'libunwind.a')) - .writeAsStringSync('INPUT(-lgcc)'); - } - - var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; - if (rustFlags.isNotEmpty) { - rustFlags = '$rustFlags\x1f'; - } - rustFlags = '$rustFlags-L\x1f$workaroundDir'; - return rustFlags; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart deleted file mode 100644 index e608cece..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart +++ /dev/null @@ -1,266 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'builder.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'rustup.dart'; -import 'target.dart'; - -class Artifact { - /// File system location of the artifact. - final String path; - - /// Actual file name that the artifact should have in destination folder. - final String finalFileName; - - AritifactType get type { - if (finalFileName.endsWith('.dll') || - finalFileName.endsWith('.dll.lib') || - finalFileName.endsWith('.pdb') || - finalFileName.endsWith('.so') || - finalFileName.endsWith('.dylib')) { - return AritifactType.dylib; - } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { - return AritifactType.staticlib; - } else { - throw Exception('Unknown artifact type for $finalFileName'); - } - } - - Artifact({ - required this.path, - required this.finalFileName, - }); -} - -final _log = Logger('artifacts_provider'); - -class ArtifactProvider { - ArtifactProvider({ - required this.environment, - required this.userOptions, - }); - - final BuildEnvironment environment; - final CargokitUserOptions userOptions; - - Future>> getArtifacts(List targets) async { - final result = await _getPrecompiledArtifacts(targets); - - final pendingTargets = List.of(targets); - pendingTargets.removeWhere((element) => result.containsKey(element)); - - if (pendingTargets.isEmpty) { - return result; - } - - final rustup = Rustup(); - for (final target in targets) { - final builder = RustBuilder(target: target, environment: environment); - builder.prepare(rustup); - _log.info('Building ${environment.crateInfo.packageName} for $target'); - final targetDir = await builder.build(); - // For local build accept both static and dynamic libraries. - final artifactNames = { - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.dylib, - remote: false, - ), - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.staticlib, - remote: false, - ) - }; - final artifacts = artifactNames - .map((artifactName) => Artifact( - path: path.join(targetDir, artifactName), - finalFileName: artifactName, - )) - .where((element) => File(element.path).existsSync()) - .toList(); - result[target] = artifacts; - } - return result; - } - - Future>> _getPrecompiledArtifacts( - List targets) async { - if (userOptions.usePrecompiledBinaries == false) { - _log.info('Precompiled binaries are disabled'); - return {}; - } - if (environment.crateOptions.precompiledBinaries == null) { - _log.fine('Precompiled binaries not enabled for this crate'); - return {}; - } - - final start = Stopwatch()..start(); - final crateHash = CrateHash.compute(environment.manifestDir, - tempStorage: environment.targetTempDir); - _log.fine( - 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); - - final downloadedArtifactsDir = - path.join(environment.targetTempDir, 'precompiled', crateHash); - Directory(downloadedArtifactsDir).createSync(recursive: true); - - final res = >{}; - - for (final target in targets) { - final requiredArtifacts = getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - remote: true, - ); - final artifactsForTarget = []; - - for (final artifact in requiredArtifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final downloadedPath = path.join(downloadedArtifactsDir, fileName); - if (!File(downloadedPath).existsSync()) { - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - await _tryDownloadArtifacts( - crateHash: crateHash, - fileName: fileName, - signatureFileName: signatureFileName, - finalPath: downloadedPath, - ); - } - if (File(downloadedPath).existsSync()) { - artifactsForTarget.add(Artifact( - path: downloadedPath, - finalFileName: artifact, - )); - } else { - break; - } - } - - // Only provide complete set of artifacts. - if (artifactsForTarget.length == requiredArtifacts.length) { - _log.fine('Found precompiled artifacts for $target'); - res[target] = artifactsForTarget; - } - } - - return res; - } - - static Future _get(Uri url, {Map? headers}) async { - int attempt = 0; - const maxAttempts = 10; - while (true) { - try { - return await get(url, headers: headers); - } on SocketException catch (e) { - // Try to detect reset by peer error and retry. - if (attempt++ < maxAttempts && - (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { - _log.severe( - 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); - await Future.delayed(Duration(seconds: 1)); - continue; - } else { - rethrow; - } - } - } - } - - Future _tryDownloadArtifacts({ - required String crateHash, - required String fileName, - required String signatureFileName, - required String finalPath, - }) async { - final precompiledBinaries = environment.crateOptions.precompiledBinaries!; - final prefix = precompiledBinaries.uriPrefix; - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); - _log.fine('Downloading signature from $signatureUrl'); - final signature = await _get(signatureUrl); - if (signature.statusCode == 404) { - _log.warning( - 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); - return; - } - if (signature.statusCode != 200) { - _log.severe( - 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); - return; - } - _log.fine('Downloading binary from $url'); - final res = await _get(url); - if (res.statusCode != 200) { - _log.severe('Failed to download binary $url: status ${res.statusCode}'); - return; - } - if (verify( - precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { - File(finalPath).writeAsBytesSync(res.bodyBytes); - } else { - _log.shout('Signature verification failed! Ignoring binary.'); - } - } -} - -enum AritifactType { - staticlib, - dylib, -} - -AritifactType artifactTypeForTarget(Target target) { - if (target.darwinPlatform != null) { - return AritifactType.staticlib; - } else { - return AritifactType.dylib; - } -} - -List getArtifactNames({ - required Target target, - required String libraryName, - required bool remote, - AritifactType? aritifactType, -}) { - aritifactType ??= artifactTypeForTarget(target); - if (target.darwinArch != null) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.dylib']; - } - } else if (target.rust.contains('-windows-')) { - if (aritifactType == AritifactType.staticlib) { - return ['$libraryName.lib']; - } else { - return [ - '$libraryName.dll', - '$libraryName.dll.lib', - if (!remote) '$libraryName.pdb' - ]; - } - } else if (target.rust.contains('-linux-')) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.so']; - } - } else { - throw Exception("Unsupported target: ${target.rust}"); - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart deleted file mode 100644 index 6f3b2a4e..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart +++ /dev/null @@ -1,40 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -class BuildCMake { - final CargokitUserOptions userOptions; - - BuildCMake({required this.userOptions}); - - Future build() async { - final targetPlatform = Environment.targetPlatform; - final target = Target.forFlutterName(Environment.targetPlatform); - if (target == null) { - throw Exception("Unknown target platform: $targetPlatform"); - } - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts([target]); - - final libs = artifacts[target]!; - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path) - .copySync(path.join(Environment.outputDir, lib.finalFileName)); - } - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart deleted file mode 100644 index 7e61fcbb..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart +++ /dev/null @@ -1,49 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -final log = Logger('build_gradle'); - -class BuildGradle { - BuildGradle({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.targetPlatforms.map((arch) { - final target = Target.forFlutterName(arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: true); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - for (final target in targets) { - final libs = artifacts[target]!; - final outputDir = path.join(Environment.outputDir, target.android!); - Directory(outputDir).createSync(recursive: true); - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); - } - } - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart deleted file mode 100644 index 8a9c0db5..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart +++ /dev/null @@ -1,89 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; -import 'util.dart'; - -class BuildPod { - BuildPod({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.darwinArchs.map((arch) { - final target = Target.forDarwin( - platformName: Environment.darwinPlatformName, darwinAarch: arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - void performLipo(String targetFile, Iterable sourceFiles) { - runCommand("lipo", [ - '-create', - ...sourceFiles, - '-output', - targetFile, - ]); - } - - final outputDir = Environment.outputDir; - - Directory(outputDir).createSync(recursive: true); - - final staticLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.staticlib) - .toList(); - final dynamicLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.dylib) - .toList(); - - final libName = environment.crateInfo.packageName; - - // If there is static lib, use it and link it with pod - if (staticLibs.isNotEmpty) { - final finalTargetFile = path.join(outputDir, "lib$libName.a"); - performLipo(finalTargetFile, staticLibs.map((e) => e.path)); - } else { - // Otherwise try to replace bundle dylib with our dylib - final bundlePaths = [ - '$libName.framework/Versions/A/$libName', - '$libName.framework/$libName', - ]; - - for (final bundlePath in bundlePaths) { - final targetFile = path.join(outputDir, bundlePath); - if (File(targetFile).existsSync()) { - performLipo(targetFile, dynamicLibs.map((e) => e.path)); - - // Replace absolute id with @rpath one so that it works properly - // when moved to Frameworks. - runCommand("install_name_tool", [ - '-id', - '@rpath/$bundlePath', - targetFile, - ]); - return; - } - } - throw Exception('Unable to find bundle for dynamic library'); - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart deleted file mode 100644 index c8f36981..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart +++ /dev/null @@ -1,271 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; - -import 'android_environment.dart'; -import 'build_cmake.dart'; -import 'build_gradle.dart'; -import 'build_pod.dart'; -import 'logging.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; -import 'util.dart'; -import 'verify_binaries.dart'; - -final log = Logger('build_tool'); - -abstract class BuildCommand extends Command { - Future runBuildCommand(CargokitUserOptions options); - - @override - Future run() async { - final options = CargokitUserOptions.load(); - - if (options.verboseLogging || - Platform.environment['CARGOKIT_VERBOSE'] == '1') { - enableVerboseLogging(); - } - - await runBuildCommand(options); - } -} - -class BuildPodCommand extends BuildCommand { - @override - final name = 'build-pod'; - - @override - final description = 'Build cocoa pod library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildPod(userOptions: options); - await build.build(); - } -} - -class BuildGradleCommand extends BuildCommand { - @override - final name = 'build-gradle'; - - @override - final description = 'Build android library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildGradle(userOptions: options); - await build.build(); - } -} - -class BuildCMakeCommand extends BuildCommand { - @override - final name = 'build-cmake'; - - @override - final description = 'Build CMake library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildCMake(userOptions: options); - await build.build(); - } -} - -class GenKeyCommand extends Command { - @override - final name = 'gen-key'; - - @override - final description = 'Generate key pair for signing precompiled binaries'; - - @override - void run() { - final kp = generateKey(); - final private = HEX.encode(kp.privateKey.bytes); - final public = HEX.encode(kp.publicKey.bytes); - print("Private Key: $private"); - print("Public Key: $public"); - } -} - -class PrecompileBinariesCommand extends Command { - PrecompileBinariesCommand() { - argParser - ..addOption( - 'repository', - mandatory: true, - help: 'Github repository slug in format owner/name', - ) - ..addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ) - ..addMultiOption('target', - help: 'Rust target triple of artifact to build.\n' - 'Can be specified multiple times or omitted in which case\n' - 'all targets for current platform will be built.') - ..addOption( - 'android-sdk-location', - help: 'Location of Android SDK (if available)', - ) - ..addOption( - 'android-ndk-version', - help: 'Android NDK version (if available)', - ) - ..addOption( - 'android-min-sdk-version', - help: 'Android minimum rquired version (if available)', - ) - ..addOption( - 'temp-dir', - help: 'Directory to store temporary build artifacts', - ) - ..addFlag( - "verbose", - abbr: "v", - defaultsTo: false, - help: "Enable verbose logging", - ); - } - - @override - final name = 'precompile-binaries'; - - @override - final description = 'Prebuild and upload binaries\n' - 'Private key must be passed through PRIVATE_KEY environment variable. ' - 'Use gen_key through generate priave key.\n' - 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; - - @override - Future run() async { - final verbose = argResults!['verbose'] as bool; - if (verbose) { - enableVerboseLogging(); - } - - final privateKeyString = Platform.environment['PRIVATE_KEY']; - if (privateKeyString == null) { - throw ArgumentError('Missing PRIVATE_KEY environment variable'); - } - final githubToken = Platform.environment['GITHUB_TOKEN']; - if (githubToken == null) { - throw ArgumentError('Missing GITHUB_TOKEN environment variable'); - } - final privateKey = HEX.decode(privateKeyString); - if (privateKey.length != 64) { - throw ArgumentError('Private key must be 64 bytes long'); - } - final manifestDir = argResults!['manifest-dir'] as String; - if (!Directory(manifestDir).existsSync()) { - throw ArgumentError('Manifest directory does not exist: $manifestDir'); - } - String? androidMinSdkVersionString = - argResults!['android-min-sdk-version'] as String?; - int? androidMinSdkVersion; - if (androidMinSdkVersionString != null) { - androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); - if (androidMinSdkVersion == null) { - throw ArgumentError( - 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); - } - } - final targetStrigns = argResults!['target'] as List; - final targets = targetStrigns.map((target) { - final res = Target.forRustTriple(target); - if (res == null) { - throw ArgumentError('Invalid target: $target'); - } - return res; - }).toList(growable: false); - final precompileBinaries = PrecompileBinaries( - privateKey: PrivateKey(privateKey), - githubToken: githubToken, - manifestDir: manifestDir, - repositorySlug: RepositorySlug.full(argResults!['repository'] as String), - targets: targets, - androidSdkLocation: argResults!['android-sdk-location'] as String?, - androidNdkVersion: argResults!['android-ndk-version'] as String?, - androidMinSdkVersion: androidMinSdkVersion, - tempDir: argResults!['temp-dir'] as String?, - ); - - await precompileBinaries.run(); - } -} - -class VerifyBinariesCommand extends Command { - VerifyBinariesCommand() { - argParser.addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ); - } - - @override - final name = "verify-binaries"; - - @override - final description = 'Verifies published binaries\n' - 'Checks whether there is a binary published for each targets\n' - 'and checks the signature.'; - - @override - Future run() async { - final manifestDir = argResults!['manifest-dir'] as String; - final verifyBinaries = VerifyBinaries( - manifestDir: manifestDir, - ); - await verifyBinaries.run(); - } -} - -Future runMain(List args) async { - try { - // Init logging before options are loaded - initLogging(); - - if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { - return AndroidEnvironment.clangLinkerWrapper(args); - } - - final runner = CommandRunner('build_tool', 'Cargokit built_tool') - ..addCommand(BuildPodCommand()) - ..addCommand(BuildGradleCommand()) - ..addCommand(BuildCMakeCommand()) - ..addCommand(GenKeyCommand()) - ..addCommand(PrecompileBinariesCommand()) - ..addCommand(VerifyBinariesCommand()); - - await runner.run(args); - } on ArgumentError catch (e) { - stderr.writeln(e.toString()); - exit(1); - } catch (e, s) { - log.severe(kDoubleSeparator); - log.severe('Cargokit BuildTool failed with error:'); - log.severe(kSeparator); - log.severe(e); - // This tells user to install Rust, there's no need to pollute the log with - // stack trace. - if (e is! RustupNotFoundException) { - log.severe(kSeparator); - log.severe(s); - log.severe(kSeparator); - log.severe('BuildTool arguments: $args'); - } - log.severe(kDoubleSeparator); - exit(1); - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart deleted file mode 100644 index 84c46e4f..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart +++ /dev/null @@ -1,198 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'android_environment.dart'; -import 'cargo.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; -import 'util.dart'; - -final _log = Logger('builder'); - -enum BuildConfiguration { - debug, - release, - profile, -} - -extension on BuildConfiguration { - bool get isDebug => this == BuildConfiguration.debug; - String get rustName => switch (this) { - BuildConfiguration.debug => 'debug', - BuildConfiguration.release => 'release', - BuildConfiguration.profile => 'release', - }; -} - -class BuildException implements Exception { - final String message; - - BuildException(this.message); - - @override - String toString() { - return 'BuildException: $message'; - } -} - -class BuildEnvironment { - final BuildConfiguration configuration; - final CargokitCrateOptions crateOptions; - final String targetTempDir; - final String manifestDir; - final CrateInfo crateInfo; - - final bool isAndroid; - final String? androidSdkPath; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? javaHome; - - BuildEnvironment({ - required this.configuration, - required this.crateOptions, - required this.targetTempDir, - required this.manifestDir, - required this.crateInfo, - required this.isAndroid, - this.androidSdkPath, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.javaHome, - }); - - static BuildConfiguration parseBuildConfiguration(String value) { - // XCode configuration adds the flavor to configuration name. - final firstSegment = value.split('-').first; - final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( - (e) => e.name == firstSegment, - ); - if (buildConfiguration == null) { - _log.warning('Unknown build configuraiton $value, will assume release'); - return BuildConfiguration.release; - } - return buildConfiguration; - } - - static BuildEnvironment fromEnvironment({ - required bool isAndroid, - }) { - final buildConfiguration = - parseBuildConfiguration(Environment.configuration); - final manifestDir = Environment.manifestDir; - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - final crateInfo = CrateInfo.load(manifestDir); - return BuildEnvironment( - configuration: buildConfiguration, - crateOptions: crateOptions, - targetTempDir: Environment.targetTempDir, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: isAndroid, - androidSdkPath: isAndroid ? Environment.sdkPath : null, - androidNdkVersion: isAndroid ? Environment.ndkVersion : null, - androidMinSdkVersion: - isAndroid ? int.parse(Environment.minSdkVersion) : null, - javaHome: isAndroid ? Environment.javaHome : null, - ); - } -} - -class RustBuilder { - final Target target; - final BuildEnvironment environment; - - RustBuilder({ - required this.target, - required this.environment, - }); - - void prepare( - Rustup rustup, - ) { - final toolchain = _toolchain; - if (rustup.installedTargets(toolchain) == null) { - rustup.installToolchain(toolchain); - } - if (toolchain == 'nightly') { - rustup.installRustSrcForNightly(); - } - if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { - rustup.installTarget(target.rust, toolchain: toolchain); - } - } - - CargoBuildOptions? get _buildOptions => - environment.crateOptions.cargo[environment.configuration]; - - String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; - - /// Returns the path of directory containing build artifacts. - Future build() async { - final extraArgs = _buildOptions?.flags ?? []; - final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); - runCommand( - 'rustup', - [ - 'run', - _toolchain, - 'cargo', - 'build', - ...extraArgs, - '--manifest-path', - manifestPath, - '-p', - environment.crateInfo.packageName, - if (!environment.configuration.isDebug) '--release', - '--target', - target.rust, - '--target-dir', - environment.targetTempDir, - ], - environment: await _buildEnvironment(), - ); - return path.join( - environment.targetTempDir, - target.rust, - environment.configuration.rustName, - ); - } - - Future> _buildEnvironment() async { - if (target.android == null) { - return {}; - } else { - final sdkPath = environment.androidSdkPath; - final ndkVersion = environment.androidNdkVersion; - final minSdkVersion = environment.androidMinSdkVersion; - if (sdkPath == null) { - throw BuildException('androidSdkPath is not set'); - } - if (ndkVersion == null) { - throw BuildException('androidNdkVersion is not set'); - } - if (minSdkVersion == null) { - throw BuildException('androidMinSdkVersion is not set'); - } - final env = AndroidEnvironment( - sdkPath: sdkPath, - ndkVersion: ndkVersion, - minSdkVersion: minSdkVersion, - targetTempDir: environment.targetTempDir, - target: target, - ); - if (!env.ndkIsInstalled() && environment.javaHome != null) { - env.installNdk(javaHome: environment.javaHome!); - } - return env.buildEnvironment(); - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart deleted file mode 100644 index 0d8958ff..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart +++ /dev/null @@ -1,48 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:toml/toml.dart'; - -class ManifestException { - ManifestException(this.message, {required this.fileName}); - - final String? fileName; - final String message; - - @override - String toString() { - if (fileName != null) { - return 'Failed to parse package manifest at $fileName: $message'; - } else { - return 'Failed to parse package manifest: $message'; - } - } -} - -class CrateInfo { - CrateInfo({required this.packageName}); - - final String packageName; - - static CrateInfo parseManifest(String manifest, {final String? fileName}) { - final toml = TomlDocument.parse(manifest); - final package = toml.toMap()['package']; - if (package == null) { - throw ManifestException('Missing package section', fileName: fileName); - } - final name = package['name']; - if (name == null) { - throw ManifestException('Missing package name', fileName: fileName); - } - return CrateInfo(packageName: name); - } - - static CrateInfo load(String manifestDir) { - final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); - final manifest = manifestFile.readAsStringSync(); - return parseManifest(manifest, fileName: manifestFile.path); - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart deleted file mode 100644 index 0c4d88d1..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart +++ /dev/null @@ -1,124 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; -import 'package:path/path.dart' as path; - -class CrateHash { - /// Computes a hash uniquely identifying crate content. This takes into account - /// content all all .rs files inside the src directory, as well as Cargo.toml, - /// Cargo.lock, build.rs and cargokit.yaml. - /// - /// If [tempStorage] is provided, computed hash is stored in a file in that directory - /// and reused on subsequent calls if the crate content hasn't changed. - static String compute(String manifestDir, {String? tempStorage}) { - return CrateHash._( - manifestDir: manifestDir, - tempStorage: tempStorage, - )._compute(); - } - - CrateHash._({ - required this.manifestDir, - required this.tempStorage, - }); - - String _compute() { - final files = getFiles(); - final tempStorage = this.tempStorage; - if (tempStorage != null) { - final quickHash = _computeQuickHash(files); - final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); - quickHashFolder.createSync(recursive: true); - final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); - if (quickHashFile.existsSync()) { - return quickHashFile.readAsStringSync(); - } - final hash = _computeHash(files); - quickHashFile.writeAsStringSync(hash); - return hash; - } else { - return _computeHash(files); - } - } - - /// Computes a quick hash based on files stat (without reading contents). This - /// is used to cache the real hash, which is slower to compute since it involves - /// reading every single file. - String _computeQuickHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - final data = ByteData(8); - for (final file in files) { - input.add(utf8.encode(file.path)); - final stat = file.statSync(); - data.setUint64(0, stat.size); - input.add(data.buffer.asUint8List()); - data.setUint64(0, stat.modified.millisecondsSinceEpoch); - input.add(data.buffer.asUint8List()); - } - - input.close(); - return base64Url.encode(output.events.single.bytes); - } - - String _computeHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - void addTextFile(File file) { - // text Files are hashed by lines in case we're dealing with github checkout - // that auto-converts line endings. - final splitter = LineSplitter(); - if (file.existsSync()) { - final data = file.readAsStringSync(); - final lines = splitter.convert(data); - for (final line in lines) { - input.add(utf8.encode(line)); - } - } - } - - for (final file in files) { - addTextFile(file); - } - - input.close(); - final res = output.events.single; - - // Truncate to 128bits. - final hash = res.bytes.sublist(0, 16); - return hex.encode(hash); - } - - List getFiles() { - final src = Directory(path.join(manifestDir, 'src')); - final files = src - .listSync(recursive: true, followLinks: false) - .whereType() - .toList(); - files.sortBy((element) => element.path); - void addFile(String relative) { - final file = File(path.join(manifestDir, relative)); - if (file.existsSync()) { - files.add(file); - } - } - - addFile('Cargo.toml'); - addFile('Cargo.lock'); - addFile('build.rs'); - addFile('cargokit.yaml'); - return files; - } - - final String manifestDir; - final String? tempStorage; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart deleted file mode 100644 index 996483a1..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart +++ /dev/null @@ -1,68 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -extension on String { - String resolveSymlink() => File(this).resolveSymbolicLinksSync(); -} - -class Environment { - /// Current build configuration (debug or release). - static String get configuration => - _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); - - static bool get isDebug => configuration == 'debug'; - static bool get isRelease => configuration == 'release'; - - /// Temporary directory where Rust build artifacts are placed. - static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); - - /// Final output directory where the build artifacts are placed. - static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); - - /// Path to the crate manifest (containing Cargo.toml). - static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); - - /// Directory inside root project. Not necessarily root folder. Symlinks are - /// not resolved on purpose. - static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); - - // Pod - - /// Platform name (macosx, iphoneos, iphonesimulator). - static String get darwinPlatformName => - _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); - - /// List of architectures to build for (arm64, armv7, x86_64). - static List get darwinArchs => - _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); - - // Gradle - static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); - static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); - static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); - static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); - static List get targetPlatforms => - _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); - - // CMAKE - static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); - - static String _getEnv(String key) { - final res = Platform.environment[key]; - if (res == null) { - throw Exception("Missing environment variable $key"); - } - return res; - } - - static String _getEnvPath(String key) { - final res = _getEnv(key); - if (Directory(res).existsSync()) { - return res.resolveSymlink(); - } else { - return res; - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart deleted file mode 100644 index 5edd4fd1..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart +++ /dev/null @@ -1,52 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:logging/logging.dart'; - -const String kSeparator = "--"; -const String kDoubleSeparator = "=="; - -bool _lastMessageWasSeparator = false; - -void _log(LogRecord rec) { - final prefix = '${rec.level.name}: '; - final out = rec.level == Level.SEVERE ? stderr : stdout; - if (rec.message == kSeparator) { - if (!_lastMessageWasSeparator) { - out.write(prefix); - out.writeln('-' * 80); - _lastMessageWasSeparator = true; - } - return; - } else if (rec.message == kDoubleSeparator) { - out.write(prefix); - out.writeln('=' * 80); - _lastMessageWasSeparator = true; - return; - } - out.write(prefix); - out.writeln(rec.message); - _lastMessageWasSeparator = false; -} - -void initLogging() { - Logger.root.level = Level.INFO; - Logger.root.onRecord.listen((LogRecord rec) { - final lines = rec.message.split('\n'); - for (final line in lines) { - if (line.isNotEmpty || lines.length == 1 || line != lines.last) { - _log(LogRecord( - rec.level, - line, - rec.loggerName, - )); - } - } - }); -} - -void enableVerboseLogging() { - Logger.root.level = Level.ALL; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/options.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/options.dart deleted file mode 100644 index 22aef1d3..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/options.dart +++ /dev/null @@ -1,309 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:source_span/source_span.dart'; -import 'package:yaml/yaml.dart'; - -import 'builder.dart'; -import 'environment.dart'; -import 'rustup.dart'; - -final _log = Logger('options'); - -/// A class for exceptions that have source span information attached. -class SourceSpanException implements Exception { - // This is a getter so that subclasses can override it. - /// A message describing the exception. - String get message => _message; - final String _message; - - // This is a getter so that subclasses can override it. - /// The span associated with this exception. - /// - /// This may be `null` if the source location can't be determined. - SourceSpan? get span => _span; - final SourceSpan? _span; - - SourceSpanException(this._message, this._span); - - /// Returns a string representation of `this`. - /// - /// [color] may either be a [String], a [bool], or `null`. If it's a string, - /// it indicates an ANSI terminal color escape that should be used to - /// highlight the span's text. If it's `true`, it indicates that the text - /// should be highlighted using the default color. If it's `false` or `null`, - /// it indicates that the text shouldn't be highlighted. - @override - String toString({Object? color}) { - if (span == null) return message; - return 'Error on ${span!.message(message, color: color)}'; - } -} - -enum Toolchain { - stable, - beta, - nightly, -} - -class CargoBuildOptions { - final Toolchain toolchain; - final List flags; - - CargoBuildOptions({ - required this.toolchain, - required this.flags, - }); - - static Toolchain _toolchainFromNode(YamlNode node) { - if (node case YamlScalar(value: String name)) { - final toolchain = - Toolchain.values.firstWhereOrNull((element) => element.name == name); - if (toolchain != null) { - return toolchain; - } - } - throw SourceSpanException( - 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', - node.span); - } - - static CargoBuildOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - Toolchain toolchain = Toolchain.stable; - List flags = []; - for (final MapEntry(:key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: 'toolchain')) { - toolchain = _toolchainFromNode(value); - } else if (key case YamlScalar(value: 'extra_flags')) { - if (value case YamlList(nodes: List list)) { - if (list.every((element) { - if (element case YamlScalar(value: String _)) { - return true; - } - return false; - })) { - flags = list.map((e) => e.value as String).toList(); - continue; - } - } - throw SourceSpanException( - 'Extra flags must be a list of strings', value.span); - } else { - throw SourceSpanException( - 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', - key.span); - } - } - return CargoBuildOptions(toolchain: toolchain, flags: flags); - } -} - -extension on YamlMap { - /// Map that extracts keys so that we can do map case check on them. - Map get valueMap => - nodes.map((key, value) => MapEntry(key.value, value)); -} - -class PrecompiledBinaries { - final String uriPrefix; - final PublicKey publicKey; - - PrecompiledBinaries({ - required this.uriPrefix, - required this.publicKey, - }); - - static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { - final bytes = HEX.decode(key); - if (bytes.length != 32) { - throw SourceSpanException( - 'Invalid public key. Must be 32 bytes long.', span); - } - return PublicKey(bytes); - } - - static PrecompiledBinaries parse(YamlNode node) { - if (node case YamlMap(valueMap: Map map)) { - if (map - case { - 'url_prefix': YamlNode urlPrefixNode, - 'public_key': YamlNode publicKeyNode, - }) { - final urlPrefix = switch (urlPrefixNode) { - YamlScalar(value: String urlPrefix) => urlPrefix, - _ => throw SourceSpanException( - 'Invalid URL prefix value.', urlPrefixNode.span), - }; - final publicKey = switch (publicKeyNode) { - YamlScalar(value: String publicKey) => - _publicKeyFromHex(publicKey, publicKeyNode.span), - _ => throw SourceSpanException( - 'Invalid public key value.', publicKeyNode.span), - }; - return PrecompiledBinaries( - uriPrefix: urlPrefix, - publicKey: publicKey, - ); - } - } - throw SourceSpanException( - 'Invalid precompiled binaries value. ' - 'Expected Map with "url_prefix" and "public_key".', - node.span); - } -} - -/// Cargokit options specified for Rust crate. -class CargokitCrateOptions { - CargokitCrateOptions({ - this.cargo = const {}, - this.precompiledBinaries, - }); - - final Map cargo; - final PrecompiledBinaries? precompiledBinaries; - - static CargokitCrateOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - final options = {}; - PrecompiledBinaries? precompiledBinaries; - - for (final entry in node.nodes.entries) { - if (entry - case MapEntry( - key: YamlScalar(value: 'cargo'), - value: YamlNode node, - )) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: String name)) { - final configuration = BuildConfiguration.values - .firstWhereOrNull((element) => element.name == name); - if (configuration != null) { - options[configuration] = CargoBuildOptions.parse(value); - continue; - } - } - throw SourceSpanException( - 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', - key.span); - } - } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { - precompiledBinaries = PrecompiledBinaries.parse(entry.value); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', - entry.key.span); - } - } - return CargokitCrateOptions( - cargo: options, - precompiledBinaries: precompiledBinaries, - ); - } - - static CargokitCrateOptions load({ - required String manifestDir, - }) { - final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); - final file = File.fromUri(uri); - if (file.existsSync()) { - final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); - return parse(contents); - } else { - return CargokitCrateOptions(); - } - } -} - -class CargokitUserOptions { - // When Rustup is installed always build locally unless user opts into - // using precompiled binaries. - static bool defaultUsePrecompiledBinaries() { - return Rustup.executablePath() == null; - } - - CargokitUserOptions({ - required this.usePrecompiledBinaries, - required this.verboseLogging, - }); - - CargokitUserOptions._() - : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), - verboseLogging = false; - - static CargokitUserOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); - bool verboseLogging = false; - - for (final entry in node.nodes.entries) { - if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { - if (entry.value case YamlScalar(value: bool value)) { - usePrecompiledBinaries = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "use_precompiled_binaries". Must be a boolean.', - entry.value.span); - } else if (entry.key case YamlScalar(value: 'verbose_logging')) { - if (entry.value case YamlScalar(value: bool value)) { - verboseLogging = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "verbose_logging". Must be a boolean.', - entry.value.span); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', - entry.key.span); - } - } - return CargokitUserOptions( - usePrecompiledBinaries: usePrecompiledBinaries, - verboseLogging: verboseLogging, - ); - } - - static CargokitUserOptions load() { - String fileName = "cargokit_options.yaml"; - var userProjectDir = Directory(Environment.rootProjectDir); - - while (userProjectDir.parent.path != userProjectDir.path) { - final configFile = File(path.join(userProjectDir.path, fileName)); - if (configFile.existsSync()) { - final contents = loadYamlNode( - configFile.readAsStringSync(), - sourceUrl: configFile.uri, - ); - final res = parse(contents); - if (res.verboseLogging) { - _log.info('Found user options file at ${configFile.path}'); - } - return res; - } - userProjectDir = userProjectDir.parent; - } - return CargokitUserOptions._(); - } - - final bool usePrecompiledBinaries; - final bool verboseLogging; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart deleted file mode 100644 index c27f4195..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart +++ /dev/null @@ -1,202 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; - -final _log = Logger('precompile_binaries'); - -class PrecompileBinaries { - PrecompileBinaries({ - required this.privateKey, - required this.githubToken, - required this.repositorySlug, - required this.manifestDir, - required this.targets, - this.androidSdkLocation, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.tempDir, - }); - - final PrivateKey privateKey; - final String githubToken; - final RepositorySlug repositorySlug; - final String manifestDir; - final List targets; - final String? androidSdkLocation; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? tempDir; - - static String fileName(Target target, String name) { - return '${target.rust}_$name'; - } - - static String signatureFileName(Target target, String name) { - return '${target.rust}_$name.sig'; - } - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final targets = List.of(this.targets); - if (targets.isEmpty) { - targets.addAll([ - ...Target.buildableTargets(), - if (androidSdkLocation != null) ...Target.androidTargets(), - ]); - } - - _log.info('Precompiling binaries for $targets'); - - final hash = CrateHash.compute(manifestDir); - _log.info('Computed crate hash: $hash'); - - final String tagName = 'precompiled_$hash'; - - final github = GitHub(auth: Authentication.withToken(githubToken)); - final repo = github.repositories; - final release = await _getOrCreateRelease( - repo: repo, - tagName: tagName, - packageName: crateInfo.packageName, - hash: hash, - ); - - final tempDir = this.tempDir != null - ? Directory(this.tempDir!) - : Directory.systemTemp.createTempSync('precompiled_'); - - tempDir.createSync(recursive: true); - - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - - final buildEnvironment = BuildEnvironment( - configuration: BuildConfiguration.release, - crateOptions: crateOptions, - targetTempDir: tempDir.path, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: androidSdkLocation != null, - androidSdkPath: androidSdkLocation, - androidNdkVersion: androidNdkVersion, - androidMinSdkVersion: androidMinSdkVersion, - ); - - final rustup = Rustup(); - - for (final target in targets) { - final artifactNames = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - if (artifactNames.every((name) { - final fileName = PrecompileBinaries.fileName(target, name); - return (release.assets ?? []).any((e) => e.name == fileName); - })) { - _log.info("All artifacts for $target already exist - skipping"); - continue; - } - - _log.info('Building for $target'); - - final builder = - RustBuilder(target: target, environment: buildEnvironment); - builder.prepare(rustup); - final res = await builder.build(); - - final assets = []; - for (final name in artifactNames) { - final file = File(path.join(res, name)); - if (!file.existsSync()) { - throw Exception('Missing artifact: ${file.path}'); - } - - final data = file.readAsBytesSync(); - final create = CreateReleaseAsset( - name: PrecompileBinaries.fileName(target, name), - contentType: "application/octet-stream", - assetData: data, - ); - final signature = sign(privateKey, data); - final signatureCreate = CreateReleaseAsset( - name: signatureFileName(target, name), - contentType: "application/octet-stream", - assetData: signature, - ); - bool verified = verify(public(privateKey), data, signature); - if (!verified) { - throw Exception('Signature verification failed'); - } - assets.add(create); - assets.add(signatureCreate); - } - _log.info('Uploading assets: ${assets.map((e) => e.name)}'); - for (final asset in assets) { - // This seems to be failing on CI so do it one by one - int retryCount = 0; - while (true) { - try { - await repo.uploadReleaseAssets(release, [asset]); - break; - } on Exception catch (e) { - if (retryCount == 10) { - rethrow; - } - ++retryCount; - _log.shout( - 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); - await Future.delayed(Duration(seconds: 2)); - } - } - } - } - - _log.info('Cleaning up'); - tempDir.deleteSync(recursive: true); - } - - Future _getOrCreateRelease({ - required RepositoriesService repo, - required String tagName, - required String packageName, - required String hash, - }) async { - Release release; - try { - _log.info('Fetching release $tagName'); - release = await repo.getReleaseByTagName(repositorySlug, tagName); - } on ReleaseNotFound { - _log.info('Release not found - creating release $tagName'); - release = await repo.createRelease( - repositorySlug, - CreateRelease.from( - tagName: tagName, - name: 'Precompiled binaries ${hash.substring(0, 8)}', - targetCommitish: null, - isDraft: false, - isPrerelease: false, - body: 'Precompiled binaries for crate $packageName, ' - 'crate hash $hash.', - )); - } - return release; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart deleted file mode 100644 index 0ac8d086..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart +++ /dev/null @@ -1,136 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; - -import 'util.dart'; - -class _Toolchain { - _Toolchain( - this.name, - this.targets, - ); - - final String name; - final List targets; -} - -class Rustup { - List? installedTargets(String toolchain) { - final targets = _installedTargets(toolchain); - return targets != null ? List.unmodifiable(targets) : null; - } - - void installToolchain(String toolchain) { - log.info("Installing Rust toolchain: $toolchain"); - runCommand("rustup", ['toolchain', 'install', toolchain]); - _installedToolchains - .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); - } - - void installTarget( - String target, { - required String toolchain, - }) { - log.info("Installing Rust target: $target"); - runCommand("rustup", [ - 'target', - 'add', - '--toolchain', - toolchain, - target, - ]); - _installedTargets(toolchain)?.add(target); - } - - final List<_Toolchain> _installedToolchains; - - Rustup() : _installedToolchains = _getInstalledToolchains(); - - List? _installedTargets(String toolchain) => _installedToolchains - .firstWhereOrNull( - (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) - ?.targets; - - static List<_Toolchain> _getInstalledToolchains() { - String extractToolchainName(String line) { - // ignore (default) after toolchain name - final parts = line.split(' '); - return parts[0]; - } - - final res = runCommand("rustup", ['toolchain', 'list']); - - // To list all non-custom toolchains, we need to filter out lines that - // don't start with "stable", "beta", or "nightly". - Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) - .map(extractToolchainName) - .toList(growable: true); - - return lines - .map( - (name) => _Toolchain( - name, - _getInstalledTargets(name), - ), - ) - .toList(growable: true); - } - - static List _getInstalledTargets(String toolchain) { - final res = runCommand("rustup", [ - 'target', - 'list', - '--toolchain', - toolchain, - '--installed', - ]); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty) - .toList(growable: true); - return lines; - } - - bool _didInstallRustSrcForNightly = false; - - void installRustSrcForNightly() { - if (_didInstallRustSrcForNightly) { - return; - } - // Useful for -Z build-std - runCommand( - "rustup", - ['component', 'add', 'rust-src', '--toolchain', 'nightly'], - ); - _didInstallRustSrcForNightly = true; - } - - static String? executablePath() { - final envPath = Platform.environment['PATH']; - final envPathSeparator = Platform.isWindows ? ';' : ':'; - final home = Platform.isWindows - ? Platform.environment['USERPROFILE'] - : Platform.environment['HOME']; - final paths = [ - if (home != null) path.join(home, '.cargo', 'bin'), - if (envPath != null) ...envPath.split(envPathSeparator), - ]; - for (final p in paths) { - final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; - final rustupPath = path.join(p, rustup); - if (File(rustupPath).existsSync()) { - return rustupPath; - } - } - return null; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/target.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/target.dart deleted file mode 100644 index 6fbc58b6..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/target.dart +++ /dev/null @@ -1,140 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:collection/collection.dart'; - -import 'util.dart'; - -class Target { - Target({ - required this.rust, - this.flutter, - this.android, - this.androidMinSdkVersion, - this.darwinPlatform, - this.darwinArch, - }); - - static final all = [ - Target( - rust: 'armv7-linux-androideabi', - flutter: 'android-arm', - android: 'armeabi-v7a', - androidMinSdkVersion: 16, - ), - Target( - rust: 'aarch64-linux-android', - flutter: 'android-arm64', - android: 'arm64-v8a', - androidMinSdkVersion: 21, - ), - Target( - rust: 'i686-linux-android', - flutter: 'android-x86', - android: 'x86', - androidMinSdkVersion: 16, - ), - Target( - rust: 'x86_64-linux-android', - flutter: 'android-x64', - android: 'x86_64', - androidMinSdkVersion: 21, - ), - Target( - rust: 'x86_64-pc-windows-msvc', - flutter: 'windows-x64', - ), - Target( - rust: 'x86_64-unknown-linux-gnu', - flutter: 'linux-x64', - ), - Target( - rust: 'aarch64-unknown-linux-gnu', - flutter: 'linux-arm64', - ), - Target( - rust: 'x86_64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'x86_64', - ), - Target( - rust: 'aarch64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios', - darwinPlatform: 'iphoneos', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios-sim', - darwinPlatform: 'iphonesimulator', - darwinArch: 'arm64', - ), - Target( - rust: 'x86_64-apple-ios', - darwinPlatform: 'iphonesimulator', - darwinArch: 'x86_64', - ), - ]; - - static Target? forFlutterName(String flutterName) { - return all.firstWhereOrNull((element) => element.flutter == flutterName); - } - - static Target? forDarwin({ - required String platformName, - required String darwinAarch, - }) { - return all.firstWhereOrNull((element) => // - element.darwinPlatform == platformName && - element.darwinArch == darwinAarch); - } - - static Target? forRustTriple(String triple) { - return all.firstWhereOrNull((element) => element.rust == triple); - } - - static List androidTargets() { - return all - .where((element) => element.android != null) - .toList(growable: false); - } - - /// Returns buildable targets on current host platform ignoring Android targets. - static List buildableTargets() { - if (Platform.isLinux) { - // Right now we don't support cross-compiling on Linux. So we just return - // the host target. - final arch = runCommand('arch', []).stdout as String; - if (arch.trim() == 'aarch64') { - return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; - } else { - return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; - } - } - return all.where((target) { - if (Platform.isWindows) { - return target.rust.contains('-windows-'); - } else if (Platform.isMacOS) { - return target.darwinPlatform != null; - } - return false; - }).toList(growable: false); - } - - @override - String toString() { - return rust; - } - - final String? flutter; - final String rust; - final String? android; - final int? androidMinSdkVersion; - final String? darwinPlatform; - final String? darwinArch; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/util.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/util.dart deleted file mode 100644 index 8bb6a872..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/util.dart +++ /dev/null @@ -1,172 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'logging.dart'; -import 'rustup.dart'; - -final log = Logger("process"); - -class CommandFailedException implements Exception { - final String executable; - final List arguments; - final ProcessResult result; - - CommandFailedException({ - required this.executable, - required this.arguments, - required this.result, - }); - - @override - String toString() { - final stdout = result.stdout.toString().trim(); - final stderr = result.stderr.toString().trim(); - return [ - "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", - "Returned Exit Code: ${result.exitCode}", - kSeparator, - "STDOUT:", - if (stdout.isNotEmpty) stdout, - kSeparator, - "STDERR:", - if (stderr.isNotEmpty) stderr, - ].join('\n'); - } -} - -class TestRunCommandArgs { - final String executable; - final List arguments; - final String? workingDirectory; - final Map? environment; - final bool includeParentEnvironment; - final bool runInShell; - final Encoding? stdoutEncoding; - final Encoding? stderrEncoding; - - TestRunCommandArgs({ - required this.executable, - required this.arguments, - this.workingDirectory, - this.environment, - this.includeParentEnvironment = true, - this.runInShell = false, - this.stdoutEncoding, - this.stderrEncoding, - }); -} - -class TestRunCommandResult { - TestRunCommandResult({ - this.pid = 1, - this.exitCode = 0, - this.stdout = '', - this.stderr = '', - }); - - final int pid; - final int exitCode; - final String stdout; - final String stderr; -} - -TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; - -ProcessResult runCommand( - String executable, - List arguments, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - Encoding? stdoutEncoding = systemEncoding, - Encoding? stderrEncoding = systemEncoding, -}) { - if (testRunCommandOverride != null) { - final result = testRunCommandOverride!(TestRunCommandArgs( - executable: executable, - arguments: arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding, - )); - return ProcessResult( - result.pid, - result.exitCode, - result.stdout, - result.stderr, - ); - } - log.finer('Running command $executable ${arguments.join(' ')}'); - final res = Process.runSync( - _resolveExecutable(executable), - arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stderrEncoding: stderrEncoding, - stdoutEncoding: stdoutEncoding, - ); - if (res.exitCode != 0) { - throw CommandFailedException( - executable: executable, - arguments: arguments, - result: res, - ); - } else { - return res; - } -} - -class RustupNotFoundException implements Exception { - @override - String toString() { - return [ - ' ', - 'rustup not found in PATH.', - ' ', - 'Maybe you need to install Rust? It only takes a minute:', - ' ', - if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', - if (hasHomebrewRustInPath()) ...[ - '\$ brew unlink rust # Unlink homebrew Rust from PATH', - ], - if (!Platform.isWindows) - "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", - ' ', - ].join('\n'); - } - - static bool hasHomebrewRustInPath() { - if (!Platform.isMacOS) { - return false; - } - final envPath = Platform.environment['PATH'] ?? ''; - final paths = envPath.split(':'); - return paths.any((p) { - return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); - }); - } -} - -String _resolveExecutable(String executable) { - if (executable == 'rustup') { - final resolved = Rustup.executablePath(); - if (resolved != null) { - return resolved; - } - throw RustupNotFoundException(); - } else { - return executable; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart deleted file mode 100644 index 2366b57b..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart +++ /dev/null @@ -1,84 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; - -import 'artifacts_provider.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; - -class VerifyBinaries { - VerifyBinaries({ - required this.manifestDir, - }); - - final String manifestDir; - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final config = CargokitCrateOptions.load(manifestDir: manifestDir); - final precompiledBinaries = config.precompiledBinaries; - if (precompiledBinaries == null) { - stdout.writeln('Crate does not support precompiled binaries.'); - } else { - final crateHash = CrateHash.compute(manifestDir); - stdout.writeln('Crate hash: $crateHash'); - - for (final target in Target.all) { - final message = 'Checking ${target.rust}...'; - stdout.write(message.padRight(40)); - stdout.flush(); - - final artifacts = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - final prefix = precompiledBinaries.uriPrefix; - - bool ok = true; - - for (final artifact in artifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = - Uri.parse('$prefix$crateHash/$signatureFileName'); - - final signature = await get(signatureUrl); - if (signature.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - final asset = await get(url); - if (asset.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - - if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, - signature.bodyBytes)) { - stdout.writeln('INVALID SIGNATURE'); - ok = false; - } - } - - if (ok) { - stdout.writeln('OK'); - } - } - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/pubspec.lock b/mobile/rust_builder/cargokit/build_tool/pubspec.lock deleted file mode 100644 index 343bdd36..00000000 --- a/mobile/rust_builder/cargokit/build_tool/pubspec.lock +++ /dev/null @@ -1,453 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 - url: "https://pub.dev" - source: hosted - version: "64.0.0" - adaptive_number: - dependency: transitive - description: - name: adaptive_number - sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - args: - dependency: "direct main" - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: "direct main" - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" - url: "https://pub.dev" - source: hosted - version: "1.6.3" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - ed25519_edwards: - dependency: "direct main" - description: - name: ed25519_edwards - sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - file: - dependency: transitive - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - github: - dependency: "direct main" - description: - name: github - sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" - url: "https://pub.dev" - source: hosted - version: "9.17.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - hex: - dependency: "direct main" - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - http: - dependency: "direct main" - description: - name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - lints: - dependency: "direct dev" - description: - name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - logging: - dependency: "direct main" - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - meta: - dependency: transitive - description: - name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: "direct main" - description: - name: path - sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 - url: "https://pub.dev" - source: hosted - version: "5.4.0" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" - source_span: - dependency: "direct main" - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" - url: "https://pub.dev" - source: hosted - version: "1.24.6" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - test_core: - dependency: transitive - description: - name: test_core - sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" - url: "https://pub.dev" - source: hosted - version: "0.5.6" - toml: - dependency: "direct main" - description: - name: toml - sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" - url: "https://pub.dev" - source: hosted - version: "0.14.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - version: - dependency: "direct main" - description: - name: version - sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" - url: "https://pub.dev" - source: hosted - version: "11.9.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.0.0 <4.0.0" diff --git a/mobile/rust_builder/cargokit/build_tool/pubspec.yaml b/mobile/rust_builder/cargokit/build_tool/pubspec.yaml deleted file mode 100644 index 18c61e33..00000000 --- a/mobile/rust_builder/cargokit/build_tool/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# This is copied from Cargokit (which is the official way to use it currently) -# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -name: build_tool -description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. -publish_to: none -version: 1.0.0 - -environment: - sdk: ">=3.0.0 <4.0.0" - -# Add regular dependencies here. -dependencies: - # these are pinned on purpose because the bundle_tool_runner doesn't have - # pubspec.lock. See run_build_tool.sh - logging: 1.2.0 - path: 1.8.0 - version: 3.0.0 - collection: 1.18.0 - ed25519_edwards: 0.3.1 - hex: 0.2.0 - yaml: 3.1.2 - source_span: 1.10.0 - github: 9.17.0 - args: 2.4.2 - crypto: 3.0.3 - convert: 3.1.1 - http: 1.1.0 - toml: 0.14.0 - -dev_dependencies: - lints: ^2.1.0 - test: ^1.24.0 diff --git a/mobile/rust_builder/cargokit/cmake/cargokit.cmake b/mobile/rust_builder/cargokit/cmake/cargokit.cmake deleted file mode 100644 index ddd05df9..00000000 --- a/mobile/rust_builder/cargokit/cmake/cargokit.cmake +++ /dev/null @@ -1,99 +0,0 @@ -SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) - -if(WIN32) - # REALPATH does not properly resolve symlinks on windows :-/ - execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) -endif() - -# Arguments -# - target: CMAKE target to which rust library is linked -# - manifest_dir: relative path from current folder to directory containing cargo manifest -# - lib_name: cargo package name -# - any_symbol_name: name of any exported symbol from the library. -# used on windows to force linking with library. -function(apply_cargokit target manifest_dir lib_name any_symbol_name) - - set(CARGOKIT_LIB_NAME "${lib_name}") - set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") - if (CMAKE_CONFIGURATION_TYPES) - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") - else() - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") - endif() - set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") - - if (FLUTTER_TARGET_PLATFORM) - set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") - else() - set(CARGOKIT_TARGET_PLATFORM "windows-x64") - endif() - - set(CARGOKIT_ENV - "CARGOKIT_CMAKE=${CMAKE_COMMAND}" - "CARGOKIT_CONFIGURATION=$" - "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" - "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" - "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" - "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" - "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" - "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" - ) - - if (WIN32) - set(SCRIPT_EXTENSION ".cmd") - set(IMPORT_LIB_EXTENSION ".lib") - else() - set(SCRIPT_EXTENSION ".sh") - set(IMPORT_LIB_EXTENSION "") - execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") - endif() - - # Using generators in custom command is only supported in CMake 3.20+ - if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") - foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) - add_custom_command( - OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endforeach() - else() - add_custom_command( - OUTPUT - ${OUTPUT_LIB} - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endif() - - - set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) - - if (TARGET ${target}) - # If we have actual cmake target provided create target and make existing - # target depend on it - add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) - add_dependencies("${target}" "${target}_cargokit") - target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") - if(WIN32) - target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") - endif() - else() - # Otherwise (FFI) just use ALL to force building always - add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) - endif() - - # Allow adding the output library to plugin bundled libraries - set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) - -endfunction() diff --git a/mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 deleted file mode 100644 index 2ac593a1..00000000 --- a/mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -function Resolve-Symlinks { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [string] $Path - ) - - [string] $separator = '/' - [string[]] $parts = $Path.Split($separator) - - [string] $realPath = '' - foreach ($part in $parts) { - if ($realPath -and !$realPath.EndsWith($separator)) { - $realPath += $separator - } - - $realPath += $part.Replace('\', '/') - - # The slash is important when using Get-Item on Drive letters in pwsh. - if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) { - $realPath += '/' - } - - $item = Get-Item $realPath - if ($item.LinkTarget) { - $realPath = $item.LinkTarget.Replace('\', '/') - } - } - $realPath -} - -$path = Resolve-Symlinks -Path $args[0] -Write-Host $path diff --git a/mobile/rust_builder/cargokit/gradle/plugin.gradle b/mobile/rust_builder/cargokit/gradle/plugin.gradle deleted file mode 100644 index 4af35ee0..00000000 --- a/mobile/rust_builder/cargokit/gradle/plugin.gradle +++ /dev/null @@ -1,179 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import java.nio.file.Paths -import org.apache.tools.ant.taskdefs.condition.Os - -CargoKitPlugin.file = buildscript.sourceFile - -apply plugin: CargoKitPlugin - -class CargoKitExtension { - String manifestDir; // Relative path to folder containing Cargo.toml - String libname; // Library name within Cargo.toml. Must be a cdylib -} - -abstract class CargoKitBuildTask extends DefaultTask { - - @Input - String buildMode - - @Input - String buildDir - - @Input - String outputDir - - @Input - String ndkVersion - - @Input - String sdkDirectory - - @Input - int compileSdkVersion; - - @Input - int minSdkVersion; - - @Input - String pluginFile - - @Input - List targetPlatforms - - @TaskAction - def build() { - if (project.cargokit.manifestDir == null) { - throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); - } - - if (project.cargokit.libname == null) { - throw new GradleException("Property 'libname' must be set on cargokit extension"); - } - - def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" - def path = Paths.get(new File(pluginFile).parent, "..", executableName); - - def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) - - def rootProjectDir = project.rootProject.projectDir - - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - project.exec { - commandLine 'chmod', '+x', path - } - } - - project.exec { - executable path - args "build-gradle" - environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir - environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" - environment "CARGOKIT_MANIFEST_DIR", manifestDir - environment "CARGOKIT_CONFIGURATION", buildMode - environment "CARGOKIT_TARGET_TEMP_DIR", buildDir - environment "CARGOKIT_OUTPUT_DIR", outputDir - environment "CARGOKIT_NDK_VERSION", ndkVersion - environment "CARGOKIT_SDK_DIR", sdkDirectory - environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion - environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion - environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") - environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] - } - } -} - -class CargoKitPlugin implements Plugin { - - static String file; - - private Plugin findFlutterPlugin(Project rootProject) { - _findFlutterPlugin(rootProject.childProjects) - } - - private Plugin _findFlutterPlugin(Map projects) { - for (project in projects) { - for (plugin in project.value.getPlugins()) { - if (plugin.class.name == "com.flutter.gradle.FlutterPlugin") { - return plugin; - } - } - def plugin = _findFlutterPlugin(project.value.childProjects); - if (plugin != null) { - return plugin; - } - } - return null; - } - - @Override - void apply(Project project) { - def plugin = findFlutterPlugin(project.rootProject); - - project.extensions.create("cargokit", CargoKitExtension) - - if (plugin == null) { - print("Flutter plugin not found, CargoKit plugin will not be applied.") - return; - } - - def cargoBuildDir = "${project.buildDir}/build" - - // Determine if the project is an application or library - def isApplication = plugin.project.plugins.hasPlugin('com.android.application') - def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants - - variants.all { variant -> - - final buildType = variant.buildType.name - - def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; - def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; - jniLibs.srcDir(new File(cargoOutputDir)) - - def platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() - - // Same thing addFlutterDependencies does in flutter.gradle - if (buildType == "debug") { - platforms.add("android-x86") - platforms.add("android-x64") - } - - // The task name depends on plugin properties, which are not available - // at this point - project.getGradle().afterProject { - def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; - - if (project.tasks.findByName(taskName)) { - return - } - - if (plugin.project.android.ndkVersion == null) { - throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") - } - - def task = project.tasks.create(taskName, CargoKitBuildTask.class) { - buildMode = variant.buildType.name - buildDir = cargoBuildDir - outputDir = cargoOutputDir - ndkVersion = plugin.project.android.ndkVersion - sdkDirectory = plugin.project.android.sdkDirectory - minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int - compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int - targetPlatforms = platforms - pluginFile = CargoKitPlugin.file - } - def onTask = { newTask -> - if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { - newTask.dependsOn task - // Fix gradle 7.4.2 not picking up JNI library changes - newTask.outputs.upToDateWhen { false } - } - } - project.tasks.each onTask - project.tasks.whenTaskAdded onTask - } - } - } -} diff --git a/mobile/rust_builder/cargokit/run_build_tool.cmd b/mobile/rust_builder/cargokit/run_build_tool.cmd deleted file mode 100755 index c45d0aa8..00000000 --- a/mobile/rust_builder/cargokit/run_build_tool.cmd +++ /dev/null @@ -1,91 +0,0 @@ -@echo off -setlocal - -setlocal ENABLEDELAYEDEXPANSION - -SET BASEDIR=%~dp0 - -if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( - mkdir "%CARGOKIT_TOOL_TEMP_DIR%" -) -cd /D "%CARGOKIT_TOOL_TEMP_DIR%" - -SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool -SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart - -set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% - -( - echo name: build_tool_runner - echo version: 1.0.0 - echo publish_to: none - echo. - echo environment: - echo sdk: '^>=3.0.0 ^<4.0.0' - echo. - echo dependencies: - echo build_tool: - echo path: %BUILD_TOOL_PKG_DIR_POSIX% -) >pubspec.yaml - -if not exist bin ( - mkdir bin -) - -( - echo import 'package:build_tool/build_tool.dart' as build_tool; - echo void main^(List^ args^) ^{ - echo build_tool.runMain^(args^); - echo ^} -) >bin\build_tool_runner.dart - -SET PRECOMPILED=bin\build_tool_runner.dill - -REM To detect changes in package we compare output of DIR /s (recursive) -set PREV_PACKAGE_INFO=.dart_tool\package_info.prev -set CUR_PACKAGE_INFO=.dart_tool\package_info.cur - -DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" - -REM Last line in dir output is free space on harddrive. That is bound to -REM change between invocation so we need to remove it -( - Set "Line=" - For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( - SetLocal EnableDelayedExpansion - If Defined Line Echo !Line! - EndLocal - Set "Line=%%A") -) >"%CUR_PACKAGE_INFO%" -DEL "%CUR_PACKAGE_INFO%_orig" - -REM Compare current directory listing with previous -FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 - -If %ERRORLEVEL% neq 0 ( - REM Changed - copy current to previous and remove precompiled kernel - if exist "%PREV_PACKAGE_INFO%" ( - DEL "%PREV_PACKAGE_INFO%" - ) - MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" - if exist "%PRECOMPILED%" ( - DEL "%PRECOMPILED%" - ) -) - -REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% -REM which means we need to do pub get and precompile -if not exist "%PRECOMPILED%" ( - echo Running pub get in "%cd%" - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart -) - -"%DART%" "%PRECOMPILED%" %* - -REM 253 means invalid snapshot version. -If %ERRORLEVEL% equ 253 ( - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart - "%DART%" "%PRECOMPILED%" %* -) diff --git a/mobile/rust_builder/cargokit/run_build_tool.sh b/mobile/rust_builder/cargokit/run_build_tool.sh deleted file mode 100755 index 069c1585..00000000 --- a/mobile/rust_builder/cargokit/run_build_tool.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Ensure Cargo/Rust toolchain is in PATH -if [ -d "$HOME/.cargo/bin" ]; then - export PATH="$HOME/.cargo/bin:$PATH" -fi - -BASEDIR=$(dirname "$0") - -mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" - -cd "$CARGOKIT_TOOL_TEMP_DIR" - -# Write a very simple bin package in temp folder that depends on build_tool package -# from Cargokit. This is done to ensure that we don't pollute Cargokit folder -# with .dart_tool contents. - -BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" - -if [[ -z $FLUTTER_ROOT ]]; then # not defined - DART=dart -else - DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" -fi - -cat << EOF > "pubspec.yaml" -name: build_tool_runner -version: 1.0.0 -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - build_tool: - path: "$BUILD_TOOL_PKG_DIR" -EOF - -mkdir -p "bin" - -cat << EOF > "bin/build_tool_runner.dart" -import 'package:build_tool/build_tool.dart' as build_tool; -void main(List args) { - build_tool.runMain(args); -} -EOF - -# Create alias for `shasum` if it does not exist and `sha1sum` exists -if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then - shopt -s expand_aliases - alias shasum="sha1sum" -fi - -# Dart run will not cache any package that has a path dependency, which -# is the case for our build_tool_runner. So instead we precompile the package -# ourselves. -# To invalidate the cached kernel we use the hash of ls -LR of the build_tool -# package directory. This should be good enough, as the build_tool package -# itself is not meant to have any path dependencies. - -if [[ "$OSTYPE" == "darwin"* ]]; then - PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) -else - PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) -fi - -PACKAGE_HASH_FILE=".package_hash" - -if [ -f "$PACKAGE_HASH_FILE" ]; then - EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") - if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then - rm "$PACKAGE_HASH_FILE" - fi -fi - -# Run pub get if needed. -if [ ! -f "$PACKAGE_HASH_FILE" ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" -fi - -# Rebuild the tool if it was deleted by Android Studio -if [ ! -f "bin/build_tool_runner.dill" ]; then - "$DART" compile kernel bin/build_tool_runner.dart -fi - -set +e - -"$DART" bin/build_tool_runner.dill "$@" - -exit_code=$? - -# 253 means invalid snapshot version. -if [ $exit_code == 253 ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - "$DART" bin/build_tool_runner.dill "$@" - exit_code=$? -fi - -exit $exit_code diff --git a/mobile/rust_builder/ios/Classes/dummy_file.c b/mobile/rust_builder/ios/Classes/dummy_file.c deleted file mode 100644 index e06dab99..00000000 --- a/mobile/rust_builder/ios/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/mobile/rust_builder/ios/rust_lib_mobile.podspec b/mobile/rust_builder/ios/rust_lib_mobile.podspec deleted file mode 100644 index e241d00c..00000000 --- a/mobile/rust_builder/ios/rust_lib_mobile.podspec +++ /dev/null @@ -1,45 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint rust_lib_mobile.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'rust_lib_mobile' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '11.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' - - s.script_phase = { - :name => 'Build Rust library', - # First argument is relative path to the `rust` folder, second is name of rust library - :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../native okena_mobile_native', - :execution_position => :before_compile, - :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], - # Let XCode know that the static library referenced in -force_load below is - # created by this build step. - :output_files => ["${BUILT_PRODUCTS_DIR}/libokena_mobile_native.a"], - } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Flutter.framework does not contain a i386 slice. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libokena_mobile_native.a', - } -end \ No newline at end of file diff --git a/mobile/rust_builder/linux/CMakeLists.txt b/mobile/rust_builder/linux/CMakeLists.txt deleted file mode 100644 index 7ca21fd5..00000000 --- a/mobile/rust_builder/linux/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# The Flutter tooling requires that developers have CMake 3.10 or later -# installed. You should not increase this version, as doing so will cause -# the plugin to fail to compile for some customers of the plugin. -cmake_minimum_required(VERSION 3.10) - -# Project-level configuration. -set(PROJECT_NAME "rust_lib_mobile") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../native okena_mobile_native "") - -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(rust_lib_mobile_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/mobile/rust_builder/macos/Classes/dummy_file.c b/mobile/rust_builder/macos/Classes/dummy_file.c deleted file mode 100644 index e06dab99..00000000 --- a/mobile/rust_builder/macos/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/mobile/rust_builder/macos/rust_lib_mobile.podspec b/mobile/rust_builder/macos/rust_lib_mobile.podspec deleted file mode 100644 index 24df209a..00000000 --- a/mobile/rust_builder/macos/rust_lib_mobile.podspec +++ /dev/null @@ -1,44 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint rust_lib_mobile.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'rust_lib_mobile' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' - - s.script_phase = { - :name => 'Build Rust library', - # First argument is relative path to the `rust` folder, second is name of rust library - :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../native okena_mobile_native', - :execution_position => :before_compile, - :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], - # Let XCode know that the static library referenced in -force_load below is - # created by this build step. - :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_mobile.a"], - } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Flutter.framework does not contain a i386 slice. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_mobile.a', - } -end \ No newline at end of file diff --git a/mobile/rust_builder/pubspec.yaml b/mobile/rust_builder/pubspec.yaml deleted file mode 100644 index f8efc186..00000000 --- a/mobile/rust_builder/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: rust_lib_mobile -description: "Utility to build Rust code" -version: 0.0.1 -publish_to: none - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - ffi: ^2.0.2 - ffigen: ^11.0.0 - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - ffiPlugin: true - ios: - ffiPlugin: true - linux: - ffiPlugin: true - macos: - ffiPlugin: true - windows: - ffiPlugin: true diff --git a/mobile/rust_builder/windows/.gitignore b/mobile/rust_builder/windows/.gitignore deleted file mode 100644 index b3eb2be1..00000000 --- a/mobile/rust_builder/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/mobile/rust_builder/windows/CMakeLists.txt b/mobile/rust_builder/windows/CMakeLists.txt deleted file mode 100644 index a3e26953..00000000 --- a/mobile/rust_builder/windows/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# The Flutter tooling requires that developers have a version of Visual Studio -# installed that includes CMake 3.14 or later. You should not increase this -# version, as doing so will cause the plugin to fail to compile for some -# customers of the plugin. -cmake_minimum_required(VERSION 3.14) - -# Project-level configuration. -set(PROJECT_NAME "rust_lib_mobile") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../../../../../native okena_mobile_native "") - -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(rust_lib_mobile_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/mobile/test/models/layout_node_test.dart b/mobile/test/models/layout_node_test.dart deleted file mode 100644 index 56a6fe53..00000000 --- a/mobile/test/models/layout_node_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/src/models/layout_node.dart'; - -void main() { - group('LayoutNode.fromJson', () { - test('parses terminal node', () { - final node = LayoutNode.fromJson({ - 'type': 'terminal', - 'terminal_id': 't1', - 'minimized': false, - 'detached': false, - }); - - expect(node, isA()); - final t = node as TerminalNode; - expect(t.terminalId, 't1'); - expect(t.minimized, isFalse); - expect(t.detached, isFalse); - }); - - test('parses terminal node with null id', () { - final node = LayoutNode.fromJson({ - 'type': 'terminal', - 'terminal_id': null, - 'minimized': true, - 'detached': true, - }); - - expect(node, isA()); - expect((node as TerminalNode).terminalId, isNull); - }); - - test('parses split node', () { - final node = LayoutNode.fromJson({ - 'type': 'split', - 'direction': 'horizontal', - 'sizes': [50.0, 50.0], - 'children': [ - {'type': 'terminal', 'terminal_id': 't1', 'minimized': false, 'detached': false}, - {'type': 'terminal', 'terminal_id': 't2', 'minimized': false, 'detached': false}, - ], - }); - - expect(node, isA()); - final s = node as SplitNode; - expect(s.direction, 'horizontal'); - expect(s.sizes, [50.0, 50.0]); - expect(s.children.length, 2); - expect((s.children[0] as TerminalNode).terminalId, 't1'); - expect((s.children[1] as TerminalNode).terminalId, 't2'); - }); - - test('parses tabs node', () { - final node = LayoutNode.fromJson({ - 'type': 'tabs', - 'active_tab': 1, - 'children': [ - {'type': 'terminal', 'terminal_id': 'a', 'minimized': false, 'detached': false}, - {'type': 'terminal', 'terminal_id': 'b', 'minimized': false, 'detached': false}, - ], - }); - - expect(node, isA()); - final t = node as TabsNode; - expect(t.activeTab, 1); - expect(t.children.length, 2); - }); - - test('parses nested split with tabs', () { - final node = LayoutNode.fromJson({ - 'type': 'split', - 'direction': 'vertical', - 'sizes': [30.0, 70.0], - 'children': [ - {'type': 'terminal', 'terminal_id': 't1', 'minimized': false, 'detached': false}, - { - 'type': 'tabs', - 'active_tab': 0, - 'children': [ - {'type': 'terminal', 'terminal_id': 't2', 'minimized': false, 'detached': false}, - {'type': 'terminal', 'terminal_id': 't3', 'minimized': false, 'detached': false}, - ], - }, - ], - }); - - expect(node, isA()); - final s = node as SplitNode; - expect(s.children[0], isA()); - expect(s.children[1], isA()); - final tabs = s.children[1] as TabsNode; - expect(tabs.children.length, 2); - }); - - test('unknown type falls back to empty terminal', () { - final node = LayoutNode.fromJson({ - 'type': 'unknown_future_type', - }); - - expect(node, isA()); - expect((node as TerminalNode).terminalId, isNull); - }); - - test('handles missing optional fields with defaults', () { - final node = LayoutNode.fromJson({ - 'type': 'split', - }); - - expect(node, isA()); - final s = node as SplitNode; - expect(s.direction, 'horizontal'); - expect(s.sizes, isEmpty); - expect(s.children, isEmpty); - }); - }); -} diff --git a/mobile/test/models/saved_server_test.dart b/mobile/test/models/saved_server_test.dart deleted file mode 100644 index 0f33c4b4..00000000 --- a/mobile/test/models/saved_server_test.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/src/models/saved_server.dart'; - -void main() { - group('SavedServer', () { - test('toJson/fromJson round-trip without label', () { - const server = SavedServer(host: '192.168.1.100', port: 19100); - final json = server.toJson(); - final restored = SavedServer.fromJson(json); - - expect(restored.host, '192.168.1.100'); - expect(restored.port, 19100); - expect(restored.label, isNull); - }); - - test('toJson/fromJson round-trip with label', () { - const server = - SavedServer(host: '10.0.0.1', port: 19200, label: 'Home PC'); - final json = server.toJson(); - final restored = SavedServer.fromJson(json); - - expect(restored.host, '10.0.0.1'); - expect(restored.port, 19200); - expect(restored.label, 'Home PC'); - }); - - test('label omitted from JSON when null', () { - const server = SavedServer(host: 'host', port: 1234); - final json = server.toJson(); - expect(json.containsKey('label'), isFalse); - }); - - test('listToJson/listFromJson round-trip', () { - const servers = [ - SavedServer(host: 'a.com', port: 100), - SavedServer(host: 'b.com', port: 200, label: 'B'), - ]; - final jsonStr = SavedServer.listToJson(servers); - final restored = SavedServer.listFromJson(jsonStr); - - expect(restored.length, 2); - expect(restored[0].host, 'a.com'); - expect(restored[1].label, 'B'); - }); - - test('displayName uses label when present', () { - const server = SavedServer(host: 'h', port: 1, label: 'My Server'); - expect(server.displayName, 'My Server'); - }); - - test('displayName falls back to host:port', () { - const server = SavedServer(host: '10.0.0.1', port: 19100); - expect(server.displayName, '10.0.0.1:19100'); - }); - - test('equality by host and port', () { - const a = SavedServer(host: 'x', port: 1); - const b = SavedServer(host: 'x', port: 1, label: 'different'); - const c = SavedServer(host: 'y', port: 1); - - expect(a, equals(b)); - expect(a, isNot(equals(c))); - }); - }); -} diff --git a/mobile/test/models/terminal_flags_test.dart b/mobile/test/models/terminal_flags_test.dart deleted file mode 100644 index c84feaa4..00000000 --- a/mobile/test/models/terminal_flags_test.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/painting.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/src/widgets/terminal_painter.dart'; - -void main() { - group('flagsToDecoration', () { - test('no flags returns none', () { - expect(flagsToDecoration(0), TextDecoration.none); - }); - - test('underline flag', () { - expect(flagsToDecoration(4), TextDecoration.underline); - }); - - test('strikethrough flag', () { - expect(flagsToDecoration(8), TextDecoration.lineThrough); - }); - - test('underline + strikethrough combined', () { - final deco = flagsToDecoration(4 | 8); - // TextDecoration.combine returns a combined decoration - expect(deco.contains(TextDecoration.underline), isTrue); - expect(deco.contains(TextDecoration.lineThrough), isTrue); - }); - - test('bold/italic flags do not add decoration', () { - expect(flagsToDecoration(1), TextDecoration.none); // bold - expect(flagsToDecoration(2), TextDecoration.none); // italic - expect(flagsToDecoration(3), TextDecoration.none); // bold+italic - }); - }); - - group('argbToColor', () { - test('opaque white', () { - final c = argbToColor(0xFFFFFFFF); - expect(c, const Color(0xFFFFFFFF)); - }); - - test('opaque red', () { - final c = argbToColor(0xFFFF0000); - expect(c.r, closeTo(1.0, 0.01)); - expect(c.g, closeTo(0.0, 0.01)); - expect(c.b, closeTo(0.0, 0.01)); - }); - - test('semi-transparent', () { - final c = argbToColor(0x80000000); - expect(c.a, closeTo(0.5, 0.01)); - }); - }); -} diff --git a/mobile/test/widget_test.dart b/mobile/test/widget_test.dart deleted file mode 100644 index a6b7d51e..00000000 --- a/mobile/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:mobile/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/mobile/test_driver/integration_test.dart b/mobile/test_driver/integration_test.dart deleted file mode 100644 index b38629cc..00000000 --- a/mobile/test_driver/integration_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/src/app/remote_commands.rs b/src/app/remote_commands.rs index 9f8b9da6..a7c2a515 100644 --- a/src/app/remote_commands.rs +++ b/src/app/remote_commands.rs @@ -156,6 +156,15 @@ pub(crate) async fn remote_command_loop( let git_statuses = git_status_tx.borrow().clone(); let data = ws.data(); + // Build terminal size map from the registry + let size_map: HashMap = { + let registry = terminals.lock(); + registry.iter().map(|(id, term)| { + let size = term.resize_state.lock().size; + (id.clone(), (size.cols, size.rows)) + }).collect() + }; + // Build a lookup map for projects let project_map: std::collections::HashMap<&str, &crate::workspace::state::ProjectData> = data.projects.iter().map(|p| (p.id.as_str(), p)).collect(); @@ -200,7 +209,7 @@ pub(crate) async fn remote_command_loop( name: p.name.clone(), path: p.path.clone(), show_in_overview: api_project_visibility(&p.id, hidden_project_ids), - layout: p.layout.as_ref().map(|l| l.to_api()), + layout: p.layout.as_ref().map(|l| l.to_api_with_sizes(&size_map)), terminal_names: p.terminal_names.clone(), git_status, folder_color: p.folder_color, diff --git a/src/remote/tls.rs b/src/remote/tls.rs index fa840a0b..59110064 100644 --- a/src/remote/tls.rs +++ b/src/remote/tls.rs @@ -103,7 +103,7 @@ fn atomic_write(path: &Path, bytes: &[u8], _mode: u32) -> std::io::Result<()> { } /// Build a rustls `ServerConfig` from the persisted self-signed cert + key, -/// using the aws_lc_rs provider (matches the client side). +/// using the ring provider (matches the client side). pub fn server_config(material: &TlsMaterial) -> Result> { let certs: Vec> = CertificateDer::pem_file_iter(&material.cert_path) @@ -113,7 +113,7 @@ pub fn server_config(material: &TlsMaterial) -> Result let key = PrivateKeyDer::from_pem_file(&material.key_path) .with_context(|| format!("reading private key {:?}", material.key_path))?; - let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider()); + let provider = Arc::new(rustls::crypto::ring::default_provider()); let config = rustls::ServerConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() .context("rustls default protocol versions")? @@ -212,7 +212,7 @@ mod handshake_tests { #[tokio::test] async fn dual_stack_serves_http_and_pinned_https_on_one_port() { // Server providers may consult the process-default CryptoProvider. - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let _ = rustls::crypto::ring::default_provider().install_default(); let dir = tempfile::tempdir().unwrap(); let material = load_or_generate(dir.path()).unwrap(); diff --git a/src/remote/types.rs b/src/remote/types.rs index e1976b0f..df0218e2 100644 --- a/src/remote/types.rs +++ b/src/remote/types.rs @@ -24,6 +24,8 @@ mod tests { terminal_id: Some("abc-123".into()), minimized: false, detached: false, + cols: None, + rows: None, }; let node = LayoutNode::from_api_prefixed(&api, "remote:conn1"); match node { @@ -40,6 +42,8 @@ mod tests { terminal_id: None, minimized: true, detached: false, + cols: None, + rows: None, }; let node = LayoutNode::from_api_prefixed(&api, "remote:x"); match node { @@ -65,6 +69,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -73,11 +79,15 @@ mod tests { terminal_id: Some("t2".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Terminal { terminal_id: Some("t3".into()), minimized: false, detached: true, + cols: None, + rows: None, }, ], }, @@ -94,6 +104,8 @@ mod tests { terminal_id: Some("raw-id".into()), minimized: false, detached: false, + cols: None, + rows: None, }; let node = LayoutNode::from_api(&api); match node {