From 4f77475978791b143b144b759664b9ac930b6087 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:36:18 +0100 Subject: [PATCH 01/31] feat(desktop): replace Tauri with a Rust IPC runtime --- Cargo.lock | 2855 +------------ Cargo.toml | 4 +- apps/desktop/src-backend/.gitignore | 3 + .../{src-tauri => src-backend}/Cargo.toml | 41 +- .../Entitlements.plist | 0 .../assets/backgrounds/blue/1.jpg | Bin .../assets/backgrounds/blue/2.jpg | Bin .../assets/backgrounds/blue/3.jpg | Bin .../assets/backgrounds/blue/4.jpg | Bin .../assets/backgrounds/blue/5.jpg | Bin .../assets/backgrounds/blue/6.jpg | Bin .../assets/backgrounds/cities/liverpool.jpg | Bin .../assets/backgrounds/cities/london.jpg | Bin .../assets/backgrounds/cities/miami.jpg | Bin .../assets/backgrounds/cities/monaco.jpg | Bin .../assets/backgrounds/cities/nyc.jpg | Bin .../assets/backgrounds/cities/rome.jpg | Bin .../assets/backgrounds/cities/santorini.jpg | Bin .../assets/backgrounds/cities/sf.jpg | Bin .../assets/backgrounds/dark/1.jpg | Bin .../assets/backgrounds/dark/2.jpg | Bin .../assets/backgrounds/dark/3.jpg | Bin .../assets/backgrounds/dark/4.jpg | Bin .../assets/backgrounds/dark/5.jpg | Bin .../assets/backgrounds/dark/6.jpg | Bin .../assets/backgrounds/macOS/sequoia-dark.jpg | Bin .../backgrounds/macOS/sequoia-light.jpg | Bin .../backgrounds/macOS/sonoma-clouds.jpg | Bin .../assets/backgrounds/macOS/sonoma-dark.jpg | Bin .../backgrounds/macOS/sonoma-evening.jpg | Bin .../backgrounds/macOS/sonoma-fromabove.jpg | Bin .../backgrounds/macOS/sonoma-horizon.jpg | Bin .../assets/backgrounds/macOS/sonoma-light.jpg | Bin .../assets/backgrounds/macOS/sonoma-river.jpg | Bin .../assets/backgrounds/macOS/tahoe-dark.jpg | Bin .../backgrounds/macOS/tahoe-dawn-min.jpg | Bin .../backgrounds/macOS/tahoe-day-min.jpg | Bin .../backgrounds/macOS/tahoe-dusk-min.jpg | Bin .../assets/backgrounds/macOS/tahoe-light.jpg | Bin .../backgrounds/macOS/tahoe-night-min.jpg | Bin .../assets/backgrounds/macOS/ventura-dark.jpg | Bin .../backgrounds/macOS/ventura-semi-dark.jpg | Bin .../assets/backgrounds/macOS/ventura.jpg | Bin .../assets/backgrounds/orange/1.jpg | Bin .../assets/backgrounds/orange/2.jpg | Bin .../assets/backgrounds/orange/3.jpg | Bin .../assets/backgrounds/orange/4.jpg | Bin .../assets/backgrounds/orange/5.jpg | Bin .../assets/backgrounds/orange/6.jpg | Bin .../assets/backgrounds/orange/7.jpg | Bin .../assets/backgrounds/orange/8.jpg | Bin .../assets/backgrounds/orange/9.jpg | Bin .../assets/backgrounds/purple/1.jpg | Bin .../assets/backgrounds/purple/2.jpg | Bin .../assets/backgrounds/purple/3.jpg | Bin .../assets/backgrounds/purple/4.jpg | Bin .../assets/backgrounds/purple/5.jpg | Bin .../assets/backgrounds/purple/6.jpg | Bin .../assets/dmg-background.png | Bin .../assets/nsis-header.bmp | Bin .../assets/nsis-sidebar.bmp | Bin .../assets/wix-banner.bmp | Bin .../assets/wix-dialog.bmp | Bin .../binaries/.gitignore | 1 + .../desktop-display-transport-benchmark.rs | 0 .../icons/128x128.png | Bin .../icons/128x128@2x.png | Bin .../icons/32x32.png | Bin .../icons/Square107x107Logo.png | Bin .../icons/Square142x142Logo.png | Bin .../icons/Square150x150Logo.png | Bin .../icons/Square284x284Logo.png | Bin .../icons/Square30x30Logo.png | Bin .../icons/Square310x310Logo.png | Bin .../icons/Square44x44Logo.png | Bin .../icons/Square71x71Logo.png | Bin .../icons/Square89x89Logo.png | Bin .../icons/StoreLogo.png | Bin .../icons/android/mipmap-hdpi/ic_launcher.png | Bin .../mipmap-hdpi/ic_launcher_foreground.png | Bin .../android/mipmap-hdpi/ic_launcher_round.png | Bin .../icons/android/mipmap-mdpi/ic_launcher.png | Bin .../mipmap-mdpi/ic_launcher_foreground.png | Bin .../android/mipmap-mdpi/ic_launcher_round.png | Bin .../android/mipmap-xhdpi/ic_launcher.png | Bin .../mipmap-xhdpi/ic_launcher_foreground.png | Bin .../mipmap-xhdpi/ic_launcher_round.png | Bin .../android/mipmap-xxhdpi/ic_launcher.png | Bin .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin .../mipmap-xxhdpi/ic_launcher_round.png | Bin .../android/mipmap-xxxhdpi/ic_launcher.png | Bin .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin .../mipmap-xxxhdpi/ic_launcher_round.png | Bin .../icons/icon.icns | Bin .../{src-tauri => src-backend}/icons/icon.ico | Bin .../{src-tauri => src-backend}/icons/icon.png | Bin .../icons/ios/AppIcon-20x20@1x.png | Bin .../icons/ios/AppIcon-20x20@2x-1.png | Bin .../icons/ios/AppIcon-20x20@2x.png | Bin .../icons/ios/AppIcon-20x20@3x.png | Bin .../icons/ios/AppIcon-29x29@1x.png | Bin .../icons/ios/AppIcon-29x29@2x-1.png | Bin .../icons/ios/AppIcon-29x29@2x.png | Bin .../icons/ios/AppIcon-29x29@3x.png | Bin .../icons/ios/AppIcon-40x40@1x.png | Bin .../icons/ios/AppIcon-40x40@2x-1.png | Bin .../icons/ios/AppIcon-40x40@2x.png | Bin .../icons/ios/AppIcon-40x40@3x.png | Bin .../icons/ios/AppIcon-512@2x.png | Bin .../icons/ios/AppIcon-60x60@2x.png | Bin .../icons/ios/AppIcon-60x60@3x.png | Bin .../icons/ios/AppIcon-76x76@1x.png | Bin .../icons/ios/AppIcon-76x76@2x.png | Bin .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin .../so.cap.desktop-tray-default-symbolic.svg | 0 .../so.cap.desktop-tray-instant-symbolic.svg | 0 ...o.cap.desktop-tray-screenshot-symbolic.svg | 0 .../so.cap.desktop-tray-stop-symbolic.svg | 0 .../so.cap.desktop-tray-studio-symbolic.svg | 0 .../icons/macos/icon.icns | Bin .../icons/macos/icon.png | Bin .../icons/tray-default-icon-instant.png | Bin .../icons/tray-default-icon-screenshot.png | Bin .../icons/tray-default-icon-studio.png | Bin .../icons/tray-default-icon.png | Bin .../icons/tray-stop-icon.png | Bin .../sounds/action.ogg | Bin .../sounds/screenshot.ogg | Bin .../sounds/start-recording.ogg | Bin .../sounds/stop-recording.ogg | Bin .../{src-tauri => src-backend}/src/api.rs | 2 +- .../{src-tauri => src-backend}/src/audio.rs | 0 .../src/audio_library.rs | 8 +- .../src/audio_meter.rs | 6 +- .../{src-tauri => src-backend}/src/auth.rs | 3 +- .../src/automation.rs | 34 +- .../{src-tauri => src-backend}/src/camera.rs | 574 ++- .../src/camera.wgsl | 0 .../src/camera_legacy.rs | 0 .../src/camera_native.rs | 0 .../src/captions.rs | 40 +- .../{src-tauri => src-backend}/src/cli.rs | 6 +- .../src/clip_thumbnails.rs | 2 +- .../src/crash_sentinel.rs | 6 +- .../src/deeplink_actions.rs | 4 +- .../src/editor_window.rs | 24 +- .../src/exit_shutdown.rs | 0 .../{src-tauri => src-backend}/src/export.rs | 77 +- .../src/fake_window.rs | 54 +- apps/desktop/src-backend/src/flags.rs | 3 + .../src/frame_ws.rs | 0 .../src/general_settings.rs | 14 +- .../src/gpu_context.rs | 0 .../{src-tauri => src-backend}/src/hotkeys.rs | 164 +- .../src/http_client.rs | 0 .../{src-tauri => src-backend}/src/import.rs | 14 +- .../{src-tauri => src-backend}/src/lib.rs | 1097 +++-- .../{src-tauri => src-backend}/src/logging.rs | 2 +- .../{src-tauri => src-backend}/src/main.rs | 18 + .../src/notifications.rs | 18 +- .../src/panel_manager.rs | 41 +- .../src/permissions.rs | 54 +- .../src/platform/macos/delegates.rs | 4 +- .../src-backend/src/platform/macos/mod.rs | 20 + .../platform/macos/sc_shareable_content.rs | 0 .../src/platform/mod.rs | 4 +- .../src/platform/win.rs | 0 .../src/power_observer.rs | 6 +- .../{src-tauri => src-backend}/src/presets.rs | 3 +- .../src/recording.rs | 92 +- .../src/recording_settings.rs | 5 +- .../src/recording_telemetry.rs | 0 .../src/recordings_locations.rs | 8 +- .../src/recovery.rs | 8 +- .../src/resource.rs | 0 .../src/screenshot_editor.rs | 26 +- .../src/target_select_overlay.rs | 80 +- .../src/telemetry.rs | 4 +- .../src/thumbnails/linux.rs | 0 .../src/thumbnails/mac.rs | 0 .../src/thumbnails/mod.rs | 0 .../src/thumbnails/windows.rs | 0 apps/desktop/src-backend/src/tray.rs | 204 + .../src/update_project_names.rs | 3 +- apps/desktop/src-backend/src/updates.rs | 82 + .../{src-tauri => src-backend}/src/upload.rs | 17 +- .../{src-tauri => src-backend}/src/web_api.rs | 2 +- .../src/window_exclusion.rs | 31 +- .../src/window_position_persistence.rs | 2 +- apps/desktop/src-backend/src/windows.rs | 860 ++++ .../tests/exit_shutdown.rs | 0 .../tests/web_api_startup.rs | 18 +- apps/desktop/src-tauri/.gitignore | 7 - apps/desktop/src-tauri/Info.plist | 33 - apps/desktop/src-tauri/build.rs | 3 - .../src-tauri/capabilities/default.json | 74 - apps/desktop/src-tauri/src/flags.rs | 16 - .../src-tauri/src/platform/macos/mod.rs | 655 --- apps/desktop/src-tauri/src/tray.rs | 1125 ----- apps/desktop/src-tauri/src/updates.rs | 380 -- apps/desktop/src-tauri/src/windows.rs | 3707 ----------------- apps/desktop/src-tauri/tauri.conf.json | 124 - apps/desktop/src-tauri/tauri.prod.conf.json | 29 - crates/desktop-runtime-macros/Cargo.toml | 15 + crates/desktop-runtime-macros/src/lib.rs | 236 ++ crates/desktop-runtime/Cargo.toml | 18 + crates/desktop-runtime/src/lib.rs | 1334 ++++++ crates/desktop-runtime/src/protocol.rs | 244 ++ crates/desktop-runtime/src/transport.rs | 212 + crates/scap-targets/src/platform/macos.rs | 20 +- crates/workspace-hack/Cargo.toml | 2 - 211 files changed, 4526 insertions(+), 10356 deletions(-) create mode 100644 apps/desktop/src-backend/.gitignore rename apps/desktop/{src-tauri => src-backend}/Cargo.toml (77%) rename apps/desktop/{src-tauri => src-backend}/Entitlements.plist (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/blue/1.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/blue/2.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/blue/3.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/blue/4.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/blue/5.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/blue/6.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/liverpool.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/london.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/miami.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/monaco.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/nyc.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/rome.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/santorini.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/cities/sf.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/dark/1.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/dark/2.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/dark/3.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/dark/4.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/dark/5.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/dark/6.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sequoia-dark.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sequoia-light.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-clouds.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-dark.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-evening.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-fromabove.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-horizon.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-light.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/sonoma-river.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/tahoe-dark.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/tahoe-dawn-min.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/tahoe-day-min.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/tahoe-dusk-min.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/tahoe-light.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/tahoe-night-min.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/ventura-dark.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/ventura-semi-dark.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/macOS/ventura.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/1.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/2.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/3.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/4.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/5.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/6.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/7.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/8.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/orange/9.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/purple/1.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/purple/2.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/purple/3.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/purple/4.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/purple/5.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/backgrounds/purple/6.jpg (100%) rename apps/desktop/{src-tauri => src-backend}/assets/dmg-background.png (100%) rename apps/desktop/{src-tauri => src-backend}/assets/nsis-header.bmp (100%) rename apps/desktop/{src-tauri => src-backend}/assets/nsis-sidebar.bmp (100%) rename apps/desktop/{src-tauri => src-backend}/assets/wix-banner.bmp (100%) rename apps/desktop/{src-tauri => src-backend}/assets/wix-dialog.bmp (100%) rename apps/desktop/{src-tauri => src-backend}/binaries/.gitignore (88%) rename apps/desktop/{src-tauri => src-backend}/examples/desktop-display-transport-benchmark.rs (100%) rename apps/desktop/{src-tauri => src-backend}/icons/128x128.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/128x128@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/32x32.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square107x107Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square142x142Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square150x150Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square284x284Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square30x30Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square310x310Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square44x44Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square71x71Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/Square89x89Logo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/StoreLogo.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-hdpi/ic_launcher.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-hdpi/ic_launcher_foreground.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-hdpi/ic_launcher_round.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-mdpi/ic_launcher.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-mdpi/ic_launcher_foreground.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-mdpi/ic_launcher_round.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xhdpi/ic_launcher.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xhdpi/ic_launcher_foreground.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xhdpi/ic_launcher_round.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xxhdpi/ic_launcher.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xxhdpi/ic_launcher_round.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xxxhdpi/ic_launcher.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/android/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/icon.icns (100%) rename apps/desktop/{src-tauri => src-backend}/icons/icon.ico (100%) rename apps/desktop/{src-tauri => src-backend}/icons/icon.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-20x20@1x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-20x20@2x-1.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-20x20@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-20x20@3x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-29x29@1x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-29x29@2x-1.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-29x29@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-29x29@3x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-40x40@1x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-40x40@2x-1.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-40x40@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-40x40@3x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-512@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-60x60@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-60x60@3x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-76x76@1x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-76x76@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/ios/AppIcon-83.5x83.5@2x.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/linux/so.cap.desktop-tray-default-symbolic.svg (100%) rename apps/desktop/{src-tauri => src-backend}/icons/linux/so.cap.desktop-tray-instant-symbolic.svg (100%) rename apps/desktop/{src-tauri => src-backend}/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg (100%) rename apps/desktop/{src-tauri => src-backend}/icons/linux/so.cap.desktop-tray-stop-symbolic.svg (100%) rename apps/desktop/{src-tauri => src-backend}/icons/linux/so.cap.desktop-tray-studio-symbolic.svg (100%) rename apps/desktop/{src-tauri => src-backend}/icons/macos/icon.icns (100%) rename apps/desktop/{src-tauri => src-backend}/icons/macos/icon.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/tray-default-icon-instant.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/tray-default-icon-screenshot.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/tray-default-icon-studio.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/tray-default-icon.png (100%) rename apps/desktop/{src-tauri => src-backend}/icons/tray-stop-icon.png (100%) rename apps/desktop/{src-tauri => src-backend}/sounds/action.ogg (100%) rename apps/desktop/{src-tauri => src-backend}/sounds/screenshot.ogg (100%) rename apps/desktop/{src-tauri => src-backend}/sounds/start-recording.ogg (100%) rename apps/desktop/{src-tauri => src-backend}/sounds/stop-recording.ogg (100%) rename apps/desktop/{src-tauri => src-backend}/src/api.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/audio.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/audio_library.rs (96%) rename apps/desktop/{src-tauri => src-backend}/src/audio_meter.rs (97%) rename apps/desktop/{src-tauri => src-backend}/src/auth.rs (98%) rename apps/desktop/{src-tauri => src-backend}/src/automation.rs (97%) rename apps/desktop/{src-tauri => src-backend}/src/camera.rs (82%) rename apps/desktop/{src-tauri => src-backend}/src/camera.wgsl (100%) rename apps/desktop/{src-tauri => src-backend}/src/camera_legacy.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/camera_native.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/captions.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/cli.rs (80%) rename apps/desktop/{src-tauri => src-backend}/src/clip_thumbnails.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/crash_sentinel.rs (98%) rename apps/desktop/{src-tauri => src-backend}/src/deeplink_actions.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/editor_window.rs (95%) rename apps/desktop/{src-tauri => src-backend}/src/exit_shutdown.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/export.rs (97%) rename apps/desktop/{src-tauri => src-backend}/src/fake_window.rs (92%) create mode 100644 apps/desktop/src-backend/src/flags.rs rename apps/desktop/{src-tauri => src-backend}/src/frame_ws.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/general_settings.rs (98%) rename apps/desktop/{src-tauri => src-backend}/src/gpu_context.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/hotkeys.rs (75%) rename apps/desktop/{src-tauri => src-backend}/src/http_client.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/import.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/lib.rs (90%) rename apps/desktop/{src-tauri => src-backend}/src/logging.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/main.rs (92%) rename apps/desktop/{src-tauri => src-backend}/src/notifications.rs (91%) rename apps/desktop/{src-tauri => src-backend}/src/panel_manager.rs (82%) rename apps/desktop/{src-tauri => src-backend}/src/permissions.rs (94%) rename apps/desktop/{src-tauri => src-backend}/src/platform/macos/delegates.rs (99%) create mode 100644 apps/desktop/src-backend/src/platform/macos/mod.rs rename apps/desktop/{src-tauri => src-backend}/src/platform/macos/sc_shareable_content.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/platform/mod.rs (96%) rename apps/desktop/{src-tauri => src-backend}/src/platform/win.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/power_observer.rs (98%) rename apps/desktop/{src-tauri => src-backend}/src/presets.rs (97%) rename apps/desktop/{src-tauri => src-backend}/src/recording.rs (98%) rename apps/desktop/{src-tauri => src-backend}/src/recording_settings.rs (97%) rename apps/desktop/{src-tauri => src-backend}/src/recording_telemetry.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/recordings_locations.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/recovery.rs (97%) rename apps/desktop/{src-tauri => src-backend}/src/resource.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/screenshot_editor.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/target_select_overlay.rs (90%) rename apps/desktop/{src-tauri => src-backend}/src/telemetry.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/thumbnails/linux.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/thumbnails/mac.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/thumbnails/mod.rs (100%) rename apps/desktop/{src-tauri => src-backend}/src/thumbnails/windows.rs (100%) create mode 100644 apps/desktop/src-backend/src/tray.rs rename apps/desktop/{src-tauri => src-backend}/src/update_project_names.rs (99%) create mode 100644 apps/desktop/src-backend/src/updates.rs rename apps/desktop/{src-tauri => src-backend}/src/upload.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/web_api.rs (99%) rename apps/desktop/{src-tauri => src-backend}/src/window_exclusion.rs (94%) rename apps/desktop/{src-tauri => src-backend}/src/window_position_persistence.rs (98%) create mode 100644 apps/desktop/src-backend/src/windows.rs rename apps/desktop/{src-tauri => src-backend}/tests/exit_shutdown.rs (100%) rename apps/desktop/{src-tauri => src-backend}/tests/web_api_startup.rs (72%) delete mode 100644 apps/desktop/src-tauri/.gitignore delete mode 100644 apps/desktop/src-tauri/Info.plist delete mode 100644 apps/desktop/src-tauri/build.rs delete mode 100644 apps/desktop/src-tauri/capabilities/default.json delete mode 100644 apps/desktop/src-tauri/src/flags.rs delete mode 100644 apps/desktop/src-tauri/src/platform/macos/mod.rs delete mode 100644 apps/desktop/src-tauri/src/tray.rs delete mode 100644 apps/desktop/src-tauri/src/updates.rs delete mode 100644 apps/desktop/src-tauri/src/windows.rs delete mode 100644 apps/desktop/src-tauri/tauri.conf.json delete mode 100644 apps/desktop/src-tauri/tauri.prod.conf.json create mode 100644 crates/desktop-runtime-macros/Cargo.toml create mode 100644 crates/desktop-runtime-macros/src/lib.rs create mode 100644 crates/desktop-runtime/Cargo.toml create mode 100644 crates/desktop-runtime/src/lib.rs create mode 100644 crates/desktop-runtime/src/protocol.rs create mode 100644 crates/desktop-runtime/src/transport.rs diff --git a/Cargo.lock b/Cargo.lock index 88ca9aca6df..61b90f95dff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ dependencies = [ "bitflags 2.13.1", "bytes", "bytestring", - "derive_more 2.0.1", + "derive_more", "encoding_rs", "foldhash", "futures-core", @@ -153,7 +153,7 @@ dependencies = [ "bytes", "bytestring", "cfg-if", - "derive_more 2.0.1", + "derive_more", "encoding_rs", "foldhash", "futures-core", @@ -234,21 +234,6 @@ dependencies = [ "equator", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -403,30 +388,6 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image 0.25.8", - "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.1", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "wl-clipboard-rs", - "x11rb", -] [[package]] name = "arg_enum_proc_macro" @@ -476,14 +437,10 @@ dependencies = [ "futures-channel", "futures-util", "rand 0.9.2", - "raw-window-handle", "serde", "serde_repr", "tokio", "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", "zbus", ] @@ -640,29 +597,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps 6.2.2", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -1033,27 +967,6 @@ dependencies = [ "piper", ] -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "build-time" version = "0.1.3" @@ -1116,9 +1029,6 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -dependencies = [ - "serde", -] [[package]] name = "bytestring" @@ -1129,31 +1039,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.1", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps 6.2.2", -] - [[package]] name = "calloop" version = "0.13.0" @@ -1180,15 +1065,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "camino" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" -dependencies = [ - "serde_core", -] - [[package]] name = "cap" version = "0.1.0" @@ -1450,6 +1326,7 @@ dependencies = [ "cap-camera-effects", "cap-camera-ffmpeg", "cap-cli-install", + "cap-desktop-runtime", "cap-editor", "cap-enc-ffmpeg", "cap-export", @@ -1475,16 +1352,14 @@ dependencies = [ "flume", "futures", "futures-intrusive", - "global-hotkey", "image 0.25.8", "kameo", "keyed_priority_queue", "lazy_static", - "libappindicator", "log", "lz4_flex", "md5", - "nix 0.29.0", + "nix", "objc", "objc2 0.6.2", "objc2-app-kit 0.3.1", @@ -1509,31 +1384,8 @@ dependencies = [ "serde", "serde_json", "specta", - "specta-typescript", "swift-rs", "sysinfo 0.35.2", - "tauri", - "tauri-build", - "tauri-nspanel", - "tauri-plugin-clipboard-manager", - "tauri-plugin-deep-link", - "tauri-plugin-dialog", - "tauri-plugin-fs", - "tauri-plugin-global-shortcut", - "tauri-plugin-http", - "tauri-plugin-notification", - "tauri-plugin-oauth", - "tauri-plugin-opener", - "tauri-plugin-os", - "tauri-plugin-positioner", - "tauri-plugin-process", - "tauri-plugin-sentry", - "tauri-plugin-shell", - "tauri-plugin-single-instance", - "tauri-plugin-store", - "tauri-plugin-updater", - "tauri-plugin-window-state", - "tauri-specta", "tempfile", "thiserror 1.0.69", "tokio", @@ -1544,17 +1396,40 @@ dependencies = [ "tracing-futures", "tracing-opentelemetry", "tracing-subscriber", + "url", "uuid", - "webview2-com", "wgpu", "whisper-rs", "windows 0.60.0", - "windows-core 0.61.2", "windows-sys 0.59.0", "winreg", "workspace-hack", ] +[[package]] +name = "cap-desktop-runtime" +version = "0.1.0" +dependencies = [ + "cap-desktop-runtime-macros", + "dirs 6.0.0", + "futures", + "inventory", + "serde", + "serde_json", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "cap-desktop-runtime-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "cap-editor" version = "0.1.0" @@ -2008,7 +1883,7 @@ dependencies = [ "flume", "futures", "libc", - "nix 0.29.0", + "nix", "serde", "serde_json", "tempfile", @@ -2036,39 +1911,6 @@ dependencies = [ "workspace-hack", ] -[[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.16", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.7", -] - [[package]] name = "castaway" version = "0.2.4" @@ -2114,17 +1956,6 @@ dependencies = [ "nom 7.1.3", ] -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -2151,12 +1982,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -2259,7 +2084,7 @@ version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.106", @@ -2419,32 +2244,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.18.1" @@ -2718,30 +2517,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crash-context" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" -dependencies = [ - "cfg-if", - "libc", - "mach2", -] - -[[package]] -name = "crash-handler" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "mach2", - "parking_lot", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -2826,43 +2601,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.106", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.106", -] - [[package]] name = "ctor" version = "0.10.1" @@ -3004,17 +2742,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -3046,19 +2773,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.106", -] - [[package]] name = "derive_more" version = "2.0.1" @@ -3201,7 +2915,6 @@ checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.13.1", "block2 0.6.1", - "libc", "objc2 0.6.2", ] @@ -3225,38 +2938,6 @@ dependencies = [ "libloading 0.8.8", ] -[[package]] -name = "dlopen2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "document-features" version = "0.2.11" @@ -3301,9 +2982,6 @@ name = "dpi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] [[package]] name = "drm" @@ -3345,29 +3023,14 @@ dependencies = [ ] [[package]] -name = "dtoa" -version = "1.0.10" +name = "dtor" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" [[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dtor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" - -[[package]] -name = "dunce" -version = "1.0.5" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" @@ -3383,26 +3046,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "embed-resource" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 0.9.7", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - [[package]] name = "encode_unicode" version = "1.0.0" @@ -3471,17 +3114,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.14" @@ -3637,16 +3269,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - [[package]] name = "filetime" version = "0.2.26" @@ -3677,12 +3299,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.1.2" @@ -3833,16 +3449,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.31" @@ -3974,105 +3580,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps 6.2.2", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps 6.2.2", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps 6.2.2", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps 6.2.2", - "x11", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -4093,17 +3600,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -4186,38 +3682,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps 6.2.2", - "winapi", -] - [[package]] name = "gl_generator" version = "0.14.0" @@ -4229,77 +3693,12 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.1", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps 6.2.2", -] - [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "global-hotkey" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" -dependencies = [ - "crossbeam-channel", - "keyboard-types", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "once_cell", - "serde", - "thiserror 2.0.16", - "windows-sys 0.59.0", - "x11rb", - "xkeysym", -] - [[package]] name = "glow" version = "0.16.0" @@ -4334,28 +3733,6 @@ dependencies = [ "wgpu", ] -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps 6.2.2", -] - -[[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-alloc" version = "0.6.0" @@ -4407,58 +3784,6 @@ dependencies = [ "bitflags 2.13.1", ] -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps 6.2.2", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "guardian" version = "1.3.0" @@ -4501,12 +3826,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -4518,12 +3837,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -4598,18 +3911,6 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever", - "match_token", -] - [[package]] name = "http" version = "0.2.12" @@ -4655,12 +3956,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" - [[package]] name = "httparse" version = "1.10.1" @@ -4779,16 +4074,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ico" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" -dependencies = [ - "byteorder", - "png 0.17.16", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -5029,8 +4314,6 @@ checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown 0.15.5", - "serde", - "serde_core", ] [[package]] @@ -5047,19 +4330,10 @@ dependencies = [ ] [[package]] -name = "infer" -version = "0.19.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", @@ -5199,29 +4473,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps 6.2.2", -] - [[package]] name = "jni" version = "0.21.1" @@ -5322,28 +4573,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "kameo" version = "0.17.2" @@ -5366,24 +4595,13 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3f384b32bf6426ae93a8b37da62c85073b676a31a82a86d608ad86453878de0" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.106", "uuid", ] -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.13.1", - "serde", - "unicode-segmentation", -] - [[package]] name = "keyed_priority_queue" version = "0.4.2" @@ -5431,18 +4649,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.11.4", - "selectors", -] - [[package]] name = "kurbo" version = "0.11.3" @@ -5504,30 +4710,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading 0.7.4", - "once_cell", -] - [[package]] name = "libc" version = "0.2.175" @@ -5544,16 +4726,6 @@ dependencies = [ "cc", ] -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libloading" version = "0.8.8" @@ -5752,24 +4924,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "mac-notification-sys" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" -dependencies = [ - "cc", - "objc2 0.6.2", - "objc2-foundation 0.3.1", - "time", -] - [[package]] name = "mach2" version = "0.4.3" @@ -5814,31 +4968,6 @@ dependencies = [ "libc", ] -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "matchers" version = "0.2.0" @@ -5848,12 +4977,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" version = "0.7.3" @@ -5956,87 +5079,12 @@ dependencies = [ "unicase", ] -[[package]] -name = "minidump-common" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" -dependencies = [ - "bitflags 2.13.1", - "debugid", - "num-derive", - "num-traits", - "range-map", - "scroll", - "smart-default", -] - -[[package]] -name = "minidump-writer" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" -dependencies = [ - "bitflags 2.13.1", - "byteorder", - "cfg-if", - "crash-context", - "goblin", - "libc", - "log", - "mach2", - "memmap2", - "memoffset", - "minidump-common", - "nix 0.28.0", - "procfs-core", - "scroll", - "tempfile", - "thiserror 1.0.69", -] - -[[package]] -name = "minidumper" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "log", - "minidump-writer", - "parking_lot", - "polling", - "scroll", - "thiserror 1.0.69", - "uds", -] - -[[package]] -name = "minidumper-child" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4f23f835dbe67e44ddf884d3802ff549ca5948bf60e9fd70e9a13c96324d1" -dependencies = [ - "crash-handler", - "minidumper", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "minisign-verify" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -6123,27 +5171,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "muda" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "once_cell", - "png 0.17.16", - "serde", - "thiserror 2.0.16", - "windows-sys 0.60.2", -] - [[package]] name = "naga" version = "25.0.1" @@ -6153,7 +5180,7 @@ dependencies = [ "arrayvec", "bit-set", "bitflags 2.13.1", - "cfg_aliases 0.2.1", + "cfg_aliases", "codespan-reporting", "half", "hashbrown 0.15.5", @@ -6299,18 +5326,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -6319,16 +5334,10 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.13.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nom" version = "7.1.3" @@ -6354,20 +5363,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "notify-rust" -version = "4.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" -dependencies = [ - "futures-lite", - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus", -] - [[package]] name = "ntapi" version = "0.4.2" @@ -6503,7 +5498,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", @@ -6525,17 +5520,6 @@ dependencies = [ "objc_exception", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -6559,7 +5543,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", - "objc2-exception-helper", ] [[package]] @@ -6799,15 +5782,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - [[package]] name = "objc2-foundation" version = "0.2.2" @@ -6855,16 +5829,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" -dependencies = [ - "objc2 0.6.2", - "objc2-core-foundation", -] - [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -6889,18 +5853,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-osa-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -6925,17 +5877,6 @@ dependencies = [ "objc2-foundation 0.3.1", ] -[[package]] -name = "objc2-security" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.2", - "objc2-core-foundation", -] - [[package]] name = "objc2-symbols" version = "0.2.2" @@ -6967,18 +5908,6 @@ dependencies = [ "objc2-user-notifications", ] -[[package]] -name = "objc2-ui-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.2", - "objc2-core-foundation", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-uniform-type-identifiers" version = "0.2.2" @@ -7004,39 +5933,14 @@ dependencies = [ ] [[package]] -name = "objc2-web-kit" -version = "0.3.1" +name = "objc_exception" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" -dependencies = [ - "bitflags 2.13.1", - "block2 0.6.1", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" dependencies = [ "cc", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.7" @@ -7118,7 +6022,6 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" dependencies = [ - "dunce", "is-wsl", "libc", ] @@ -7281,16 +6184,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -7338,30 +6231,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "os_pipe" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "osakit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" -dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", - "objc2-osa-kit", - "serde", - "serde_json", - "thiserror 2.0.16", -] - [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -7371,31 +6240,6 @@ dependencies = [ "ttf-parser 0.25.1", ] -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps 6.2.2", -] - [[package]] name = "parakeet-rs" version = "0.3.4" @@ -7485,132 +6329,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap 2.11.4", -] - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.10.0" @@ -7709,12 +6427,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "plist" version = "1.8.0" @@ -7813,12 +6525,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "presser" version = "0.3.1" @@ -7854,26 +6560,6 @@ dependencies = [ "num-integer", ] -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -7883,36 +6569,6 @@ dependencies = [ "toml_edit 0.23.10+spec-1.0.0", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.101" @@ -7922,16 +6578,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs-core" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" -dependencies = [ - "bitflags 2.13.1", - "hex", -] - [[package]] name = "profiling" version = "1.0.17" @@ -8039,7 +6685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -8079,7 +6725,7 @@ version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", "socket2 0.6.0", @@ -8102,20 +6748,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - [[package]] name = "rand" version = "0.8.5" @@ -8137,16 +6769,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -8167,15 +6789,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -8194,39 +6807,12 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "range-alloc" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" -[[package]] -name = "range-map" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" -dependencies = [ - "num-traits", -] - [[package]] name = "rangemap" version = "1.6.0" @@ -8579,35 +7165,10 @@ dependencies = [ ] [[package]] -name = "rfd" -version = "0.15.4" +name = "rgb" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" -dependencies = [ - "ashpd", - "block2 0.6.1", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] @@ -8713,16 +7274,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rust-ini" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -9000,18 +7551,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - [[package]] name = "schemars" version = "1.0.4" @@ -9078,26 +7617,6 @@ dependencies = [ "objc", ] -[[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 2.0.106", -] - [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -9166,24 +7685,6 @@ dependencies = [ "libc", ] -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc", - "smallvec", -] - [[package]] name = "self_cell" version = "1.2.0" @@ -9312,17 +7813,6 @@ dependencies = [ "sentry-core", ] -[[package]] -name = "sentry-rust-minidump" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63964525bf74b16233dbcfb307e11485ebd8ff8f87f6ae212b07ca7937cd2db1" -dependencies = [ - "minidumper-child", - "sentry", - "thiserror 2.0.16", -] - [[package]] name = "sentry-tracing" version = "0.42.0" @@ -9363,18 +7853,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -9471,70 +7949,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.4", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - [[package]] name = "sha1" version = "0.10.6" @@ -9566,34 +7980,12 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared_child" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" -dependencies = [ - "libc", - "sigchld", - "windows-sys 0.60.2", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "sigchld" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" -dependencies = [ - "libc", - "os_pipe", - "signal-hook", -] - [[package]] name = "signal-hook" version = "0.3.18" @@ -9685,7 +8077,7 @@ dependencies = [ "bindgen 0.71.1", "cc", "flate2", - "heck 0.5.0", + "heck", "lazy_static", "regex", "serde_json", @@ -9735,17 +8127,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smart-default" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "smithay-client-toolkit" version = "0.19.2" @@ -9819,7 +8200,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "as-raw-xcb-connection", "bytemuck", - "cfg_aliases 0.2.1", + "cfg_aliases", "core-graphics 0.24.0", "drm", "fastrand", @@ -9843,32 +8224,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps 6.2.2", -] - [[package]] name = "specta" version = "2.0.0-rc.20" @@ -9895,27 +8250,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "specta-serde" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12260cbb21abb2e83a0375b1521867910e3aed8a7afa782206150ce552cd2e5a" -dependencies = [ - "specta", - "thiserror 1.0.69", -] - -[[package]] -name = "specta-typescript" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4472229365ceb6395487e3a60d921ad8e21f9ad06eaecc396f098902c9adc" -dependencies = [ - "specta", - "specta-serde", - "thiserror 1.0.69", -] - [[package]] name = "spin" version = "0.9.8" @@ -9973,31 +8307,6 @@ dependencies = [ "float-cmp", ] -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - [[package]] name = "strsim" version = "0.11.1" @@ -10028,7 +8337,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -10041,7 +8350,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.106", @@ -10247,7 +8556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr 0.15.8", - "heck 0.5.0", + "heck", "pkg-config", "toml 0.8.2", "version-compare", @@ -10260,61 +8569,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" dependencies = [ "cfg-expr 0.20.8", - "heck 0.5.0", + "heck", "pkg-config", "toml 1.1.0+spec-1.1.0", "version-compare", ] -[[package]] -name = "tao" -version = "0.34.3" -dependencies = [ - "bitflags 2.13.1", - "block2 0.6.1", - "core-foundation 0.10.1", - "core-graphics 0.24.0", - "crossbeam-channel", - "dispatch", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni 0.21.1", - "lazy_static", - "libc", - "log", - "ndk 0.9.0", - "ndk-context", - "ndk-sys 0.6.0+11769913", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", - "once_cell", - "parking_lot", - "raw-window-handle", - "scopeguard", - "tao-macros", - "unicode-segmentation", - "url", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "tar" version = "0.4.44" @@ -10329,634 +8589,14 @@ dependencies = [ [[package]] name = "target-lexicon" version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - -[[package]] -name = "tauri" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs 6.0.0", - "dunce", - "embed_plist", - "getrandom 0.3.3", - "glob", - "gtk", - "heck 0.5.0", - "http 1.3.1", - "http-range", - "image 0.25.8", - "jni 0.21.1", - "libc", - "log", - "mime", - "muda", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", - "objc2-ui-kit 0.3.1", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "specta", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.16", - "tokio", - "tray-icon", - "url", - "urlpattern", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows 0.61.3", -] - -[[package]] -name = "tauri-build" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs 6.0.0", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "toml 0.9.7", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png 0.17.16", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.106", - "tauri-utils", - "thiserror 2.0.16", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-nspanel" -version = "2.0.1" -source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#18ffb9a201fbf6fedfaa382fd4b92315ea30ab1a" -dependencies = [ - "bitflags 2.13.1", - "block", - "cocoa", - "core-foundation 0.10.1", - "core-graphics 0.25.0", - "objc", - "objc-foundation", - "objc_id", - "tauri", -] - -[[package]] -name = "tauri-plugin" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.7", - "walkdir", -] - -[[package]] -name = "tauri-plugin-clipboard-manager" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca" -dependencies = [ - "arboard", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", -] - -[[package]] -name = "tauri-plugin-deep-link" -version = "2.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd67112fb1131834c2a7398ffcba520dbbf62c17de3b10329acd1a3554b1a9bb" -dependencies = [ - "dunce", - "plist", - "rust-ini", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.16", - "tracing", - "url", - "windows-registry", - "windows-result 0.3.4", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.16", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" -dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.16", - "toml 0.9.7", - "url", -] - -[[package]] -name = "tauri-plugin-global-shortcut" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df9f0f7bf2fe768b85fee4951c2505a35b72c44df1f6403e74e110bc13c5f58" -dependencies = [ - "global-hotkey", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", -] - -[[package]] -name = "tauri-plugin-http" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938a3d7051c9a82b431e3a0f3468f85715b3442b3c3a3913095e9fa509e2652c" -dependencies = [ - "bytes", - "cookie_store", - "data-url", - "http 1.3.1", - "regex", - "reqwest", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.16", - "tokio", - "url", - "urlpattern", -] - -[[package]] -name = "tauri-plugin-notification" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fbc86b929b5376ab84b25c060f966d146b2fbd59b6af8264027b343c82c219" -dependencies = [ - "log", - "notify-rust", - "rand 0.9.2", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", - "time", - "url", -] - -[[package]] -name = "tauri-plugin-oauth" -version = "2.0.0" -source = "git+https://github.com/FabianLars/tauri-plugin-oauth?branch=v2#f941c99c3ed528a55db7fe05c931480a8255041d" -dependencies = [ - "httparse", - "log", - "serde", - "tauri", - "tauri-plugin", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "tauri-plugin-opener" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" -dependencies = [ - "dunce", - "glob", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", - "open", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", - "url", - "windows 0.61.3", - "zbus", -] - -[[package]] -name = "tauri-plugin-os" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba" -dependencies = [ - "gethostname", - "log", - "os_info", - "serde", - "serde_json", - "serialize-to-javascript", - "sys-locale", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", -] - -[[package]] -name = "tauri-plugin-positioner" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a01e373ea3f3f5f46d40f434ba13bd12fa4833aabab50dfc09f6362bad27c95" -dependencies = [ - "log", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", -] - -[[package]] -name = "tauri-plugin-process" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" -dependencies = [ - "tauri", - "tauri-plugin", -] - -[[package]] -name = "tauri-plugin-sentry" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7432b519b6d2d027082a940783c61ba9b684b38d8df7558cd5f924d87009b295" -dependencies = [ - "base64 0.22.1", - "schemars 0.8.22", - "sentry", - "sentry-rust-minidump", - "serde", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", -] - -[[package]] -name = "tauri-plugin-shell" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" -dependencies = [ - "encoding_rs", - "log", - "open", - "os_pipe", - "regex", - "schemars 0.8.22", - "serde", - "serde_json", - "shared_child", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", - "tokio", -] - -[[package]] -name = "tauri-plugin-single-instance" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9cac815bf11c4a80fb498666bcdad66d65b89e3ae24669e47806febb76389c" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-plugin-deep-link", - "thiserror 2.0.16", - "tracing", - "windows-sys 0.60.2", - "zbus", -] - -[[package]] -name = "tauri-plugin-store" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19" -dependencies = [ - "dunce", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", - "tokio", - "tracing", -] - -[[package]] -name = "tauri-plugin-updater" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" -dependencies = [ - "base64 0.22.1", - "dirs 6.0.0", - "flate2", - "futures-util", - "http 1.3.1", - "infer", - "log", - "minisign-verify", - "osakit", - "percent-encoding", - "reqwest", - "semver", - "serde", - "serde_json", - "tar", - "tauri", - "tauri-plugin", - "tempfile", - "thiserror 2.0.16", - "time", - "tokio", - "url", - "windows-sys 0.60.2", - "zip", -] - -[[package]] -name = "tauri-plugin-window-state" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e" -dependencies = [ - "bitflags 2.13.1", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.16", -] - -[[package]] -name = "tauri-runtime" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http 1.3.1", - "jni 0.21.1", - "objc2 0.6.2", - "objc2-ui-kit 0.3.1", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.16", - "url", - "webkit2gtk", - "webview2-com", - "windows 0.61.3", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" -dependencies = [ - "gtk", - "http 1.3.1", - "jni 0.21.1", - "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows 0.61.3", - "wry", -] - -[[package]] -name = "tauri-specta" -version = "2.0.0-rc.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d06336a2fa3ead0c8d08719e78cbc1fb73650845789605bd2ab908cbde72815" -dependencies = [ - "heck 0.5.0", - "serde", - "serde_json", - "specta", - "specta-typescript", - "tauri", - "tauri-specta-macros", - "thiserror 1.0.69", -] - -[[package]] -name = "tauri-specta-macros" -version = "2.0.0-rc.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tauri-utils" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor 0.2.9", - "dunce", - "glob", - "html5ever", - "http 1.3.1", - "infer", - "json-patch", - "kuchikiki", - "log", - "memchr", - "phf 0.11.3", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.16", - "toml 0.9.7", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" -dependencies = [ - "embed-resource", - "toml 0.9.7", -] +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] -name = "tauri-winrt-notification" -version = "0.7.2" +name = "target-lexicon" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" -dependencies = [ - "quick-xml 0.37.5", - "thiserror 2.0.16", - "windows 0.61.3", - "windows-version", -] +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -10971,17 +8611,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -11101,15 +8730,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tiny-skia" version = "0.11.4" @@ -11143,7 +8763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" dependencies = [ "as-raw-xcb-connection", - "ctor 0.10.1", + "ctor", "libloading 0.8.8", "pkg-config", "tracing", @@ -11308,21 +8928,6 @@ dependencies = [ "toml_edit 0.20.2", ] -[[package]] -name = "toml" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" -dependencies = [ - "indexmap 2.11.4", - "serde_core", - "serde_spanned 1.1.0", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.13", -] - [[package]] name = "toml" version = "1.1.0+spec-1.1.0" @@ -11365,17 +8970,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.20.2" @@ -11610,40 +9204,6 @@ dependencies = [ "strength_reduce", ] -[[package]] -name = "tray-icon" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" -dependencies = [ - "crossbeam-channel", - "dirs 6.0.0", - "libappindicator", - "muda", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.1", - "once_cell", - "png 0.17.16", - "serde", - "thiserror 2.0.16", - "windows-sys 0.59.0", -] - -[[package]] -name = "tree_magic_mini" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" -dependencies = [ - "memchr", - "nom 7.1.3", - "once_cell", - "petgraph", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -11695,27 +9255,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" -[[package]] -name = "uds" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" -dependencies = [ - "libc", -] - [[package]] name = "uds_windows" version = "1.1.0" @@ -11736,47 +9281,6 @@ dependencies = [ "libc", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicase" version = "2.8.1" @@ -11937,18 +9441,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - [[package]] name = "usvg" version = "0.45.1" @@ -12067,26 +9559,6 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -12106,12 +9578,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -12350,50 +9816,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webkit2gtk" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps 6.2.2", -] - [[package]] name = "webpki-root-certs" version = "1.0.2" @@ -12412,42 +9834,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webview2-com" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" -dependencies = [ - "thiserror 2.0.16", - "windows 0.61.3", - "windows-core 0.61.2", -] - [[package]] name = "weezl" version = "0.1.10" @@ -12462,7 +9848,7 @@ checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", "bitflags 2.13.1", - "cfg_aliases 0.2.1", + "cfg_aliases", "document-features", "hashbrown 0.15.5", "js-sys", @@ -12492,7 +9878,7 @@ dependencies = [ "bit-set", "bit-vec", "bitflags 2.13.1", - "cfg_aliases 0.2.1", + "cfg_aliases", "document-features", "hashbrown 0.15.5", "indexmap 2.11.4", @@ -12552,7 +9938,7 @@ dependencies = [ "block", "bytemuck", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "core-graphics-types 0.1.3", "glow", "glutin_wgl_sys", @@ -12672,21 +10058,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - [[package]] name = "windows" version = "0.48.0" @@ -13145,15 +10516,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-version" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e061eb0a22b4a1d778ad70f7575ec7845490abb35b08fa320df7895882cacb" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-win" version = "3.0.0" @@ -13356,7 +10718,7 @@ dependencies = [ "block2 0.5.1", "bytemuck", "calloop", - "cfg_aliases 0.2.1", + "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", "core-graphics 0.23.2", @@ -13369,7 +10731,7 @@ dependencies = [ "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", - "objc2-ui-kit 0.2.2", + "objc2-ui-kit", "orbclient", "percent-encoding", "pin-project", @@ -13438,25 +10800,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wl-clipboard-rs" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb" -dependencies = [ - "libc", - "log", - "os_pipe", - "rustix 0.38.44", - "tempfile", - "thiserror 2.0.16", - "tree_magic_mini", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", -] - [[package]] name = "workspace-hack" version = "0.1.0" @@ -13516,7 +10859,6 @@ dependencies = [ "specta-macros", "stable_deref_trait", "syn 2.0.106", - "tauri-utils", "thiserror 2.0.16", "time", "tokio", @@ -13532,51 +10874,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -[[package]] -name = "wry" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" -dependencies = [ - "base64 0.22.1", - "block2 0.6.1", - "cookie", - "crossbeam-channel", - "dirs 6.0.0", - "dpi", - "dunce", - "gdkx11", - "gtk", - "html5ever", - "http 1.3.1", - "javascriptcore-rs", - "jni 0.21.1", - "kuchikiki", - "libc", - "ndk 0.9.0", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "objc2-ui-kit 0.3.1", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.16", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - [[package]] name = "x11" version = "2.21.0" @@ -13771,7 +11068,7 @@ version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", @@ -13877,18 +11174,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zip" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" -dependencies = [ - "arbitrary", - "crc32fast", - "indexmap 2.11.4", - "memchr", -] - [[package]] name = "zlib-rs" version = "0.5.2" @@ -13940,7 +11225,7 @@ version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", diff --git a/Cargo.toml b/Cargo.toml index 91e45dbbdc9..3bf97911967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "apps/cli", - "apps/desktop/src-tauri", + "apps/desktop/src-backend", "crates/*", "crates/workspace-hack", ] @@ -24,7 +24,6 @@ tokio = { version = "1.39.3", features = [ "rt-multi-thread", "time", ] } -tauri = { version = "2.5.0", features = ["specta"] } specta = { version = "=2.0.0-rc.20", features = [ "derive", "serde_json", @@ -117,7 +116,6 @@ cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8" } # https://github.com/gfx-rs/wgpu/pull/7550 # wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "cd41a6e32a6239b65d1cecbeccde6a43a100914a" } wgpu-hal = { path = "vendor/wgpu-hal" } -tao = { path = "vendor/tao" } # https://github.com/CapSoftware/reqwest/commit/9b5ecbd5210a9510fde766015cabb724c1e70d2e reqwest = { git = "https://github.com/CapSoftware/reqwest", rev = "9b5ecbd5210a9510fde766015cabb724c1e70d2e" } diff --git a/apps/desktop/src-backend/.gitignore b/apps/desktop/src-backend/.gitignore new file mode 100644 index 00000000000..aba21e242c9 --- /dev/null +++ b/apps/desktop/src-backend/.gitignore @@ -0,0 +1,3 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-backend/Cargo.toml similarity index 77% rename from apps/desktop/src-tauri/Cargo.toml rename to apps/desktop/src-backend/Cargo.toml index 73f82aef047..3d8a1daeb9c 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-backend/Cargo.toml @@ -12,42 +12,15 @@ workspace = true name = "cap_desktop_lib" crate-type = ["lib", "cdylib", "staticlib"] -[build-dependencies] -tauri-build = { version = "2.1.0", features = [] } - [target.'cfg(target_os = "macos")'.build-dependencies] swift-rs = { version = "1.0.6", features = ["build"] } [dependencies] -tauri = { workspace = true, features = [ - "macos-private-api", - "protocol-asset", - "tray-icon", - "image-png", -] } -tauri-specta = { version = "=2.0.0-rc.20", features = ["derive", "typescript"] } -tauri-plugin-dialog = "2.2.0" -tauri-plugin-fs = "2.2.0" -tauri-plugin-global-shortcut = "2.2.0" -tauri-plugin-http = "=2.5.2" -tauri-plugin-notification = "2.2.0" -tauri-plugin-os = "2.2.0" -tauri-plugin-process = "2.2.0" -tauri-plugin-shell = "2.2.0" -tauri-plugin-single-instance = { version = "2.2.0", features = ["deep-link"] } -tauri-plugin-store = "2.2.0" -tauri-plugin-updater = "2.9.0" -tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", branch = "v2" } -tauri-plugin-window-state = "2.2.0" -tauri-plugin-positioner = "2.2.0" -tauri-plugin-deep-link = "2.2.0" -tauri-plugin-clipboard-manager = "2.2.1" -tauri-plugin-opener = "2.2.6" +cap-desktop-runtime = { path = "../../../crates/desktop-runtime" } serde = { workspace = true } serde_json = "1.0.111" specta.workspace = true -specta-typescript = "0.0.7" tokio = { workspace = true, features = ["signal"] } uuid = { version = "1.10.0", features = ["v4"] } image = "0.25.2" @@ -66,7 +39,6 @@ device_query = "4.0.1" base64 = "0.22.1" reqwest = { version = "0.12.24", features = ["json", "stream", "multipart"] } dotenvy_macro = "0.15.7" -global-hotkey = "0.7.0" rand = "0.8.5" cpal.workspace = true keyed_priority_queue = "0.4.2" @@ -76,6 +48,7 @@ whisper-rs = "0.11.0" lazy_static = "1.4.0" log = "0.4.20" semver = "1" +url = "2" cap-audio = { path = "../../../crates/audio" } cap-camera = { path = "../../../crates/camera", features = ["serde", "specta"] } @@ -109,7 +82,6 @@ wgpu.workspace = true pollster = "0.4" bytemuck = "1.23.1" kameo = "0.17.2" -tauri-plugin-sentry = "0.5.0" thiserror.workspace = true bytes = "1.10.1" async-stream = "0.3.6" @@ -144,16 +116,12 @@ objc2-foundation = { version = "0.3.0", features = [ cocoa = "0.26.0" objc = "0.2.7" swift-rs = "1.0.6" -tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } cidre = { workspace = true } cap-camera-ffmpeg = { path = "../../../crates/camera-ffmpeg" } [target.'cfg(not(all(target_os = "macos", target_arch = "x86_64")))'.dependencies] parakeet-rs = "0.3.4" -[target.'cfg(target_os = "linux")'.dependencies] -libappindicator = "0.9.0" - [target.'cfg(target_os= "windows")'.dependencies] windows = { workspace = true, features = [ "Foundation", @@ -172,11 +140,6 @@ windows = { workspace = true, features = [ ] } windows-sys = { workspace = true } winreg = "0.55" -webview2-com = "0.38.0" -windows-core = "0.61.2" [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["fs"] } - -[dev-dependencies] -tauri = { workspace = true, features = ["test"] } diff --git a/apps/desktop/src-tauri/Entitlements.plist b/apps/desktop/src-backend/Entitlements.plist similarity index 100% rename from apps/desktop/src-tauri/Entitlements.plist rename to apps/desktop/src-backend/Entitlements.plist diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/1.jpg b/apps/desktop/src-backend/assets/backgrounds/blue/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/1.jpg rename to apps/desktop/src-backend/assets/backgrounds/blue/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/2.jpg b/apps/desktop/src-backend/assets/backgrounds/blue/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/2.jpg rename to apps/desktop/src-backend/assets/backgrounds/blue/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/3.jpg b/apps/desktop/src-backend/assets/backgrounds/blue/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/3.jpg rename to apps/desktop/src-backend/assets/backgrounds/blue/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/4.jpg b/apps/desktop/src-backend/assets/backgrounds/blue/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/4.jpg rename to apps/desktop/src-backend/assets/backgrounds/blue/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/5.jpg b/apps/desktop/src-backend/assets/backgrounds/blue/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/5.jpg rename to apps/desktop/src-backend/assets/backgrounds/blue/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/6.jpg b/apps/desktop/src-backend/assets/backgrounds/blue/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/6.jpg rename to apps/desktop/src-backend/assets/backgrounds/blue/6.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/liverpool.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/liverpool.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/liverpool.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/liverpool.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/london.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/london.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/london.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/london.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/miami.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/miami.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/miami.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/miami.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/monaco.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/monaco.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/monaco.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/monaco.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/nyc.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/nyc.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/nyc.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/nyc.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/rome.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/rome.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/rome.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/rome.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/santorini.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/santorini.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/santorini.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/santorini.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/sf.jpg b/apps/desktop/src-backend/assets/backgrounds/cities/sf.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/sf.jpg rename to apps/desktop/src-backend/assets/backgrounds/cities/sf.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/1.jpg b/apps/desktop/src-backend/assets/backgrounds/dark/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/1.jpg rename to apps/desktop/src-backend/assets/backgrounds/dark/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/2.jpg b/apps/desktop/src-backend/assets/backgrounds/dark/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/2.jpg rename to apps/desktop/src-backend/assets/backgrounds/dark/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/3.jpg b/apps/desktop/src-backend/assets/backgrounds/dark/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/3.jpg rename to apps/desktop/src-backend/assets/backgrounds/dark/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/4.jpg b/apps/desktop/src-backend/assets/backgrounds/dark/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/4.jpg rename to apps/desktop/src-backend/assets/backgrounds/dark/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/5.jpg b/apps/desktop/src-backend/assets/backgrounds/dark/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/5.jpg rename to apps/desktop/src-backend/assets/backgrounds/dark/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/6.jpg b/apps/desktop/src-backend/assets/backgrounds/dark/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/6.jpg rename to apps/desktop/src-backend/assets/backgrounds/dark/6.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-dark.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sequoia-dark.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-dark.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sequoia-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-light.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sequoia-light.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-light.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sequoia-light.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-clouds.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-clouds.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-clouds.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-clouds.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-dark.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-dark.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-dark.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-evening.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-evening.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-evening.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-evening.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-fromabove.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-fromabove.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-fromabove.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-fromabove.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-horizon.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-horizon.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-horizon.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-horizon.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-light.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-light.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-light.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-light.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-river.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-river.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-river.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/sonoma-river.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dark.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-dark.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dark.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dawn-min.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-dawn-min.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dawn-min.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-dawn-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-day-min.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-day-min.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-day-min.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-day-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dusk-min.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-dusk-min.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dusk-min.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-dusk-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-light.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-light.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-light.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-light.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-night-min.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-night-min.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-night-min.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/tahoe-night-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-dark.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/ventura-dark.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-dark.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/ventura-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-semi-dark.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/ventura-semi-dark.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-semi-dark.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/ventura-semi-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura.jpg b/apps/desktop/src-backend/assets/backgrounds/macOS/ventura.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura.jpg rename to apps/desktop/src-backend/assets/backgrounds/macOS/ventura.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/1.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/1.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/2.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/2.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/3.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/3.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/4.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/4.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/5.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/5.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/6.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/6.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/6.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/7.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/7.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/7.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/7.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/8.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/8.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/8.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/8.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/9.jpg b/apps/desktop/src-backend/assets/backgrounds/orange/9.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/9.jpg rename to apps/desktop/src-backend/assets/backgrounds/orange/9.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/1.jpg b/apps/desktop/src-backend/assets/backgrounds/purple/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/1.jpg rename to apps/desktop/src-backend/assets/backgrounds/purple/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/2.jpg b/apps/desktop/src-backend/assets/backgrounds/purple/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/2.jpg rename to apps/desktop/src-backend/assets/backgrounds/purple/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/3.jpg b/apps/desktop/src-backend/assets/backgrounds/purple/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/3.jpg rename to apps/desktop/src-backend/assets/backgrounds/purple/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/4.jpg b/apps/desktop/src-backend/assets/backgrounds/purple/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/4.jpg rename to apps/desktop/src-backend/assets/backgrounds/purple/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/5.jpg b/apps/desktop/src-backend/assets/backgrounds/purple/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/5.jpg rename to apps/desktop/src-backend/assets/backgrounds/purple/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/6.jpg b/apps/desktop/src-backend/assets/backgrounds/purple/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/6.jpg rename to apps/desktop/src-backend/assets/backgrounds/purple/6.jpg diff --git a/apps/desktop/src-tauri/assets/dmg-background.png b/apps/desktop/src-backend/assets/dmg-background.png similarity index 100% rename from apps/desktop/src-tauri/assets/dmg-background.png rename to apps/desktop/src-backend/assets/dmg-background.png diff --git a/apps/desktop/src-tauri/assets/nsis-header.bmp b/apps/desktop/src-backend/assets/nsis-header.bmp similarity index 100% rename from apps/desktop/src-tauri/assets/nsis-header.bmp rename to apps/desktop/src-backend/assets/nsis-header.bmp diff --git a/apps/desktop/src-tauri/assets/nsis-sidebar.bmp b/apps/desktop/src-backend/assets/nsis-sidebar.bmp similarity index 100% rename from apps/desktop/src-tauri/assets/nsis-sidebar.bmp rename to apps/desktop/src-backend/assets/nsis-sidebar.bmp diff --git a/apps/desktop/src-tauri/assets/wix-banner.bmp b/apps/desktop/src-backend/assets/wix-banner.bmp similarity index 100% rename from apps/desktop/src-tauri/assets/wix-banner.bmp rename to apps/desktop/src-backend/assets/wix-banner.bmp diff --git a/apps/desktop/src-tauri/assets/wix-dialog.bmp b/apps/desktop/src-backend/assets/wix-dialog.bmp similarity index 100% rename from apps/desktop/src-tauri/assets/wix-dialog.bmp rename to apps/desktop/src-backend/assets/wix-dialog.bmp diff --git a/apps/desktop/src-tauri/binaries/.gitignore b/apps/desktop/src-backend/binaries/.gitignore similarity index 88% rename from apps/desktop/src-tauri/binaries/.gitignore rename to apps/desktop/src-backend/binaries/.gitignore index a28175f65f0..bb54721cb8a 100644 --- a/apps/desktop/src-tauri/binaries/.gitignore +++ b/apps/desktop/src-backend/binaries/.gitignore @@ -1,4 +1,5 @@ cap-muxer-* cap-exporter-* cap-* +*.dll !.gitignore diff --git a/apps/desktop/src-tauri/examples/desktop-display-transport-benchmark.rs b/apps/desktop/src-backend/examples/desktop-display-transport-benchmark.rs similarity index 100% rename from apps/desktop/src-tauri/examples/desktop-display-transport-benchmark.rs rename to apps/desktop/src-backend/examples/desktop-display-transport-benchmark.rs diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-backend/icons/128x128.png similarity index 100% rename from apps/desktop/src-tauri/icons/128x128.png rename to apps/desktop/src-backend/icons/128x128.png diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-backend/icons/128x128@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/128x128@2x.png rename to apps/desktop/src-backend/icons/128x128@2x.png diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-backend/icons/32x32.png similarity index 100% rename from apps/desktop/src-tauri/icons/32x32.png rename to apps/desktop/src-backend/icons/32x32.png diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-backend/icons/Square107x107Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square107x107Logo.png rename to apps/desktop/src-backend/icons/Square107x107Logo.png diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-backend/icons/Square142x142Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square142x142Logo.png rename to apps/desktop/src-backend/icons/Square142x142Logo.png diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-backend/icons/Square150x150Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square150x150Logo.png rename to apps/desktop/src-backend/icons/Square150x150Logo.png diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-backend/icons/Square284x284Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square284x284Logo.png rename to apps/desktop/src-backend/icons/Square284x284Logo.png diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-backend/icons/Square30x30Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square30x30Logo.png rename to apps/desktop/src-backend/icons/Square30x30Logo.png diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-backend/icons/Square310x310Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square310x310Logo.png rename to apps/desktop/src-backend/icons/Square310x310Logo.png diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-backend/icons/Square44x44Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square44x44Logo.png rename to apps/desktop/src-backend/icons/Square44x44Logo.png diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-backend/icons/Square71x71Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square71x71Logo.png rename to apps/desktop/src-backend/icons/Square71x71Logo.png diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-backend/icons/Square89x89Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square89x89Logo.png rename to apps/desktop/src-backend/icons/Square89x89Logo.png diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-backend/icons/StoreLogo.png similarity index 100% rename from apps/desktop/src-tauri/icons/StoreLogo.png rename to apps/desktop/src-backend/icons/StoreLogo.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/apps/desktop/src-backend/icons/android/mipmap-hdpi/ic_launcher.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png rename to apps/desktop/src-backend/icons/android/mipmap-hdpi/ic_launcher.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/desktop/src-backend/icons/android/mipmap-hdpi/ic_launcher_foreground.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png rename to apps/desktop/src-backend/icons/android/mipmap-hdpi/ic_launcher_foreground.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/desktop/src-backend/icons/android/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png rename to apps/desktop/src-backend/icons/android/mipmap-hdpi/ic_launcher_round.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/desktop/src-backend/icons/android/mipmap-mdpi/ic_launcher.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png rename to apps/desktop/src-backend/icons/android/mipmap-mdpi/ic_launcher.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/apps/desktop/src-backend/icons/android/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png rename to apps/desktop/src-backend/icons/android/mipmap-mdpi/ic_launcher_foreground.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/apps/desktop/src-backend/icons/android/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png rename to apps/desktop/src-backend/icons/android/mipmap-mdpi/ic_launcher_round.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/apps/desktop/src-backend/icons/android/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png rename to apps/desktop/src-backend/icons/android/mipmap-xhdpi/ic_launcher.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/apps/desktop/src-backend/icons/android/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png rename to apps/desktop/src-backend/icons/android/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/desktop/src-backend/icons/android/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png rename to apps/desktop/src-backend/icons/android/mipmap-xhdpi/ic_launcher_round.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/apps/desktop/src-backend/icons/android/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png rename to apps/desktop/src-backend/icons/android/mipmap-xxhdpi/ic_launcher.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/desktop/src-backend/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png rename to apps/desktop/src-backend/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/apps/desktop/src-backend/icons/android/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png rename to apps/desktop/src-backend/icons/android/mipmap-xxhdpi/ic_launcher_round.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/desktop/src-backend/icons/android/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png rename to apps/desktop/src-backend/icons/android/mipmap-xxxhdpi/ic_launcher.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/desktop/src-backend/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png rename to apps/desktop/src-backend/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/desktop/src-backend/icons/android/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png rename to apps/desktop/src-backend/icons/android/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-backend/icons/icon.icns similarity index 100% rename from apps/desktop/src-tauri/icons/icon.icns rename to apps/desktop/src-backend/icons/icon.icns diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-backend/icons/icon.ico similarity index 100% rename from apps/desktop/src-tauri/icons/icon.ico rename to apps/desktop/src-backend/icons/icon.ico diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-backend/icons/icon.png similarity index 100% rename from apps/desktop/src-tauri/icons/icon.png rename to apps/desktop/src-backend/icons/icon.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/apps/desktop/src-backend/icons/ios/AppIcon-20x20@1x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-20x20@1x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/apps/desktop/src-backend/icons/ios/AppIcon-20x20@2x-1.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png rename to apps/desktop/src-backend/icons/ios/AppIcon-20x20@2x-1.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-20x20@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-20x20@2x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png b/apps/desktop/src-backend/icons/ios/AppIcon-20x20@3x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-20x20@3x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/apps/desktop/src-backend/icons/ios/AppIcon-29x29@1x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-29x29@1x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/apps/desktop/src-backend/icons/ios/AppIcon-29x29@2x-1.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png rename to apps/desktop/src-backend/icons/ios/AppIcon-29x29@2x-1.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-29x29@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-29x29@2x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/apps/desktop/src-backend/icons/ios/AppIcon-29x29@3x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-29x29@3x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/apps/desktop/src-backend/icons/ios/AppIcon-40x40@1x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-40x40@1x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/apps/desktop/src-backend/icons/ios/AppIcon-40x40@2x-1.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png rename to apps/desktop/src-backend/icons/ios/AppIcon-40x40@2x-1.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-40x40@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-40x40@2x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/apps/desktop/src-backend/icons/ios/AppIcon-40x40@3x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-40x40@3x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-512@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-512@2x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-60x60@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-60x60@2x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/apps/desktop/src-backend/icons/ios/AppIcon-60x60@3x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-60x60@3x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/apps/desktop/src-backend/icons/ios/AppIcon-76x76@1x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-76x76@1x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-76x76@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-76x76@2x.png diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/apps/desktop/src-backend/icons/ios/AppIcon-83.5x83.5@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png rename to apps/desktop/src-backend/icons/ios/AppIcon-83.5x83.5@2x.png diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-default-symbolic.svg b/apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-default-symbolic.svg similarity index 100% rename from apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-default-symbolic.svg rename to apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-default-symbolic.svg diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-instant-symbolic.svg b/apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-instant-symbolic.svg similarity index 100% rename from apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-instant-symbolic.svg rename to apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-instant-symbolic.svg diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg b/apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg similarity index 100% rename from apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg rename to apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-stop-symbolic.svg b/apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-stop-symbolic.svg similarity index 100% rename from apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-stop-symbolic.svg rename to apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-stop-symbolic.svg diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-studio-symbolic.svg b/apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-studio-symbolic.svg similarity index 100% rename from apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-studio-symbolic.svg rename to apps/desktop/src-backend/icons/linux/so.cap.desktop-tray-studio-symbolic.svg diff --git a/apps/desktop/src-tauri/icons/macos/icon.icns b/apps/desktop/src-backend/icons/macos/icon.icns similarity index 100% rename from apps/desktop/src-tauri/icons/macos/icon.icns rename to apps/desktop/src-backend/icons/macos/icon.icns diff --git a/apps/desktop/src-tauri/icons/macos/icon.png b/apps/desktop/src-backend/icons/macos/icon.png similarity index 100% rename from apps/desktop/src-tauri/icons/macos/icon.png rename to apps/desktop/src-backend/icons/macos/icon.png diff --git a/apps/desktop/src-tauri/icons/tray-default-icon-instant.png b/apps/desktop/src-backend/icons/tray-default-icon-instant.png similarity index 100% rename from apps/desktop/src-tauri/icons/tray-default-icon-instant.png rename to apps/desktop/src-backend/icons/tray-default-icon-instant.png diff --git a/apps/desktop/src-tauri/icons/tray-default-icon-screenshot.png b/apps/desktop/src-backend/icons/tray-default-icon-screenshot.png similarity index 100% rename from apps/desktop/src-tauri/icons/tray-default-icon-screenshot.png rename to apps/desktop/src-backend/icons/tray-default-icon-screenshot.png diff --git a/apps/desktop/src-tauri/icons/tray-default-icon-studio.png b/apps/desktop/src-backend/icons/tray-default-icon-studio.png similarity index 100% rename from apps/desktop/src-tauri/icons/tray-default-icon-studio.png rename to apps/desktop/src-backend/icons/tray-default-icon-studio.png diff --git a/apps/desktop/src-tauri/icons/tray-default-icon.png b/apps/desktop/src-backend/icons/tray-default-icon.png similarity index 100% rename from apps/desktop/src-tauri/icons/tray-default-icon.png rename to apps/desktop/src-backend/icons/tray-default-icon.png diff --git a/apps/desktop/src-tauri/icons/tray-stop-icon.png b/apps/desktop/src-backend/icons/tray-stop-icon.png similarity index 100% rename from apps/desktop/src-tauri/icons/tray-stop-icon.png rename to apps/desktop/src-backend/icons/tray-stop-icon.png diff --git a/apps/desktop/src-tauri/sounds/action.ogg b/apps/desktop/src-backend/sounds/action.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/action.ogg rename to apps/desktop/src-backend/sounds/action.ogg diff --git a/apps/desktop/src-tauri/sounds/screenshot.ogg b/apps/desktop/src-backend/sounds/screenshot.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/screenshot.ogg rename to apps/desktop/src-backend/sounds/screenshot.ogg diff --git a/apps/desktop/src-tauri/sounds/start-recording.ogg b/apps/desktop/src-backend/sounds/start-recording.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/start-recording.ogg rename to apps/desktop/src-backend/sounds/start-recording.ogg diff --git a/apps/desktop/src-tauri/sounds/stop-recording.ogg b/apps/desktop/src-backend/sounds/stop-recording.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/stop-recording.ogg rename to apps/desktop/src-backend/sounds/stop-recording.ogg diff --git a/apps/desktop/src-tauri/src/api.rs b/apps/desktop/src-backend/src/api.rs similarity index 99% rename from apps/desktop/src-tauri/src/api.rs rename to apps/desktop/src-backend/src/api.rs index ef0988298df..bbd997e110d 100644 --- a/apps/desktop/src-tauri/src/api.rs +++ b/apps/desktop/src-backend/src/api.rs @@ -1,11 +1,11 @@ //! TODO: We should investigate generating this with OpenAPI. //! This will come part of the EffectTS rewrite work. +use cap_desktop_runtime::AppHandle; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; use std::collections::HashMap; -use tauri::AppHandle; use tracing::{instrument, trace}; use crate::web_api::{AuthedApiError, ManagerExt}; diff --git a/apps/desktop/src-tauri/src/audio.rs b/apps/desktop/src-backend/src/audio.rs similarity index 100% rename from apps/desktop/src-tauri/src/audio.rs rename to apps/desktop/src-backend/src/audio.rs diff --git a/apps/desktop/src-tauri/src/audio_library.rs b/apps/desktop/src-backend/src/audio_library.rs similarity index 96% rename from apps/desktop/src-tauri/src/audio_library.rs rename to apps/desktop/src-backend/src/audio_library.rs index 561b9673696..145d884799b 100644 --- a/apps/desktop/src-tauri/src/audio_library.rs +++ b/apps/desktop/src-backend/src/audio_library.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; +use cap_desktop_runtime::{AppHandle, Manager, path::BaseDirectory}; use cap_enc_ffmpeg::remux::get_media_duration; use serde::{Deserialize, Serialize}; use specta::Type; -use tauri::{AppHandle, Manager, path::BaseDirectory}; use crate::editor_window::WindowEditorInstance; @@ -46,7 +46,7 @@ pub struct ImportedAudioTrack { pub duration: f64, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub fn list_audio_library() -> Vec { AUDIO_LIBRARY @@ -65,7 +65,7 @@ fn probe_duration(path: &std::path::Path) -> f64 { .unwrap_or(0.0) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[tracing::instrument(skip(app, editor_instance))] pub async fn add_audio_library_track( @@ -114,7 +114,7 @@ pub async fn add_audio_library_track( .map_err(|e| format!("Audio import task failed: {e}"))? } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[tracing::instrument(skip(editor_instance))] pub async fn import_audio_track_file( diff --git a/apps/desktop/src-tauri/src/audio_meter.rs b/apps/desktop/src-backend/src/audio_meter.rs similarity index 97% rename from apps/desktop/src-tauri/src/audio_meter.rs rename to apps/desktop/src-backend/src/audio_meter.rs index 8070eb9e9b2..b6eaaf33ff8 100644 --- a/apps/desktop/src-tauri/src/audio_meter.rs +++ b/apps/desktop/src-backend/src/audio_meter.rs @@ -1,3 +1,5 @@ +use cap_desktop_runtime::AppHandle; +use cap_desktop_runtime::Event; use cap_recording::feeds::microphone::MicrophoneSamples; use cpal::{SampleFormat, StreamInstant}; use keyed_priority_queue::KeyedPriorityQueue; @@ -5,14 +7,12 @@ use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::VecDeque; use std::time::Duration; -use tauri::AppHandle; -use tauri_specta::Event; const MAX_AMPLITUDE_F32: f64 = (u16::MAX / 2) as f64; // i16 max value const ZERO_AMPLITUDE: u16 = 0; const MIN_DB: f64 = -96.0; -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct AudioInputLevelChange(f64); pub fn spawn_event_emitter( diff --git a/apps/desktop/src-tauri/src/auth.rs b/apps/desktop/src-backend/src/auth.rs similarity index 98% rename from apps/desktop/src-tauri/src/auth.rs rename to apps/desktop/src-backend/src/auth.rs index 5ae16506dba..5d5e38d60ad 100644 --- a/apps/desktop/src-tauri/src/auth.rs +++ b/apps/desktop/src-backend/src/auth.rs @@ -1,8 +1,7 @@ +use cap_desktop_runtime::{AppHandle, Runtime}; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; -use tauri::{AppHandle, Runtime}; -use tauri_plugin_store::StoreExt; use web_api::ManagerExt; diff --git a/apps/desktop/src-tauri/src/automation.rs b/apps/desktop/src-backend/src/automation.rs similarity index 97% rename from apps/desktop/src-tauri/src/automation.rs rename to apps/desktop/src-backend/src/automation.rs index a05f0a030f8..f9ccb67f817 100644 --- a/apps/desktop/src-tauri/src/automation.rs +++ b/apps/desktop/src-backend/src/automation.rs @@ -3,6 +3,7 @@ use cap_automation::{ Capability, CaptureTargetKind, ClipboardSource, ExportDestination, ExportFormat, ExportProfile, Trigger, TriggerContext, sanitize_filename_component, }; +use cap_desktop_runtime::{AppHandle, Manager, Wry}; use cap_recording::sources::screen_capture::ScreenCaptureTarget; use clipboard_rs::Clipboard; use clipboard_rs::common::RustImage; @@ -10,8 +11,6 @@ use serde_json::json; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; -use tauri::{AppHandle, Manager, Wry}; -use tauri_plugin_store::StoreExt; use tokio::sync::RwLock; use tracing::{error, info, warn}; @@ -210,7 +209,7 @@ impl AutomationHost for DesktopAutomationHost { existing.clone() } else if let Some(project_path) = ctx.project_path.as_ref() { info!(path = %project_path.display(), "Automation: uploading recording"); - let channel = tauri::ipc::Channel::new(|_| Ok(())); + let channel = cap_desktop_runtime::ipc::Channel::new(|_| Ok(())); let result = crate::upload_exported_video( self.app.clone(), project_path.clone(), @@ -265,8 +264,6 @@ impl AutomationHost for DesktopAutomationHost { } async fn open_file(&self, ctx: &TriggerContext) -> Result<(), String> { - use tauri_plugin_opener::OpenerExt; - let path = ctx .image_path .as_ref() @@ -275,9 +272,7 @@ impl AutomationHost for DesktopAutomationHost { let path_str = path.to_str().ok_or("Invalid path")?; self.app - .opener() - .open_path(path_str, None::) - .map_err(|e| format!("Failed to open file: {e}"))?; + .native_operation("opener.openPath", serde_json::json!({ "path": path_str }))?; Ok(()) } @@ -426,8 +421,6 @@ impl AutomationHost for DesktopAutomationHost { title_template: &str, body_template: &str, ) -> Result<(), String> { - use tauri_plugin_notification::NotificationExt; - let enabled = crate::general_settings::GeneralSettingsStore::get(&self.app) .map(|s| s.is_some_and(|s| s.enable_notifications)) .unwrap_or(false); @@ -439,13 +432,10 @@ impl AutomationHost for DesktopAutomationHost { let title = apply_body_template(&apply_filename_template(title_template, ctx), ctx); let body = apply_body_template(&apply_filename_template(body_template, ctx), ctx); - self.app - .notification() - .builder() - .title(title) - .body(body) - .show() - .map_err(|e| format!("Failed to send notification: {e}"))?; + self.app.native_operation( + "notification.show", + serde_json::json!({ "title": title, "body": body }), + )?; Ok(()) } @@ -823,13 +813,13 @@ pub fn get_store(app: &AppHandle) -> Result, Strin } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn get_automations(app: AppHandle) -> Result { Ok(get_store(&app)?.unwrap_or_default()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn set_automations(app: AppHandle, store: AutomationsStore) -> Result<(), String> { let tauri_store = app.store("store").map_err(|e| e.to_string())?; @@ -853,7 +843,7 @@ pub struct AutomationTestReport { pub action_checks: Vec, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn test_automation( app: AppHandle, @@ -901,7 +891,7 @@ pub async fn test_automation( }) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn automation_should_open_screenshot_editor( app: AppHandle, @@ -910,7 +900,7 @@ pub async fn automation_should_open_screenshot_editor( should_open_screenshot_editor(&app, &target) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn list_automation_capabilities() -> Vec { vec![ diff --git a/apps/desktop/src-tauri/src/camera.rs b/apps/desktop/src-backend/src/camera.rs similarity index 82% rename from apps/desktop/src-tauri/src/camera.rs rename to apps/desktop/src-backend/src/camera.rs index 111d9effeea..ec8d36e58cf 100644 --- a/apps/desktop/src-tauri/src/camera.rs +++ b/apps/desktop/src-backend/src/camera.rs @@ -9,6 +9,7 @@ use cap_utils::macos_qos::{MacOsQosClass, set_current_thread_qos}; #[cfg(target_os = "macos")] use crate::camera_native::{NativeFrameConverter, classify_frame}; +use cap_desktop_runtime::{LogicalPosition, LogicalSize, PhysicalSize, WebviewWindow}; use ffmpeg::{ format::{self, Pixel}, frame, @@ -24,7 +25,6 @@ use std::{ }, thread, }; -use tauri::{LogicalPosition, LogicalSize, PhysicalSize, WebviewWindow}; use tokio::{ runtime::Runtime, sync::{broadcast, oneshot}, @@ -263,7 +263,7 @@ fn camera_preview_texture_dimensions( } pub struct CameraPreviewManager { - store: Result>, String>, + store: Result, String>, store_save_generation: Arc, preview: Option, preview_session_id: Arc, @@ -271,10 +271,10 @@ pub struct CameraPreviewManager { } impl CameraPreviewManager { - pub fn new(app: &tauri::AppHandle) -> Self { + pub fn new(app: &cap_desktop_runtime::AppHandle) -> Self { Self { - store: tauri_plugin_store::StoreBuilder::new(app, "cameraPreview") - .build() + store: app + .store("cameraPreview") .map_err(|err| format!("Error initializing camera preview store: {err}")), store_save_generation: Arc::new(AtomicU64::new(0)), preview: None, @@ -311,7 +311,7 @@ impl CameraPreviewManager { let generation = self.store_save_generation.fetch_add(1, Ordering::AcqRel) + 1; let save_generation = self.store_save_generation.clone(); let store = store.clone(); - tauri::async_runtime::spawn(async move { + cap_desktop_runtime::async_runtime::spawn(async move { tokio::time::sleep(Duration::from_millis(150)).await; if save_generation.load(Ordering::Acquire) == generation && let Err(err) = store.save() @@ -532,283 +532,292 @@ impl InitializedCameraPreview { default_state: &CameraPreviewState, instance: wgpu::Instance, ) -> anyhow::Result { - let aspect = if default_state.shape == CameraPreviewShape::Full { - WIDE_CAMERA_ASPECT_RATIO - } else { - 1.0 - }; + #[cfg(any())] + { + let aspect = if default_state.shape == CameraPreviewShape::Full { + WIDE_CAMERA_ASPECT_RATIO + } else { + 1.0 + }; - let (window_width, window_height, surface_scale) = - resize_window(&window, default_state, aspect, false) - .await - .context("Error resizing Tauri window")?; + let (window_width, window_height, surface_scale) = + resize_window(&window, default_state, aspect, false) + .await + .context("Error resizing Tauri window")?; - let (tx, rx) = oneshot::channel(); - window - .run_on_main_thread({ - let window = window.clone(); - let instance = instance.clone(); - move || { - let surface = instance.create_surface(window.clone()); - tx.send(surface).ok(); - } - }) - .with_context(|| "Failed to initialize wgpu instance")?; + let (tx, rx) = oneshot::channel(); + window + .run_on_main_thread({ + let window = window.clone(); + let instance = instance.clone(); + move || { + let surface = instance.create_surface(window.clone()); + tx.send(surface).ok(); + } + }) + .with_context(|| "Failed to initialize wgpu instance")?; - let surface = rx - .await - .with_context(|| "Failed to receive initialized wgpu surface")?; - let surface = surface.with_context(|| "Failed to initialize wgpu surface")?; - - let force_software_adapter = cap_rendering::force_software_wgpu_adapter(); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - force_fallback_adapter: force_software_adapter, - compatible_surface: Some(&surface), - }) - .await - .with_context(|| "Failed to find an appropriate wgpu adapter")?; + let surface = rx + .await + .with_context(|| "Failed to receive initialized wgpu surface")?; + let surface = surface.with_context(|| "Failed to initialize wgpu surface")?; + + let force_software_adapter = cap_rendering::force_software_wgpu_adapter(); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::LowPower, + force_fallback_adapter: force_software_adapter, + compatible_surface: Some(&surface), + }) + .await + .with_context(|| "Failed to find an appropriate wgpu adapter")?; + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()), + memory_hints: Default::default(), + trace: wgpu::Trace::Off, + }) + .await + .with_context(|| "Failed to create wgpu device")?; - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()), - memory_hints: Default::default(), - trace: wgpu::Trace::Off, - }) - .await - .with_context(|| "Failed to create wgpu device")?; + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "./camera.wgsl" + ))), + }); - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: None, - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( - "./camera.wgsl" - ))), - }); + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Uniform Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Texture Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); - let uniform_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Uniform Bind Group Layout"), + let state_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("State Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let window_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Window Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let camera_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Camera Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Uniform Bind Group"), + layout: &uniform_bind_group_layout, entries: &[ - wgpu::BindGroupLayoutEntry { + wgpu::BindGroupEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, + resource: state_uniform_buffer.as_entire_binding(), }, - wgpu::BindGroupLayoutEntry { + wgpu::BindGroupEntry { binding: 1, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, + resource: window_uniform_buffer.as_entire_binding(), }, - wgpu::BindGroupLayoutEntry { + wgpu::BindGroupEntry { binding: 2, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, + resource: camera_uniform_buffer.as_entire_binding(), }, ], }); - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Texture Bind Group Layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - let state_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("State Uniform Buffer"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let window_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Window Uniform Buffer"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let camera_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Camera Uniform Buffer"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bind_group_layout, &uniform_bind_group_layout], + push_constant_ranges: &[], + }); - let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Uniform Bind Group"), - layout: &uniform_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: state_uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: window_uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: camera_uniform_buffer.as_entire_binding(), + let swapchain_format = wgpu::TextureFormat::Bgra8Unorm; + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: Default::default(), }, - ], - }); - - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[&bind_group_layout, &uniform_bind_group_layout], - push_constant_ranges: &[], - }); + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: swapchain_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: Default::default(), + multiview: None, + cache: None, + }); - let swapchain_format = wgpu::TextureFormat::Bgra8Unorm; - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: swapchain_format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: Default::default(), - multiview: None, - cache: None, - }); + let surface_capabilities = surface.get_capabilities(&adapter); + let alpha_mode = preferred_alpha_mode(&surface_capabilities.alpha_modes); - let surface_capabilities = surface.get_capabilities(&adapter); - let alpha_mode = preferred_alpha_mode(&surface_capabilities.alpha_modes); + let present_mode = if surface_capabilities + .present_modes + .contains(&wgpu::PresentMode::Mailbox) + { + wgpu::PresentMode::Mailbox + } else { + wgpu::PresentMode::Fifo + }; - let present_mode = if surface_capabilities - .present_modes - .contains(&wgpu::PresentMode::Mailbox) - { - wgpu::PresentMode::Mailbox - } else { - wgpu::PresentMode::Fifo - }; + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: window_width, + height: window_height, + present_mode, + alpha_mode, + view_formats: vec![], + desired_maximum_frame_latency: 1, + }; - let surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: swapchain_format, - width: window_width, - height: window_height, - present_mode, - alpha_mode, - view_formats: vec![], - desired_maximum_frame_latency: 1, - }; + surface.configure(&device, &surface_config); - surface.configure(&device, &surface_config); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); + let mut renderer = Renderer { + surface: Some(surface), + surface_config, + surface_scale, + render_pipeline, + device, + queue, + sampler, + bind_group_layout, + state_uniform_buffer, + window_uniform_buffer, + camera_uniform_buffer, + uniform_bind_group, + texture: Cached::default(), + aspect_ratio: Cached::default(), + blur_processor: None, + blur_processor_init_attempted: false, + blur_source_texture: None, + #[cfg(target_os = "macos")] + native_converter: None, + #[cfg(target_os = "macos")] + native_converter_init_attempted: false, + }; - let mut renderer = Renderer { - surface: Some(surface), - surface_config, - surface_scale, - render_pipeline, - device, - queue, - sampler, - bind_group_layout, - state_uniform_buffer, - window_uniform_buffer, - camera_uniform_buffer, - uniform_bind_group, - texture: Cached::default(), - aspect_ratio: Cached::default(), - blur_processor: None, - blur_processor_init_attempted: false, - blur_source_texture: None, - #[cfg(target_os = "macos")] - native_converter: None, - #[cfg(target_os = "macos")] - native_converter_init_attempted: false, - }; + renderer.update_state_uniforms(default_state); + renderer + .sync_ratio_uniform_and_resize_window_to_it(&window, default_state, aspect) + .await; + renderer.reconfigure_gpu_surface(window_width, window_height); - renderer.update_state_uniforms(default_state); - renderer - .sync_ratio_uniform_and_resize_window_to_it(&window, default_state, aspect) - .await; - renderer.reconfigure_gpu_surface(window_width, window_height); + let initial_surface = renderer.acquire_surface_texture(); + if let Some(surface) = initial_surface { + let output_width = 5; + let output_height = 5; - let initial_surface = renderer.acquire_surface_texture(); - if let Some(surface) = initial_surface { - let output_width = 5; - let output_height = 5; + let (buffer, stride) = + render_solid_frame([0x11, 0x11, 0x11, 0xFF], output_width, output_height); - let (buffer, stride) = - render_solid_frame([0x11, 0x11, 0x11, 0xFF], output_width, output_height); + PreparedTexture::init( + renderer.device.clone(), + renderer.queue.clone(), + &renderer.sampler, + &renderer.bind_group_layout, + renderer.uniform_bind_group.clone(), + renderer.render_pipeline.clone(), + output_width, + output_height, + ) + .render(&surface, &buffer, stride); + surface.present(); + } - PreparedTexture::init( - renderer.device.clone(), - renderer.queue.clone(), - &renderer.sampler, - &renderer.bind_group_layout, - renderer.uniform_bind_group.clone(), - renderer.render_pipeline.clone(), - output_width, - output_height, - ) - .render(&surface, &buffer, stride); - surface.present(); + return Ok(renderer); } - Ok(renderer) + let _ = (window, default_state, instance); + Err(anyhow!( + "Direct native-window camera rendering is unavailable under Electron" + )) } } @@ -1705,65 +1714,9 @@ async fn resize_window( .run_on_main_thread({ let window = window.clone(); move || { - let result: tauri::Result<(u32, u32, f64)> = (|| { + let result: cap_desktop_runtime::Result<(u32, u32, f64)> = (|| { if should_move { - let (monitor_size, monitor_offset, monitor_scale_factor): ( - PhysicalSize, - LogicalPosition, - _, - ) = if let Some(monitor) = window.current_monitor()? { - let size = monitor.position().to_logical(monitor.scale_factor()); - (*monitor.size(), size, monitor.scale_factor()) - } else { - (PhysicalSize::new(640, 360), LogicalPosition::new(0, 0), 1.0) - }; - - let x = (monitor_size.width as f64 / monitor_scale_factor - - window_width as f64 - - 100.0) as u32 - + monitor_offset.x; - let y = (monitor_size.height as f64 / monitor_scale_factor - - window_height as f64 - - 100.0) as u32 - + monitor_offset.y; - - window.set_position(LogicalPosition::new(x, y))?; - } else if let Some(monitor) = window.current_monitor()? { - let scale_factor = monitor.scale_factor(); - let monitor_pos = monitor.position().to_logical::(scale_factor); - let monitor_size = monitor.size().to_logical::(scale_factor); - - let current_pos = window - .outer_position() - .map(|p| p.to_logical::(scale_factor)) - .unwrap_or(monitor_pos); - - let mut new_x = current_pos.x; - let mut new_y = current_pos.y; - let new_width = window_width as f64; - let new_height = window_height as f64; - - if new_x + new_width > monitor_pos.x + monitor_size.width { - new_x = monitor_pos.x + monitor_size.width - new_width; - } - - if new_y + new_height > monitor_pos.y + monitor_size.height { - new_y = monitor_pos.y + monitor_size.height - new_height; - } - - if new_x < monitor_pos.x { - new_x = monitor_pos.x; - } - - if new_y < monitor_pos.y { - new_y = monitor_pos.y; - } - - if (new_x - current_pos.x).abs() > 1.0 - || (new_y - current_pos.y).abs() > 1.0 - { - window.set_position(LogicalPosition::new(new_x, new_y))?; - } + window.set_position(LogicalPosition::new(100.0, 100.0))?; } window.set_size(LogicalSize::new(window_width, window_height))?; @@ -1780,11 +1733,12 @@ async fn resize_window( tx.send(result).ok(); } }) + .map_err(anyhow::Error::msg) .context("Failed to run window resize on main thread")?; rx.await .context("Failed to receive window resize result")? - .map_err(anyhow::Error::new) + .map_err(anyhow::Error::msg) } #[cfg(test)] diff --git a/apps/desktop/src-tauri/src/camera.wgsl b/apps/desktop/src-backend/src/camera.wgsl similarity index 100% rename from apps/desktop/src-tauri/src/camera.wgsl rename to apps/desktop/src-backend/src/camera.wgsl diff --git a/apps/desktop/src-tauri/src/camera_legacy.rs b/apps/desktop/src-backend/src/camera_legacy.rs similarity index 100% rename from apps/desktop/src-tauri/src/camera_legacy.rs rename to apps/desktop/src-backend/src/camera_legacy.rs diff --git a/apps/desktop/src-tauri/src/camera_native.rs b/apps/desktop/src-backend/src/camera_native.rs similarity index 100% rename from apps/desktop/src-tauri/src/camera_native.rs rename to apps/desktop/src-backend/src/camera_native.rs diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-backend/src/captions.rs similarity index 99% rename from apps/desktop/src-tauri/src/captions.rs rename to apps/desktop/src-backend/src/captions.rs index 53cd4abdc44..8b5bef5e947 100644 --- a/apps/desktop/src-tauri/src/captions.rs +++ b/apps/desktop/src-backend/src/captions.rs @@ -1,5 +1,7 @@ use anyhow::Result; use cap_audio::AudioData; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager}; use ffmpeg::{ ChannelLayout, codec as avcodec, format::{self as avformat}, @@ -16,8 +18,6 @@ use std::io::Read; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use tauri::{AppHandle, Manager}; -use tauri_specta::Event; use tempfile::tempdir; use tokio::io::AsyncWriteExt; use tokio::sync::{Mutex, Notify}; @@ -196,14 +196,14 @@ fn validate_model_path(app: &AppHandle, path: &str) -> Result { resolve_path_with_base(&app_data_dir, path) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn create_dir(path: String, _recursive: bool) -> Result<(), String> { std::fs::create_dir_all(path).map_err(|e| format!("Failed to create directory: {e}")) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn save_model_file(path: String, data: Vec) -> Result<(), String> { @@ -1309,7 +1309,7 @@ fn process_with_parakeet( Err(PARAKEET_UNSUPPORTED_MESSAGE.to_string()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn transcribe_audio( @@ -1438,7 +1438,7 @@ pub async fn transcribe_audio( } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn save_captions( @@ -1841,7 +1841,7 @@ pub fn parse_captions_json(json: &str) -> Result Result Ok(captions_dir) } -#[derive(Debug, Serialize, Type, tauri_specta::Event, Clone)] +#[derive(Debug, Serialize, Type, cap_desktop_runtime::Event, Clone)] pub struct DownloadProgress { pub progress: f64, pub message: String, @@ -2046,7 +2046,7 @@ async fn clear_model_download_status(path: &Path) { let _ = downloads.remove(&key); } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn get_model_download_status( @@ -2059,7 +2059,7 @@ pub async fn get_model_download_status( Ok(downloads.get(&key).map(|entry| entry.status.clone())) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn download_whisper_model( @@ -2076,7 +2076,7 @@ pub async fn download_whisper_model( let download_app = app.clone(); let download_key = key.clone(); - tauri::async_runtime::spawn(async move { + cap_desktop_runtime::async_runtime::spawn(async move { let result = download_whisper_model_to_path( &download_app, &model_name, @@ -2217,7 +2217,7 @@ async fn total_content_length(client: &reqwest::Client, urls: &[&str]) -> u64 { total } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn check_model_exists(app: AppHandle, model_path: String) -> Result { @@ -2225,7 +2225,7 @@ pub async fn check_model_exists(app: AppHandle, model_path: String) -> Result Result<(), String> { @@ -2418,7 +2418,7 @@ fn finalize_parakeet_model_download( } #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn download_parakeet_model(app: AppHandle, output_dir: String) -> Result<(), String> { @@ -2431,7 +2431,7 @@ pub async fn download_parakeet_model(app: AppHandle, output_dir: String) -> Resu let download_app = app.clone(); let download_key = key.clone(); - tauri::async_runtime::spawn(async move { + cap_desktop_runtime::async_runtime::spawn(async move { let result = download_parakeet_model_to_dir(&download_app, &validated_dir, &download_key).await; let final_status = match &result { @@ -2565,7 +2565,7 @@ async fn download_parakeet_model_to_dir( } #[cfg(all(target_os = "macos", target_arch = "x86_64"))] -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(_app))] pub async fn download_parakeet_model(_app: AppHandle, _output_dir: String) -> Result<(), String> { @@ -2573,7 +2573,7 @@ pub async fn download_parakeet_model(_app: AppHandle, _output_dir: String) -> Re } #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn check_parakeet_model_exists( @@ -2597,7 +2597,7 @@ pub async fn check_parakeet_model_exists( } #[cfg(all(target_os = "macos", target_arch = "x86_64"))] -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(_app))] pub async fn check_parakeet_model_exists( @@ -2607,7 +2607,7 @@ pub async fn check_parakeet_model_exists( Ok(false) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn delete_parakeet_model(app: AppHandle, model_dir: String) -> Result<(), String> { @@ -2652,7 +2652,7 @@ fn format_srt_time(seconds: f64) -> String { format!("{hours:02}:{minutes:02}:{secs:02},{millis:03}") } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] pub async fn export_captions_srt( diff --git a/apps/desktop/src-tauri/src/cli.rs b/apps/desktop/src-backend/src/cli.rs similarity index 80% rename from apps/desktop/src-tauri/src/cli.rs rename to apps/desktop/src-backend/src/cli.rs index d258d170dd0..dca779ff244 100644 --- a/apps/desktop/src-tauri/src/cli.rs +++ b/apps/desktop/src-backend/src/cli.rs @@ -1,18 +1,18 @@ pub use cap_cli_install::CliInstallStatus; -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub fn get_cli_install_status() -> Result { cap_cli_install::status() } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub fn install_cli() -> Result { cap_cli_install::install() } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub fn uninstall_cli() -> Result { cap_cli_install::uninstall() diff --git a/apps/desktop/src-tauri/src/clip_thumbnails.rs b/apps/desktop/src-backend/src/clip_thumbnails.rs similarity index 99% rename from apps/desktop/src-tauri/src/clip_thumbnails.rs rename to apps/desktop/src-backend/src/clip_thumbnails.rs index bc898f7fc19..82c94ea7773 100644 --- a/apps/desktop/src-tauri/src/clip_thumbnails.rs +++ b/apps/desktop/src-backend/src/clip_thumbnails.rs @@ -11,7 +11,7 @@ const SEEK_DECODE_PACKET_LIMIT: usize = 240; static THUMBNAIL_SEMAPHORE: LazyLock = LazyLock::new(|| Semaphore::new(4)); -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[tracing::instrument(skip(editor_instance))] pub async fn get_clip_thumbnail( diff --git a/apps/desktop/src-tauri/src/crash_sentinel.rs b/apps/desktop/src-backend/src/crash_sentinel.rs similarity index 98% rename from apps/desktop/src-tauri/src/crash_sentinel.rs rename to apps/desktop/src-backend/src/crash_sentinel.rs index f394c8f9b65..51258a52539 100644 --- a/apps/desktop/src-tauri/src/crash_sentinel.rs +++ b/apps/desktop/src-backend/src/crash_sentinel.rs @@ -106,10 +106,10 @@ pub fn init(logs_dir: &Path, app_version: &str) -> Option let os = format!( "{} {}", - tauri_plugin_os::platform(), - tauri_plugin_os::version() + std::env::consts::OS, + sysinfo::System::long_os_version().unwrap_or_else(|| "unknown".to_string()) ); - let arch = tauri_plugin_os::arch().to_string(); + let arch = std::env::consts::ARCH.to_string(); let record = SessionRecord { pid: std::process::id(), diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-backend/src/deeplink_actions.rs similarity index 99% rename from apps/desktop/src-tauri/src/deeplink_actions.rs rename to apps/desktop/src-backend/src/deeplink_actions.rs index 6eb7f4c8939..70f7be70a37 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-backend/src/deeplink_actions.rs @@ -1,15 +1,15 @@ +use cap_desktop_runtime::{AppHandle, Manager, Url}; use cap_recording::{ RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget, }; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; -use tauri::{AppHandle, Manager, Url}; use tracing::trace; use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; #[cfg(debug_assertions)] -use tauri::Emitter; +use cap_desktop_runtime::Emitter; #[cfg(debug_assertions)] use crate::camera::CameraPreviewState; diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-backend/src/editor_window.rs similarity index 95% rename from apps/desktop/src-tauri/src/editor_window.rs rename to apps/desktop/src-backend/src/editor_window.rs index c1620a1b222..1330bc1eef5 100644 --- a/apps/desktop/src-tauri/src/editor_window.rs +++ b/apps/desktop/src-backend/src/editor_window.rs @@ -1,10 +1,10 @@ +use cap_desktop_runtime::{AppHandle, CommandContext, Listener, Manager, Window, ipc::CommandArg}; use std::{collections::HashMap, ops::Deref, path::PathBuf, sync::Arc, time::Instant}; -use tauri::{AppHandle, Listener, Manager, Runtime, Window, ipc::CommandArg}; use tokio::sync::{RwLock, watch}; use tokio_util::sync::CancellationToken; +use cap_desktop_runtime::Event; use cap_rendering::GpuOutputFormat; -use tauri_specta::Event; use crate::{ FrameLayoutEvent, create_editor_instance_impl, @@ -61,7 +61,7 @@ pub struct EditorInstance { pub ws_port: u16, pub ws_shutdown_token: CancellationToken, app_handle: AppHandle, - render_frame_event_id: tauri::EventId, + render_frame_event_id: cap_desktop_runtime::EventId, } type PendingResult = Result, String>; @@ -256,11 +256,11 @@ impl AsRef for WindowEditorInstance { } } -impl<'de, R: Runtime> CommandArg<'de, R> for WindowEditorInstance { +impl CommandArg for WindowEditorInstance { fn from_command( - command: tauri::ipc::CommandItem<'de, R>, - ) -> Result { - let window = Window::from_command(command)?; + context: &CommandContext, + ) -> Result { + let window = context.window(); let Some(instances) = window.try_state::() else { return Err("editor instance registry unavailable".into()); @@ -299,13 +299,11 @@ impl Deref for OptionalWindowEditorInstance { } } -impl<'de, R: Runtime> CommandArg<'de, R> for OptionalWindowEditorInstance { +impl CommandArg for OptionalWindowEditorInstance { fn from_command( - command: tauri::ipc::CommandItem<'de, R>, - ) -> Result { - let Ok(window) = Window::from_command(command) else { - return Ok(Self(None)); - }; + context: &CommandContext, + ) -> Result { + let window = context.window(); let Some(instances) = window.try_state::() else { return Ok(Self(None)); diff --git a/apps/desktop/src-tauri/src/exit_shutdown.rs b/apps/desktop/src-backend/src/exit_shutdown.rs similarity index 100% rename from apps/desktop/src-tauri/src/exit_shutdown.rs rename to apps/desktop/src-backend/src/exit_shutdown.rs diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-backend/src/export.rs similarity index 97% rename from apps/desktop/src-tauri/src/export.rs rename to apps/desktop/src-backend/src/export.rs index d0fbe410cb8..594207e64e9 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-backend/src/export.rs @@ -1,5 +1,6 @@ use crate::editor_window::{OptionalWindowEditorInstance, WindowEditorInstance}; use crate::{FramesRendered, get_video_metadata}; +use cap_desktop_runtime::Manager; use cap_export::{ExporterBase, make_cursor_only_project}; use cap_project::{RecordingMeta, TimelineFrameMapping, XY}; use cap_rendering::{ @@ -22,8 +23,6 @@ use std::{ atomic::{AtomicBool, AtomicUsize, Ordering}, }, }; -use tauri::Manager; -use tauri_plugin_dialog::DialogExt; use tokio::io::AsyncBufReadExt; use tokio_util::sync::CancellationToken; use tracing::{error, info, instrument, warn}; @@ -177,10 +176,10 @@ impl ExportWorkerMode { } #[derive(Clone)] -struct ExportProgress(tauri::ipc::Channel); +struct ExportProgress(cap_desktop_runtime::ipc::Channel); struct ExportSaveDialogRequest { - app: tauri::AppHandle, + app: cap_desktop_runtime::AppHandle, file_name: String, file_type: String, } @@ -337,17 +336,17 @@ pub fn cancel_all_exports() { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub fn cancel_export(export_id: String) -> bool { cancel_export_by_id(&export_id) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(window))] -pub fn cancel_current_window_exports(window: tauri::Window) { +pub fn cancel_current_window_exports(window: cap_desktop_runtime::Window) { cancel_exports_for_window(window.label()); } @@ -441,15 +440,15 @@ fn release_all_window_export_sessions() -> usize { count } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] -pub fn begin_export_session(window: tauri::Window) { +pub fn begin_export_session(window: cap_desktop_runtime::Window) { retain_export_session_for_window(window.label()); } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] -pub fn end_export_session(window: tauri::Window) { +pub fn end_export_session(window: cap_desktop_runtime::Window) { release_export_session_for_window(window.label()); } @@ -711,12 +710,12 @@ fn resolve_exporter_binary() -> Result { } Err(format!( - "Export worker binary not found; place {} next to the app executable or build the Tauri sidecar bundle", + "Export worker binary not found; place {} next to the app executable or build the Electron sidecar bundle", exporter_bin_name() )) } -/// Dev-loop trap: `tauri dev` rebuilds the app but not the exporter sidecar, +/// Dev-loop trap: the frontend dev server rebuilds the app but not the exporter sidecar, /// so exports can silently run renderer code from days earlier and disagree /// with the preview. Debug builds log loudly when that is happening. fn warn_if_exporter_stale(app_exe: &Path, exporter: &Path) { @@ -748,7 +747,7 @@ fn exporter_binary_candidates(root: &Path) -> Vec { candidates.push( root.join("apps") .join("desktop") - .join("src-tauri") + .join("src-backend") .join("binaries") .join(exporter_bin_name()), ); @@ -757,7 +756,7 @@ fn exporter_binary_candidates(root: &Path) -> Vec { candidates.push( root.join("apps") .join("desktop") - .join("src-tauri") + .join("src-backend") .join("binaries") .join(format!( "cap-exporter-{target_triple}{}", @@ -1041,13 +1040,13 @@ fn should_use_out_of_process_export() -> bool { cfg!(any(target_os = "macos", target_os = "windows")) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(window, progress, editor))] pub async fn export_video( - window: tauri::Window, + window: cap_desktop_runtime::Window, project_path: PathBuf, - progress: tauri::ipc::Channel, + progress: cap_desktop_runtime::ipc::Channel, settings: ExportSettings, editor: OptionalWindowEditorInstance, ) -> Result { @@ -1067,13 +1066,13 @@ pub async fn export_video( .await } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(window, progress, editor))] pub async fn export_video_with_id( - window: tauri::Window, + window: cap_desktop_runtime::Window, project_path: PathBuf, - progress: tauri::ipc::Channel, + progress: cap_desktop_runtime::ipc::Channel, settings: ExportSettings, export_id: String, editor: OptionalWindowEditorInstance, @@ -1093,13 +1092,13 @@ pub async fn export_video_with_id( .await } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(window, progress, editor))] pub async fn export_video_to_file( - window: tauri::Window, + window: cap_desktop_runtime::Window, project_path: PathBuf, - progress: tauri::ipc::Channel, + progress: cap_desktop_runtime::ipc::Channel, settings: ExportSettings, file_name: String, file_type: String, @@ -1267,7 +1266,7 @@ async fn export_video_inner( } async fn show_export_save_dialog( - app: &tauri::AppHandle, + app: &cap_desktop_runtime::AppHandle, file_name: String, file_type: String, ) -> Result, String> { @@ -1285,17 +1284,17 @@ async fn show_export_save_dialog( info!(file_name, name, extension, "Showing save file dialog"); - let (tx, rx) = tokio::sync::oneshot::channel(); - app.dialog() - .file() - .set_title("Save File") - .set_file_name(file_name) - .add_filter(name, &[extension]) - .save_file(move |path| { - let _ = tx.send(path.and_then(|p| p.as_path().map(PathBuf::from))); - }); - - rx.await.map_err(|e| e.to_string()).inspect(|result| { + app.native_request::, _>( + "dialog.save", + serde_json::json!({ + "title": "Save File", + "defaultPath": file_name, + "filters": [{ "name": name, "extensions": [extension] }] + }), + ) + .await + .map(|path| path.map(PathBuf::from)) + .inspect(|result| { info!(path = ?result, "Save file dialog completed"); }) } @@ -1340,7 +1339,7 @@ pub struct ExportEstimates { pub estimated_size_mb: f64, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn get_export_estimates( @@ -1448,7 +1447,7 @@ fn bpp_to_jpeg_quality(bpp: f32) -> u8 { ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8 } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip_all)] pub async fn generate_export_preview( @@ -1809,7 +1808,7 @@ mod tests { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip_all)] pub async fn generate_export_preview_fast( diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-backend/src/fake_window.rs similarity index 92% rename from apps/desktop/src-tauri/src/fake_window.rs rename to apps/desktop/src-backend/src/fake_window.rs index ec5b011cb06..8640de64024 100644 --- a/apps/desktop/src-tauri/src/fake_window.rs +++ b/apps/desktop/src-backend/src/fake_window.rs @@ -1,3 +1,4 @@ +use cap_desktop_runtime::{AppHandle, Manager, WebviewWindow}; use cap_recording::sources::screen_capture::ScreenCaptureTarget; use scap_targets::{Display, DisplayId, Window as ScapWindow, bounds::LogicalBounds}; use std::{ @@ -8,7 +9,6 @@ use std::{ }, time::Duration, }; -use tauri::{AppHandle, Manager, WebviewWindow}; use tokio::{sync::RwLock, time::sleep}; use tokio_util::sync::CancellationToken; use tracing::{debug, instrument}; @@ -82,14 +82,14 @@ impl FakeWindowListeners { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(state))] pub async fn set_fake_window_bounds( - window: tauri::Window, + window: cap_desktop_runtime::Window, name: String, bounds: LogicalBounds, - state: tauri::State<'_, FakeWindowBounds>, + state: cap_desktop_runtime::State<'_, FakeWindowBounds>, ) -> Result<(), String> { let mut state = state.0.write().await; let map = state.entry(window.label().to_string()).or_default(); @@ -99,13 +99,13 @@ pub async fn set_fake_window_bounds( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(state, window))] pub async fn remove_fake_window( - window: tauri::Window, + window: cap_desktop_runtime::Window, name: String, - state: tauri::State<'_, FakeWindowBounds>, + state: cap_desktop_runtime::State<'_, FakeWindowBounds>, ) -> Result<(), String> { let mut state = state.0.write().await; let Some(map) = state.get_mut(window.label()) else { @@ -122,8 +122,8 @@ pub async fn remove_fake_window( } fn should_ignore_cursor_events( - window_position: tauri::PhysicalPosition, - mouse_position: tauri::PhysicalPosition, + window_position: cap_desktop_runtime::PhysicalPosition, + mouse_position: cap_desktop_runtime::PhysicalPosition, scale_factor: f64, windows: &HashMap, default_ignore: bool, @@ -171,7 +171,7 @@ fn should_ignore_cursor_events( } fn recording_controls_size_allows_default_interaction( - window_size: tauri::PhysicalSize, + window_size: cap_desktop_runtime::PhysicalSize, scale_factor: f64, ) -> bool { let scale_factor = scale_factor.max(1.0); @@ -198,7 +198,7 @@ fn prepare_recording_controls_default_interaction(window: &WebviewWindow) -> boo return false; } - let _ = window.set_size(tauri::LogicalSize::new( + let _ = window.set_size(cap_desktop_runtime::LogicalSize::new( RECORDING_CONTROLS_WIDTH, RECORDING_CONTROLS_HEIGHT, )); @@ -536,8 +536,8 @@ mod tests { #[test] fn recording_controls_without_bounds_are_interactive() { assert!(!should_ignore_cursor_events( - tauri::PhysicalPosition::new(100, 200), - tauri::PhysicalPosition::new(150.0, 240.0), + cap_desktop_runtime::PhysicalPosition::new(100, 200), + cap_desktop_runtime::PhysicalPosition::new(150.0, 240.0), 2.0, &HashMap::new(), false, @@ -548,8 +548,8 @@ mod tests { #[test] fn recording_controls_without_bounds_pass_through_when_window_is_large() { assert!(should_ignore_cursor_events( - tauri::PhysicalPosition::new(0, 0), - tauri::PhysicalPosition::new(150.0, 240.0), + cap_desktop_runtime::PhysicalPosition::new(0, 0), + cap_desktop_runtime::PhysicalPosition::new(150.0, 240.0), 2.0, &HashMap::new(), false, @@ -560,8 +560,8 @@ mod tests { #[test] fn non_recording_windows_without_bounds_pass_through() { assert!(should_ignore_cursor_events( - tauri::PhysicalPosition::new(100, 200), - tauri::PhysicalPosition::new(150.0, 240.0), + cap_desktop_runtime::PhysicalPosition::new(100, 200), + cap_desktop_runtime::PhysicalPosition::new(150.0, 240.0), 2.0, &HashMap::new(), true, @@ -575,8 +575,8 @@ mod tests { windows.insert("controls".to_string(), bounds(10.0, 20.0, 100.0, 40.0)); assert!(!should_ignore_cursor_events( - tauri::PhysicalPosition::new(100, 200), - tauri::PhysicalPosition::new(150.0, 250.0), + cap_desktop_runtime::PhysicalPosition::new(100, 200), + cap_desktop_runtime::PhysicalPosition::new(150.0, 250.0), 2.0, &windows, false, @@ -590,8 +590,8 @@ mod tests { windows.insert("controls".to_string(), bounds(10.0, 20.0, 100.0, 40.0)); assert!(should_ignore_cursor_events( - tauri::PhysicalPosition::new(100, 200), - tauri::PhysicalPosition::new(50.0, 250.0), + cap_desktop_runtime::PhysicalPosition::new(100, 200), + cap_desktop_runtime::PhysicalPosition::new(50.0, 250.0), 2.0, &windows, false, @@ -605,8 +605,8 @@ mod tests { windows.insert("controls".to_string(), bounds(10.0, 20.0, 0.0, 40.0)); assert!(!should_ignore_cursor_events( - tauri::PhysicalPosition::new(100, 200), - tauri::PhysicalPosition::new(50.0, 250.0), + cap_desktop_runtime::PhysicalPosition::new(100, 200), + cap_desktop_runtime::PhysicalPosition::new(50.0, 250.0), 2.0, &windows, false, @@ -620,8 +620,8 @@ mod tests { windows.insert("controls".to_string(), bounds(0.0, 0.0, 1440.0, 900.0)); assert!(should_ignore_cursor_events( - tauri::PhysicalPosition::new(0, 0), - tauri::PhysicalPosition::new(150.0, 240.0), + cap_desktop_runtime::PhysicalPosition::new(0, 0), + cap_desktop_runtime::PhysicalPosition::new(150.0, 240.0), 2.0, &windows, false, @@ -632,11 +632,11 @@ mod tests { #[test] fn recording_controls_default_interaction_is_size_limited() { assert!(recording_controls_size_allows_default_interaction( - tauri::PhysicalSize::new(640, 300), + cap_desktop_runtime::PhysicalSize::new(640, 300), 2.0, )); assert!(!recording_controls_size_allows_default_interaction( - tauri::PhysicalSize::new(1920, 1080), + cap_desktop_runtime::PhysicalSize::new(1920, 1080), 2.0, )); } diff --git a/apps/desktop/src-backend/src/flags.rs b/apps/desktop/src-backend/src/flags.rs new file mode 100644 index 00000000000..8b7233ea640 --- /dev/null +++ b/apps/desktop/src-backend/src/flags.rs @@ -0,0 +1,3 @@ +pub fn value() -> serde_json::Value { + serde_json::to_value(&cap_flags::FLAGS).unwrap_or_default() +} diff --git a/apps/desktop/src-tauri/src/frame_ws.rs b/apps/desktop/src-backend/src/frame_ws.rs similarity index 100% rename from apps/desktop/src-tauri/src/frame_ws.rs rename to apps/desktop/src-backend/src/frame_ws.rs diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-backend/src/general_settings.rs similarity index 98% rename from apps/desktop/src-tauri/src/general_settings.rs rename to apps/desktop/src-backend/src/general_settings.rs index 8ea2ce2fd9c..ab38f11df4e 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-backend/src/general_settings.rs @@ -1,14 +1,13 @@ use crate::updates::UpdateChannel; use crate::window_exclusion::WindowExclusion; +#[cfg(target_os = "macos")] +use cap_desktop_runtime::Listener; +use cap_desktop_runtime::{AppHandle, Manager, Wry}; use scap_targets::DisplayId; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; use std::collections::BTreeMap; -#[cfg(target_os = "macos")] -use tauri::Listener; -use tauri::{AppHandle, Manager, Wry}; -use tauri_plugin_store::StoreExt; use tracing::{error, instrument}; use uuid::Uuid; @@ -84,7 +83,10 @@ pub fn default_studio_recording_quality() -> StudioRecordingQuality { } impl MainWindowRecordingStartBehaviour { - pub fn perform(&self, window: &tauri::WebviewWindow) -> tauri::Result<()> { + pub fn perform( + &self, + window: &cap_desktop_runtime::WebviewWindow, + ) -> cap_desktop_runtime::Result<()> { match self { Self::Close => { // On Windows, hide() leaves the DirectComposition surface composited on screen as @@ -573,7 +575,7 @@ fn bundled_muxer_bin_name() -> &'static str { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub fn get_default_excluded_windows() -> Vec { diff --git a/apps/desktop/src-tauri/src/gpu_context.rs b/apps/desktop/src-backend/src/gpu_context.rs similarity index 100% rename from apps/desktop/src-tauri/src/gpu_context.rs rename to apps/desktop/src-backend/src/gpu_context.rs diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-backend/src/hotkeys.rs similarity index 75% rename from apps/desktop/src-tauri/src/hotkeys.rs rename to apps/desktop/src-backend/src/hotkeys.rs index 2c9a6eed18c..4a91eb75577 100644 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ b/apps/desktop/src-backend/src/hotkeys.rs @@ -4,48 +4,48 @@ use crate::{ tray, windows::ShowCapWindow, }; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager}; use cap_recording::feeds::microphone::MicrophoneFeed; use cap_recording::screen_capture::ScreenCaptureTarget; -use global_hotkey::HotKeyState; use serde::{Deserialize, Serialize}; use specta::Type; use std::collections::HashMap; use std::sync::Mutex; -use tauri::{AppHandle, Manager}; -use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; -use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut}; -use tauri_plugin_store::StoreExt; -use tauri_specta::Event; use tracing::instrument; -#[derive(Serialize, Deserialize, Type, PartialEq, Clone, Copy, Debug)] +#[derive(Serialize, Deserialize, Type, PartialEq, Clone, Debug)] pub struct Hotkey { #[specta(type = String)] - code: Code, + code: String, meta: bool, ctrl: bool, alt: bool, shift: bool, } -impl From for Shortcut { - fn from(hotkey: Hotkey) -> Self { - let mut modifiers = Modifiers::empty(); - - if hotkey.meta { - modifiers |= Modifiers::META; +impl Hotkey { + fn accelerator(&self) -> String { + let mut parts = Vec::new(); + if self.meta { + parts.push("CommandOrControl".to_string()); } - if hotkey.ctrl { - modifiers |= Modifiers::CONTROL; + if self.ctrl && !self.meta { + parts.push("Control".to_string()); } - if hotkey.alt { - modifiers |= Modifiers::ALT; + if self.alt { + parts.push("Alt".to_string()); } - if hotkey.shift { - modifiers |= Modifiers::SHIFT; + if self.shift { + parts.push("Shift".to_string()); } - - Shortcut::new(Some(modifiers), hotkey.code) + let key = self + .code + .strip_prefix("Key") + .or_else(|| self.code.strip_prefix("Digit")) + .unwrap_or(&self.code); + parts.push(key.to_string()); + parts.join("+") } } @@ -85,7 +85,7 @@ impl HotkeysStore { } } -#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] +#[derive(Serialize, Type, cap_desktop_runtime::Event, Debug, Clone)] pub struct OnEscapePress; pub type HotkeysState = Mutex; @@ -139,20 +139,18 @@ async fn confirm_direct_recording_without_microphone(app: &AppHandle) -> bool { return true; } - let (sender, receiver) = tokio::sync::oneshot::channel(); - app.dialog() - .message("This recording will not include your voice.") - .title("No microphone detected") - .kind(MessageDialogKind::Warning) - .buttons(MessageDialogButtons::OkCancelCustom( - "Record without microphone".to_string(), - "Cancel".to_string(), - )) - .show(move |confirmed| { - let _ = sender.send(confirmed); - }); - - receiver.await.unwrap_or(false) + app.native_request::( + "dialog.confirm", + serde_json::json!({ + "message": "This recording will not include your voice.", + "title": "No microphone detected", + "kind": "warning", + "okLabel": "Record without microphone", + "cancelLabel": "Cancel" + }), + ) + .await + .unwrap_or(false) } async fn start_recording_from_hotkey( @@ -177,37 +175,6 @@ async fn start_recording_from_hotkey( } pub fn init(app: &AppHandle) { - app.plugin( - tauri_plugin_global_shortcut::Builder::new() - .with_handler(|app, shortcut, event| { - if !matches!(event.state(), HotKeyState::Pressed) { - return; - } - - if shortcut.key == Code::Escape { - OnEscapePress.emit(app).ok(); - } - - if shortcut.key == Code::Comma && shortcut.mods == Modifiers::META { - let app = app.clone(); - tokio::spawn(async move { - let _ = ShowCapWindow::Settings { page: None }.show(&app).await; - }); - } - - let state = app.state::(); - let store = state.lock().unwrap(); - - for (action, hotkey) in &store.hotkeys { - if &Shortcut::from(*hotkey) == shortcut { - tokio::spawn(handle_hotkey(app.clone(), *action)); - } - } - }) - .build(), - ) - .unwrap(); - let store = match HotkeysStore::get(app) { Ok(Some(s)) => s, Ok(None) => HotkeysStore::default(), @@ -217,12 +184,46 @@ pub fn init(app: &AppHandle) { } }; - let global_shortcut = app.global_shortcut(); - for hotkey in store.hotkeys.values() { - global_shortcut.register(Shortcut::from(*hotkey)).ok(); - } - app.manage(Mutex::new(store)); + sync_electron_hotkeys(app); + + let app_handle = app.clone(); + app.listen("hotkey://trigger", move |event| { + let Ok(action) = serde_json::from_str::(event.payload()) else { + return; + }; + tokio::spawn(handle_hotkey(app_handle.clone(), action)); + }); + let app_handle = app.clone(); + app.listen("hotkey://escape", move |_| { + OnEscapePress.emit(&app_handle).ok(); + }); + let app_handle = app.clone(); + app.listen("hotkey://settings", move |_| { + let app = app_handle.clone(); + tokio::spawn(async move { + let _ = ShowCapWindow::Settings { page: None }.show(&app).await; + }); + }); +} + +fn sync_electron_hotkeys(app: &AppHandle) { + let state = app.state::(); + let store = state.lock().unwrap(); + let hotkeys = store + .hotkeys + .iter() + .map(|(action, hotkey)| { + serde_json::json!({ + "action": action, + "accelerator": hotkey.accelerator() + }) + }) + .collect::>(); + let _ = app.native_operation( + "hotkeys.configure", + serde_json::json!({ "hotkeys": hotkeys }), + ); } async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), String> { @@ -336,32 +337,21 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin } } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] #[instrument(skip(app))] pub fn set_hotkey(app: AppHandle, action: HotkeyAction, hotkey: Option) -> Result<(), ()> { - let global_shortcut = app.global_shortcut(); let state = app.state::(); let mut store = state.lock().unwrap(); - let prev = store.hotkeys.get(&action).cloned(); - if let Some(hotkey) = hotkey { store.hotkeys.insert(action, hotkey); } else { store.hotkeys.remove(&action); } - if let Some(prev) = prev - && !store.hotkeys.values().any(|h| h == &prev) - { - global_shortcut.unregister(Shortcut::from(prev)).ok(); - } - - if let Some(hotkey) = hotkey { - global_shortcut.register(Shortcut::from(hotkey)).ok(); - } - + drop(store); + sync_electron_hotkeys(&app); Ok(()) } diff --git a/apps/desktop/src-tauri/src/http_client.rs b/apps/desktop/src-backend/src/http_client.rs similarity index 100% rename from apps/desktop/src-tauri/src/http_client.rs rename to apps/desktop/src-backend/src/http_client.rs diff --git a/apps/desktop/src-tauri/src/import.rs b/apps/desktop/src-backend/src/import.rs similarity index 99% rename from apps/desktop/src-tauri/src/import.rs rename to apps/desktop/src-backend/src/import.rs index 94b4b89f722..bad30206b44 100644 --- a/apps/desktop/src-tauri/src/import.rs +++ b/apps/desktop/src-backend/src/import.rs @@ -1,3 +1,5 @@ +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager, Window}; use cap_enc_ffmpeg::{ AudioEncoder, h264::{H264EncoderBuilder, H264Preset}, @@ -25,8 +27,6 @@ use std::{ path::{Path, PathBuf}, str::FromStr, }; -use tauri::{AppHandle, Manager, Window}; -use tauri_specta::Event; use tracing::{debug, error, info}; use crate::{ @@ -52,7 +52,7 @@ pub enum ImportStage { Failed, } -#[derive(Serialize, Deserialize, Type, tauri_specta::Event, Clone, Debug)] +#[derive(Serialize, Deserialize, Type, cap_desktop_runtime::Event, Clone, Debug)] pub struct VideoImportProgress { pub project_path: String, pub stage: ImportStage, @@ -1356,7 +1356,7 @@ fn transcode_video( Ok((fps, sample_rate)) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn start_video_import(app: AppHandle, source_path: PathBuf) -> Result { info!("Starting video import from: {:?}", source_path); @@ -1865,7 +1865,7 @@ async fn append_cap_project_to_editor_project( Ok(copied_segments.len()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn add_existing_recording_to_editor( window: Window, @@ -1894,7 +1894,7 @@ pub async fn add_existing_recording_to_editor( Ok(imported_count) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn start_image_import(app: AppHandle, source_path: PathBuf) -> Result { info!("Starting image import from: {:?}", source_path); @@ -2016,7 +2016,7 @@ pub async fn start_image_import(app: AppHandle, source_path: PathBuf) -> Result< Ok(image_path) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn check_import_ready(project_path: PathBuf) -> Result { debug!("check_import_ready called for: {:?}", project_path); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-backend/src/lib.rs similarity index 90% rename from apps/desktop/src-tauri/src/lib.rs rename to apps/desktop/src-backend/src/lib.rs index 9c58541d522..cc48574d3a4 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-backend/src/lib.rs @@ -1,4 +1,5 @@ #![recursion_limit = "256"] +#![allow(dead_code, unused_imports)] mod api; mod audio; @@ -86,6 +87,11 @@ use screenshot_editor::{ }; mod gpu_context; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::Listener; +use cap_desktop_runtime::{ + AppHandle, Emitter, Manager, PhysicalPosition, State, Window, WindowEvent, ipc::Channel, +}; pub use gpu_context::{PendingScreenshot, PendingScreenshots}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -103,15 +109,6 @@ use std::{ }, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; -use tauri::Listener; -use tauri::{AppHandle, Emitter, Manager, State, Window, WindowEvent, ipc::Channel}; -use tauri_plugin_deep_link::DeepLinkExt; -use tauri_plugin_dialog::DialogExt; -use tauri_plugin_global_shortcut::GlobalShortcutExt; -use tauri_plugin_notification::{NotificationExt, PermissionState}; -use tauri_plugin_opener::OpenerExt; -use tauri_plugin_shell::ShellExt; -use tauri_specta::Event; use tokio::sync::{Mutex, RwLock, oneshot, watch}; use tracing::*; use upload::{create_or_get_video, upload_screenshot_bytes, upload_screenshot_file, upload_video}; @@ -129,16 +126,16 @@ use crate::{ recording_settings::{RecordingSettingsStore, RecordingTargetMode}, upload::InstantMultipartUpload, }; +#[cfg(any())] +use cap_desktop_runtime::menu::{ + AboutMetadata, HELP_SUBMENU_ID, Menu, MenuItem, PredefinedMenuItem, Submenu, WINDOW_SUBMENU_ID, +}; use exit_shutdown::{ AppExitAction, ExitRequestDecision, app_exit_action, collect_device_inventory, handle_exit_requested, run_while_active, }; use futures::FutureExt; use std::panic::AssertUnwindSafe; -#[cfg(target_os = "macos")] -use tauri::menu::{ - AboutMetadata, HELP_SUBMENU_ID, Menu, MenuItem, PredefinedMenuItem, Submenu, WINDOW_SUBMENU_ID, -}; type FinalizingRecordingsMap = std::collections::HashMap, watch::Receiver)>; @@ -476,8 +473,10 @@ fn should_show_onboarding(app: &AppHandle) -> bool { || !permissions::do_permissions_check(false).necessary_granted() } -#[cfg(target_os = "macos")] -fn build_macos_app_menu(app_handle: &AppHandle) -> tauri::Result> { +#[cfg(any())] +fn build_macos_app_menu( + app_handle: &AppHandle, +) -> cap_desktop_runtime::Result> { let pkg_info = app_handle.package_info(); let config = app_handle.config(); let about_metadata = AboutMetadata { @@ -1101,7 +1100,7 @@ impl App { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(state))] async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { @@ -1239,19 +1238,19 @@ async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> R } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] async fn upload_logs(app_handle: AppHandle) -> Result<(), String> { logging::upload_log_file(&app_handle).await } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] fn get_system_diagnostics() -> cap_recording::diagnostics::SystemDiagnostics { cap_recording::diagnostics::collect_diagnostics() } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app_handle, state))] #[allow(unused_mut)] @@ -1512,7 +1511,7 @@ async fn set_camera_input( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app_handle, state))] async fn set_native_camera_preview_enabled( @@ -1722,14 +1721,14 @@ fn spawn_device_watchers(app_handle: AppHandle) { spawn_camera_watcher(app_handle); } -#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] +#[derive(Serialize, Type, cap_desktop_runtime::Event, Debug, Clone)] pub struct DevicesUpdated { cameras: Vec, microphones: Vec, permissions: permissions::OSPermissionsCheck, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] async fn get_devices_snapshot() -> DevicesUpdated { let permissions = permissions::do_permissions_check(false); @@ -1882,7 +1881,7 @@ fn spawn_system_resume_detector(app_handle: AppHandle) { } pub(crate) fn schedule_resume_recovery(app_handle: AppHandle) { - tauri::async_runtime::spawn(async move { + cap_desktop_runtime::async_runtime::spawn(async move { if app_is_exiting(&app_handle) { return; } @@ -2399,72 +2398,72 @@ pub(crate) fn is_camera_available(id: &DeviceOrModelID) -> bool { }) } -#[derive(specta::Type, Serialize, tauri_specta::Event, Clone)] +#[derive(specta::Type, Serialize, cap_desktop_runtime::Event, Clone)] pub struct RecordingOptionsChanged; -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct NewStudioRecordingAdded { path: PathBuf, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct EditorRecordingAdded { pub editor_path: PathBuf, pub recording_path: PathBuf, } -#[derive(Deserialize, specta::Type, tauri_specta::Event, Debug, Clone, Serialize)] +#[derive(Deserialize, specta::Type, cap_desktop_runtime::Event, Debug, Clone, Serialize)] pub struct RecordingDeleted { path: PathBuf, } -#[derive(specta::Type, tauri_specta::Event, Serialize)] +#[derive(specta::Type, cap_desktop_runtime::Event, Serialize)] pub struct SetCaptureAreaPending(bool); -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct NewScreenshotAdded { path: PathBuf, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RecordingStarted; -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RecordingStopped; -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RequestStartRecording { pub mode: RecordingMode, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RequestOpenRecordingPicker { pub target_mode: Option, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RequestSetTargetMode { pub target_mode: Option, pub display_id: Option, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RequestOpenSettings { page: String, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RequestScrollToSettingsSection { pub section: String, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct RequestScreenCapturePrewarm { #[serde(default)] pub force: bool, } -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, Serialize, cap_desktop_runtime::Event, Debug, Clone)] pub struct NewNotification { title: String, body: String, @@ -2532,7 +2531,7 @@ struct CurrentRecording { status: RecordingStatus, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(state))] async fn get_current_recording( @@ -2576,7 +2575,7 @@ async fn get_current_recording( }))) } -#[derive(Serialize, Type, tauri_specta::Event, Clone)] +#[derive(Serialize, Type, cap_desktop_runtime::Event, Clone)] pub struct CurrentRecordingChanged; pub(crate) async fn create_screenshot( @@ -2753,7 +2752,7 @@ fn find_first_segment(dir: &std::path::Path) -> Option { // .map_err(|e| format!("Task join error: {e}"))? // } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(), String> { @@ -2888,7 +2887,7 @@ pub fn is_valid_video(path: &std::path::Path) -> bool { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(clipboard))] async fn copy_screenshot_to_clipboard( @@ -2907,7 +2906,7 @@ async fn copy_screenshot_to_clipboard( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(clipboard, data))] async fn copy_image_to_clipboard( @@ -2926,7 +2925,7 @@ async fn copy_image_to_clipboard( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(clipboard, instance))] async fn copy_rendered_screenshot_to_clipboard( @@ -2945,7 +2944,7 @@ async fn copy_rendered_screenshot_to_clipboard( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(_app))] async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { @@ -3006,21 +3005,21 @@ async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { Ok(()) } -#[derive(Deserialize, specta::Type, tauri_specta::Event, Debug, Clone)] +#[derive(Deserialize, specta::Type, cap_desktop_runtime::Event, Debug, Clone)] struct RenderFrameEvent { frame_number: u32, fps: u32, resolution_base: XY, } -#[derive(Serialize, specta::Type, tauri_specta::Event, Debug, Clone)] +#[derive(Serialize, specta::Type, cap_desktop_runtime::Event, Debug, Clone)] struct EditorStateChanged { playhead_position: u32, } /// Rendered display/camera placement of the latest preview frame, in /// output-frame pixels — consumed by the editor's on-canvas layout overlay. -#[derive(Serialize, specta::Type, tauri_specta::Event, Debug, Clone, PartialEq)] +#[derive(Serialize, specta::Type, cap_desktop_runtime::Event, Debug, Clone, PartialEq)] pub(crate) struct FrameLayoutEvent { display: [f32; 4], camera: Option<[f32; 4]>, @@ -3047,7 +3046,7 @@ impl EditorStateChanged { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor_instance))] async fn start_playback( @@ -3060,7 +3059,7 @@ async fn start_playback( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor_instance))] async fn stop_playback(editor_instance: WindowEditorInstance) -> Result<(), String> { @@ -3087,7 +3086,7 @@ struct SerializedEditorInstance { notch_base: cap_project::DisplayNotch, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(window))] async fn create_editor_instance(window: Window) -> Result { @@ -3126,7 +3125,7 @@ async fn create_editor_instance(window: Window) -> Result Result { @@ -3146,7 +3145,7 @@ async fn get_editor_project_path(window: Window) -> Result { Ok(path.clone()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor))] async fn get_editor_meta(editor: WindowEditorInstance) -> Result { @@ -3154,13 +3153,13 @@ async fn get_editor_meta(editor: WindowEditorInstance) -> Result Result { RecordingMeta::load_for_project(&project_path).map_err(|e| e.to_string()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] async fn set_editor_recording_target( app: AppHandle, @@ -3169,7 +3168,7 @@ async fn set_editor_recording_target( EditorRecordingTarget::set(&app, project_path); Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor))] async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> Result<(), String> { @@ -3178,7 +3177,7 @@ async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> R meta.save_for_project().map_err(|e| e.to_string()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, clipboard))] async fn copy_video_to_clipboard( @@ -3196,7 +3195,7 @@ async fn copy_video_to_clipboard( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] async fn get_video_metadata(path: PathBuf) -> Result { @@ -3275,48 +3274,21 @@ async fn get_video_metadata(path: PathBuf) -> Result Result, ()> { @@ -3464,7 +3436,7 @@ pub enum UploadMode { Reupload, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, channel))] async fn upload_exported_video( @@ -3663,7 +3635,7 @@ async fn upgrade_required_result(app: &AppHandle) -> UploadResult { UploadResult::UpgradeRequired } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] fn get_screenshot_project_share_state( path: PathBuf, @@ -3679,7 +3651,7 @@ fn get_screenshot_project_share_state( Ok(ScreenshotProjectShareState { config, sharing }) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, clipboard))] async fn copy_current_screenshot_share_link( @@ -3697,7 +3669,7 @@ async fn copy_current_screenshot_share_link( Ok(Some(UploadResult::Success(link))) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, clipboard))] async fn upload_screenshot( @@ -3737,7 +3709,7 @@ async fn upload_screenshot( Ok(UploadResult::Success(uploaded.link)) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, clipboard, image_bytes))] async fn upload_rendered_screenshot( @@ -3777,7 +3749,7 @@ async fn upload_rendered_screenshot( Ok(UploadResult::Success(uploaded.link)) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn save_file_dialog( @@ -3797,8 +3769,6 @@ async fn save_file_dialog_inner( file_name: String, file_type: String, ) -> Result, String> { - use tauri_plugin_dialog::DialogExt; - info!(file_name, file_type, "Save file dialog requested"); let file_name = file_name @@ -3819,27 +3789,17 @@ async fn save_file_dialog_inner( info!(file_name, name, extension, "Showing save file dialog"); - // Use `tokio::sync::oneshot` so the async runtime worker yields while the native dialog - // is open instead of being parked by a synchronous `std::sync::mpsc` receive. The - // previous version blocked a runtime worker for the lifetime of the dialog which, in - // release builds with fewer/active workers, could starve other tasks and let an unrelated - // exit event slip through before the export session guard incremented. - let (tx, rx) = tokio::sync::oneshot::channel(); - - app.dialog() - .file() - .set_title("Save File") - .set_file_name(file_name) - .add_filter(name, &[extension]) - .save_file(move |path| { - let _ = tx.send( - path.as_ref() - .and_then(|p| p.as_path()) - .map(|p| p.to_string_lossy().to_string()), - ); - }); - - match rx.await { + match app + .native_request::, _>( + "dialog.save", + serde_json::json!({ + "title": "Save File", + "defaultPath": file_name, + "filters": [{ "name": name, "extensions": [extension] }] + }), + ) + .await + { Ok(result) => { info!(path = ?result, "Save file dialog completed"); Ok(result) @@ -3922,7 +3882,7 @@ pub enum FileType { Screenshot, } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] #[instrument] fn get_recording_meta( @@ -3954,7 +3914,7 @@ fn media_sort_time_millis(path: &Path) -> f64 { * 1000.0 } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] fn list_recordings(app: AppHandle) -> Result, String> { @@ -3985,7 +3945,7 @@ fn list_recordings(app: AppHandle) -> Result Result<(), String> { @@ -4032,7 +3992,7 @@ async fn delete_recording_directory(app: AppHandle, path: PathBuf) -> Result<(), Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] fn list_screenshots(app: AppHandle) -> Result, String> { @@ -4074,7 +4034,7 @@ fn list_screenshots(app: AppHandle) -> Result Result { @@ -4136,29 +4096,27 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result { Ok(is_pro) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] -fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> { +fn open_external_link(app: cap_desktop_runtime::AppHandle, url: String) -> Result<(), String> { if let Ok(Some(settings)) = GeneralSettingsStore::get(&app) && settings.disable_auto_open_links { return Ok(()); } - app.shell() - .open(&url, None) - .map_err(|e| format!("Failed to open URL: {e}"))?; + app.native_operation("shell.open", serde_json::json!({ "path": url }))?; Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(_app))] async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[cfg(target_os = "macos")] { - let bundle_id = _app.config().identifier.clone(); + let bundle_id = "so.cap.desktop"; Command::new("tccutil") .arg("reset") @@ -4171,13 +4129,13 @@ async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(_app))] async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), String> { #[cfg(target_os = "macos")] { - let bundle_id = _app.config().identifier.clone(); + let bundle_id = "so.cap.desktop"; Command::new("tccutil") .arg("reset") @@ -4190,14 +4148,14 @@ async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), String> { Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn clear_presets(app: AppHandle) -> Result<(), String> { presets::PresetsStore::clear(&app) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn is_camera_window_open(app: AppHandle) -> bool { @@ -4207,7 +4165,7 @@ async fn is_camera_window_open(app: AppHandle) -> bool { .unwrap_or(false) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor_instance))] async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Result<(), String> { @@ -4220,7 +4178,7 @@ async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Re Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor_instance))] async fn get_display_frame_for_cropping( @@ -4372,7 +4330,7 @@ async fn get_display_frame_for_cropping( Ok(jpeg_data.into_inner()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(editor_instance))] async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result>, String> { @@ -4394,7 +4352,7 @@ async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result Result<(), String> { let _ = window.close(); @@ -4439,7 +4397,7 @@ async fn editor_delete_project( } // keep this async otherwise opening windows may hang on windows -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { @@ -4454,14 +4412,14 @@ async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String Ok(()) } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] #[instrument] fn list_fails() -> Result, ()> { Ok(cap_fail::get_state()) } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] #[instrument] fn set_fail(name: String, value: bool) { @@ -4477,11 +4435,20 @@ async fn check_notification_permissions(app: AppHandle) { return; } - match app.notification().permission_state() { - Ok(state) if state != PermissionState::Granted => { + match app + .native_request::("notification.permission", serde_json::json!({})) + .await + { + Ok(state) if state != "granted" => { println!("Requesting notification permission"); - match app.notification().request_permission() { - Ok(PermissionState::Granted) => { + match app + .native_request::( + "notification.requestPermission", + serde_json::json!({}), + ) + .await + { + Ok(state) if state == "granted" => { println!("Notification permission granted"); } Ok(_) | Err(_) => { @@ -4526,7 +4493,7 @@ async fn check_notification_permissions(app: AppHandle) { // _guard // } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Result<(), ()> { @@ -4537,27 +4504,19 @@ async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Resul Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn pick_recordings_folder(app: AppHandle) -> Result, String> { - use tauri_plugin_dialog::DialogExt; - - let (tx, rx) = tokio::sync::oneshot::channel(); - - app.dialog() - .file() - .set_title("Choose Recordings Folder") - .pick_folder(move |path| { - let _ = tx.send( - path.as_ref() - .and_then(|p| p.as_path()) - .and_then(|p| p.to_str()) - .map(|s| s.to_string()), - ); - }); - - let result = rx.await.map_err(|e| e.to_string())?; + let result = app + .native_request::, _>( + "dialog.open", + serde_json::json!({ + "title": "Choose Recordings Folder", + "directory": true + }), + ) + .await?; if let Some(ref path) = result { general_settings::GeneralSettingsStore::update(&app, |s| { let previous = s.recordings_path.replace(path.clone()); @@ -4567,7 +4526,7 @@ async fn pick_recordings_folder(app: AppHandle) -> Result, String Ok(result) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn reset_recordings_folder(app: AppHandle) -> Result<(), String> { @@ -4577,7 +4536,7 @@ async fn reset_recordings_folder(app: AppHandle) -> Result<(), String> { }) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn set_camera_preview_state( @@ -4597,7 +4556,7 @@ async fn set_camera_preview_state( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] fn set_camera_window_position(app: AppHandle, x: f64, y: f64) -> Result<(), String> { @@ -4615,7 +4574,7 @@ fn set_camera_window_position(app: AppHandle, x: f64, y: f64) -> Result<(), Stri Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] fn ignore_camera_window_position( @@ -4626,7 +4585,7 @@ fn ignore_camera_window_position( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(state))] async fn refresh_camera_feed(state: MutableState<'_, App>) -> Result<(), String> { @@ -4676,7 +4635,7 @@ async fn refresh_camera_feed(state: MutableState<'_, App>) -> Result<(), String> Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] async fn destroy_camera_window(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { @@ -4694,7 +4653,7 @@ async fn destroy_camera_window(app: AppHandle, state: MutableState<'_, App>) -> Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result { @@ -4709,7 +4668,7 @@ async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::() - .typ::(); - - #[cfg(debug_assertions)] + #[cfg(any())] { - let bindings_path = std::path::Path::new("../src/utils/tauri.ts"); - if bindings_path.parent().is_some_and(|parent| parent.exists()) { - if let Err(err) = - specta_builder.export(specta_typescript::Typescript::default(), bindings_path) - { - warn!(error = %err, "Failed to export TypeScript bindings"); + let specta_builder = tauri_specta::Builder::new() + .commands(tauri_specta::collect_commands![ + set_mic_input, + set_camera_input, + set_native_camera_preview_enabled, + recording_settings::set_recording_mode, + upload_logs, + get_system_diagnostics, + cli::get_cli_install_status, + cli::install_cli, + cli::uninstall_cli, + recording::start_recording, + recording::stop_recording, + recording::pause_recording, + recording::resume_recording, + recording::toggle_pause_recording, + recording::set_mic_recording_muted, + recording::restart_recording, + recording::delete_recording, + recording::take_screenshot, + recording::import_current_desktop_background, + recording::list_cameras, + recording::get_camera_formats, + recording::get_microphone_info, + recording::list_capture_windows, + recording::list_capture_displays, + recording::list_displays_with_thumbnails, + recording::list_windows_with_thumbnails, + windows::refresh_window_content_protection, + general_settings::get_default_excluded_windows, + list_audio_devices, + close_recordings_overlay_window, + fake_window::set_fake_window_bounds, + fake_window::remove_fake_window, + focus_captures_panel, + get_current_recording, + export::begin_export_session, + export::end_export_session, + export::cancel_export, + export::cancel_current_window_exports, + export::export_video, + export::export_video_with_id, + export::export_video_to_file, + export::get_export_estimates, + export::generate_export_preview, + export::generate_export_preview_fast, + import::start_video_import, + import::add_existing_recording_to_editor, + import::start_image_import, + import::check_import_ready, + copy_file_to_path, + copy_video_to_clipboard, + copy_screenshot_to_clipboard, + copy_image_to_clipboard, + copy_rendered_screenshot_to_clipboard, + open_file_path, + get_video_metadata, + create_editor_instance, + get_editor_project_path, + get_mic_waveforms, + get_system_audio_waveforms, + audio_library::list_audio_library, + audio_library::add_audio_library_track, + audio_library::import_audio_track_file, + start_playback, + stop_playback, + set_playhead_position, + set_project_config, + update_project_config_in_memory, + generate_zoom_segments_from_clicks, + generate_keyboard_segments, + render_screenshot_for_export, + render_screenshot_project_for_export, + get_screenshot_project_share_state, + permissions::open_permission_settings, + permissions::do_permissions_check, + permissions::request_permission, + get_devices_snapshot, + upload_exported_video, + copy_current_screenshot_share_link, + upload_screenshot, + upload_rendered_screenshot, + create_screenshot_editor_instance, + update_screenshot_config, + prewarm_screenshot_background, + recognize_screenshot_text, + get_recording_meta, + save_file_dialog, + list_recordings, + list_screenshots, + check_upgraded_and_update, + open_external_link, + hotkeys::set_hotkey, + reset_camera_permissions, + reset_microphone_permissions, + clear_presets, + is_camera_window_open, + seek_to, + get_display_frame_for_cropping, + clip_thumbnails::get_clip_thumbnail, + windows::position_traffic_lights, + windows::set_theme, + windows::set_teleprompter_window_level, + windows::set_teleprompter_window_opacity, + windows::apply_macos_liquid_glass_background, + global_message_dialog, + show_window, + write_clipboard_string, + platform::perform_haptic_feedback, + platform::is_system_audio_capture_supported, + list_fails, + set_fail, + update_auth_plan, + set_window_transparent, + get_editor_meta, + get_recording_meta_by_path, + set_editor_recording_target, + delete_recording_directory, + set_pretty_name, + set_server_url, + pick_recordings_folder, + reset_recordings_folder, + recordings_locations::count_recordings_to_migrate, + recordings_locations::migrate_recordings_to_current_dir, + set_camera_preview_state, + set_camera_window_position, + ignore_camera_window_position, + await_camera_preview_ready, + destroy_camera_window, + refresh_camera_feed, + captions::create_dir, + captions::save_model_file, + captions::transcribe_audio, + captions::save_captions, + captions::load_captions, + captions::get_model_download_status, + captions::download_whisper_model, + captions::check_model_exists, + captions::delete_whisper_model, + captions::download_parakeet_model, + captions::check_parakeet_model_exists, + captions::delete_parakeet_model, + captions::export_captions_srt, + target_select_overlay::open_target_select_overlays, + target_select_overlay::close_target_select_overlays, + target_select_overlay::update_camera_overlay_bounds, + target_select_overlay::display_information, + target_select_overlay::get_window_icon, + target_select_overlay::focus_window, + editor_delete_project, + format_project_name, + recovery::find_incomplete_recordings, + recovery::recover_recording, + recovery::discard_incomplete_recording, + automation::get_automations, + automation::set_automations, + automation::test_automation, + automation::automation_should_open_screenshot_editor, + automation::list_automation_capabilities, + updates::updates_check, + updates::updates_download_and_install, + updates::updates_channel_changed, + ]) + .events(tauri_specta::collect_events![ + RecordingOptionsChanged, + NewStudioRecordingAdded, + EditorRecordingAdded, + NewScreenshotAdded, + RenderFrameEvent, + EditorStateChanged, + FrameLayoutEvent, + CurrentRecordingChanged, + RecordingStarted, + RecordingStopped, + RequestStartRecording, + RequestOpenRecordingPicker, + RequestSetTargetMode, + RequestOpenSettings, + RequestScrollToSettingsSection, + RequestScreenCapturePrewarm, + NewNotification, + audio_meter::AudioInputLevelChange, + captions::DownloadProgress, + recording::RecordingEvent, + RecordingDeleted, + recordings_locations::RecordingsMigrationProgress, + target_select_overlay::TargetUnderCursor, + hotkeys::OnEscapePress, + upload::UploadProgressEvent, + import::VideoImportProgress, + SetCaptureAreaPending, + DevicesUpdated, + updates::UpdateDownloadProgress, + updates::UpdateReady, + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Throw) + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::(); + + #[cfg(debug_assertions)] + { + let bindings_path = std::path::Path::new("../src/utils/tauri.ts"); + if bindings_path.parent().is_some_and(|parent| parent.exists()) { + if let Err(err) = + specta_builder.export(specta_typescript::Typescript::default(), bindings_path) + { + warn!(error = %err, "Failed to export TypeScript bindings"); + } + } else { + debug!("Skipping TypeScript bindings export outside source checkout"); } - } else { - debug!("Skipping TypeScript bindings export outside source checkout"); } } @@ -5155,122 +5115,22 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { mic_feed }; - tauri::async_runtime::set(tokio::runtime::Handle::current()); + cap_desktop_runtime::async_runtime::set(tokio::runtime::Handle::current()); - #[allow(unused_mut)] - let mut builder = tauri::Builder::default(); - - // The Linux single-instance plugin establishes its D-Bus connection through a - // blocking zbus call, which panics ("Cannot start a runtime from within a - // runtime") when initialized inside the Tokio runtime that drives the app. - #[cfg(not(target_os = "linux"))] - { - builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { - trace!("Single instance invoked with args {args:?}"); - - let action_urls = args - .iter() - .filter(|arg| arg.starts_with("cap-desktop://")) - .filter_map(|arg| tauri::Url::parse(arg).ok()) - .collect::>(); - if !action_urls.is_empty() { - deeplink_actions::handle(app, action_urls); - return; - } - - let Some(cap_file) = args - .iter() - .find(|arg| arg.ends_with(".cap")) - .map(PathBuf::from) - else { - let app = app.clone(); - tokio::spawn(async move { - ShowCapWindow::Main { - init_target_mode: None, - } - .show(&app) - .await - }); - return; - }; - - let _ = open_project_from_path(&cap_file, app.clone()); - })); - } - - #[cfg(target_os = "macos")] - { - builder = builder - .menu(build_macos_app_menu) - .on_menu_event(|app, event| { - if event.id() == APP_MENU_QUIT_ID { - let app = app.clone(); - tokio::spawn(async move { - request_app_exit(app).await; - }); - } - }) - .plugin(tauri_nspanel::init()); - } - - builder - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_store::Builder::new().build()) - .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_oauth::init()) - .plugin(tauri_plugin_http::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_notification::init()) - .plugin(flags::plugin::init()) - .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_opener::init()) - .plugin( - tauri_plugin_window_state::Builder::new() - .with_state_flags({ - use tauri_plugin_window_state::StateFlags; - let mut flags = StateFlags::all(); - flags.remove(StateFlags::VISIBLE); - flags - }) - .with_denylist(&[ - CapWindowId::Onboarding.label().as_str(), - CapWindowId::Main.label().as_str(), - CapWindowId::Settings.label().as_str(), - "window-capture-occluder", - "target-select-overlay", - CapWindowId::CaptureArea.label().as_str(), - CapWindowId::Camera.label().as_str(), - CapWindowId::RecordingsOverlay.label().as_str(), - CapWindowId::RecordingControls.label().as_str(), - CapWindowId::Upgrade.label().as_str(), - "editor", - "screenshot-editor", - ]) - .map_label(|label| match label { - label if label.starts_with("camera-") => "camera", - label if label.starts_with("editor-") => "editor", - label if label.starts_with("screenshot-editor-") => "screenshot-editor", - label if label.starts_with("window-capture-occluder-") => { - "window-capture-occluder" - } - label if label.starts_with("target-select-overlay") => "target-select-overlay", - _ => label, - }) - .build(), - ) - .invoke_handler(specta_builder.invoke_handler()) - .setup(move |app| { - let app = app.handle().clone(); + let resource_dir = std::env::var_os("CAP_ELECTRON_RESOURCE_DIR") + .map(PathBuf::from) + .or_else(|| std::env::current_exe().ok()?.parent().map(PathBuf::from)) + .unwrap_or_else(|| PathBuf::from(".")); + let backend_options = cap_desktop_runtime::BackendOptions::new("so.cap.desktop", resource_dir); + if let Err(error) = cap_desktop_runtime::run_backend( + backend_options, + move |app| { + let app = app.clone(); if let Err(err) = update_project_names::migrate_if_needed(&app) { tracing::error!("Failed to migrate project file names: {}", err); } - specta_builder.mount_events(&app); hotkeys::init(&app); general_settings::init(&app); configure_camera_blur_recovery(&app, previous_termination); @@ -5287,7 +5147,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { app.manage(http_client::RetryableHttpClient::default()); app.manage(PendingScreenshots::default()); app.manage(FinalizingRecordings::default()); - app.manage(updates::UpdatesState::default()); + app.manage(updates::UpdatesState); updates::spawn_background_loop(app.clone()); #[cfg(unix)] @@ -5560,29 +5420,39 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { }); let app_handle = app.clone(); - app.deep_link().on_open_url(move |event| { - deeplink_actions::handle(&app_handle, event.urls()); + app.listen("electron://open-urls", move |event| { + let Ok(urls) = serde_json::from_str::>(event.payload()) else { + return; + }; + let urls = urls + .into_iter() + .filter_map(|url| cap_desktop_runtime::Url::parse(&url).ok()) + .collect(); + deeplink_actions::handle(&app_handle, urls); }); - Ok(()) - }) - .on_window_event(|window, event| { + let app_handle = app.clone(); + app.listen("electron://reopen", move |_| { + handle_electron_reopen(&app_handle); + }); + + app.on_window_event(|window, event| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let label = window.label(); let app = window.app_handle(); if matches!( event, - WindowEvent::CloseRequested { .. } - | WindowEvent::Moved(_) - | WindowEvent::Focused(_) + WindowEvent::CloseRequested + | WindowEvent::Moved { .. } + | WindowEvent::Focused { .. } ) && app_is_exiting(app) { return; } match event { - WindowEvent::CloseRequested { api, .. } => { + WindowEvent::CloseRequested => { let window_id = CapWindowId::from_str(label).ok(); if !matches!( window_id, @@ -5590,7 +5460,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { | Some(CapWindowId::ScreenshotEditor { .. }) ) && export::export_session_active() { - api.prevent_close(); warn!( window = label, "Preventing window close request during active export" @@ -5608,7 +5477,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { return; } - api.prevent_close(); let _ = window.hide(); tracing::warn!("Camera window CloseRequested event received!"); let session_id = app @@ -5621,7 +5489,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { }); } CapWindowId::Main => { - api.prevent_close(); let _ = window.hide(); #[cfg(target_os = "macos")] @@ -5676,7 +5543,9 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { }); } } - _ => {} + _ => { + let _ = window.close(); + } } } } @@ -5814,7 +5683,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { if let Some(focus_manager) = app.try_state::() { - focus_manager.destroy(&display_id, app.global_shortcut()); + focus_manager.destroy(&display_id, app); } let session_id = app .try_state::>() @@ -5856,7 +5725,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { crate::permissions::sync_macos_dock_visibility(app); } #[cfg(target_os = "macos")] - WindowEvent::Focused(focused) => { + WindowEvent::Focused { focused } => { let window_id = CapWindowId::from_str(label); if matches!(window_id, Ok(CapWindowId::Upgrade)) { @@ -5869,20 +5738,21 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { } } - if *focused + if focused && let Ok(window_id) = window_id && window_id.activates_dock() { crate::permissions::sync_macos_dock_visibility(app); } } - WindowEvent::DragDrop(tauri::DragDropEvent::Drop { paths, .. }) => { + WindowEvent::DragDrop { paths } => { let window_id = CapWindowId::from_str(label).ok(); for path in paths { + let path = PathBuf::from(path); let result = if matches!(window_id, Some(CapWindowId::Main)) { - open_importable_from_path(path, app.clone()) + open_importable_from_path(&path, app.clone()) } else { - open_project_from_path(path, app.clone()) + open_project_from_path(&path, app.clone()) }; if let Err(err) = result { @@ -5890,13 +5760,14 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { } } } - WindowEvent::Moved(position) => { + WindowEvent::Moved { x, y } => { + let position = PhysicalPosition::new(x as i32, y as i32); let window_id = CapWindowId::from_str(label); #[cfg(target_os = "macos")] if matches!(&window_id, Ok(CapWindowId::Main)) && let Some(constrained_position) = - platform::constrain_main_window_to_visible_top(window, *position) + platform::constrain_main_window_to_visible_top(&window, position) { match window.set_position(constrained_position) { Ok(()) => { @@ -5975,7 +5846,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { if let Some(webview_window) = app.get_webview_window(label) { windows::update_window_rasterization_scale( &webview_window, - *scale_factor, + scale_factor, ); } } @@ -5985,29 +5856,43 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { if let Err(panic) = result { let message = panic_payload_message(&panic); - tracing::error!(panic = %message, "Suppressed panic in Tauri WindowEvent handler"); + tracing::error!(panic = %message, "Suppressed panic in Electron WindowEvent handler"); sentry::capture_message( - &format!("Tauri WindowEvent panic suppressed: {message}"), + &format!("Electron WindowEvent panic suppressed: {message}"), sentry::Level::Error, ); } - }) - .build(tauri_context) - .expect("error while running tauri application") - .run(move |handle, event| { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - handle_run_event(handle, event); - })); + }); - if let Err(panic) = result { - let message = panic_payload_message(&panic); - tracing::error!(panic = %message, "Suppressed panic in Tauri RunEvent handler"); - sentry::capture_message( - &format!("Tauri RunEvent panic suppressed: {message}"), - sentry::Level::Error, - ); + Ok(()) + }, + |app| async move { + let should_cleanup = app + .try_state::() + .is_none_or(|exit_state| exit_state.begin()); + if should_cleanup { + if tokio::time::timeout( + APP_EXIT_TOTAL_TIMEOUT, + cleanup_app_resources_for_exit(&app), + ) + .await + .is_err() + { + error!( + timeout_ms = APP_EXIT_TOTAL_TIMEOUT.as_millis(), + "Timed out while cleaning up the Electron backend" + ); + } else { + crash_sentinel::mark_clean_exit(); + } } - }); + Ok(()) + }, + ) + .await + { + error!(%error, "Electron backend stopped unexpectedly"); + } } pub(crate) fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String { @@ -6020,7 +5905,7 @@ pub(crate) fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> Str } } -// Catches panics inside an async Tauri command body so a single bad code path can't +// Catches panics inside an async desktop command body so a single bad code path can't // unwind through the IPC dispatcher and terminate the whole desktop process. This is // particularly important for commands that participate in the export flow because the // sidecar architecture relies on the desktop app remaining alive to babysit the worker. @@ -6035,10 +5920,10 @@ where error!( command = command_name, panic = %message, - "Suppressed panic in Tauri command" + "Suppressed panic in desktop command" ); sentry::capture_message( - &format!("Tauri command '{command_name}' panicked: {message}"), + &format!("Desktop command '{command_name}' panicked: {message}"), sentry::Level::Error, ); Err(format!("{command_name} failed unexpectedly")) @@ -6061,10 +5946,46 @@ where } } -fn handle_run_event(_handle: &AppHandle, event: tauri::RunEvent) { +fn handle_electron_reopen(app: &AppHandle) { + let should_focus_onboarding = should_show_onboarding(app); + if should_focus_onboarding && let Some(onboarding) = CapWindowId::Onboarding.get(app) { + onboarding.show().ok(); + onboarding.set_focus().ok(); + return; + } + + let window = app + .webview_windows() + .into_iter() + .find(|(label, _)| { + label.starts_with("editor-") + || label.starts_with("screenshot-editor-") + || label.as_str() == "settings" + || label.as_str() == "signin" + || (should_focus_onboarding && label.as_str() == "onboarding") + }) + .map(|(_, window)| window); + + if let Some(window) = window { + window.show().ok(); + window.set_focus().ok(); + } else { + let app = app.clone(); + spawn_on_runtime(async move { + let _ = ShowCapWindow::Main { + init_target_mode: None, + } + .show(&app) + .await; + }); + } +} + +#[cfg(any())] +fn handle_run_event(_handle: &AppHandle, event: cap_desktop_runtime::RunEvent) { match event { #[cfg(target_os = "macos")] - tauri::RunEvent::Reopen { .. } => { + cap_desktop_runtime::RunEvent::Reopen { .. } => { let should_focus_onboarding = should_show_onboarding(_handle); if should_focus_onboarding @@ -6109,7 +6030,7 @@ fn handle_run_event(_handle: &AppHandle, event: tauri::RunEvent) { }); } } - tauri::RunEvent::ExitRequested { code, api, .. } => { + cap_desktop_runtime::RunEvent::ExitRequested { code, api, .. } => { info!(?code, "App exit requested"); match handle_exit_requested( @@ -6134,7 +6055,7 @@ fn handle_run_event(_handle: &AppHandle, event: tauri::RunEvent) { ExitRequestDecision::AllowRuntimeExit => {} } } - tauri::RunEvent::Exit => { + cap_desktop_runtime::RunEvent::Exit => { #[cfg(target_os = "macos")] { // This arm runs on the AppKit main thread, so reverse the Liquid Glass @@ -6547,7 +6468,7 @@ async fn create_editor_instance_impl( app: &AppHandle, path: PathBuf, frame_cb: cap_editor::EditorFrameCallback, -) -> Result<(Arc, tauri::EventId), String> { +) -> Result<(Arc, cap_desktop_runtime::EventId), String> { let app = app.clone(); wait_for_recording_ready(&app, &path).await?; @@ -6706,14 +6627,14 @@ fn screenshots_path(app: &AppHandle) -> PathBuf { // screenshots_path(app).join(format!("{screenshot_id}.cap")) // } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app))] fn global_message_dialog(app: AppHandle, message: String) { - app.dialog().message(message).show(|_| {}); + let _ = app.native_operation("dialog.message", serde_json::json!({ "message": message })); } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(clipboard))] async fn write_clipboard_string( @@ -6728,7 +6649,7 @@ async fn write_clipboard_string( .map_err(|e| format!("Failed to write text to clipboard: {e}")) } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] fn format_project_name( template: Option, @@ -6746,11 +6667,11 @@ fn format_project_name( ) } -trait EventExt: tauri_specta::Event { +trait EventExt: cap_desktop_runtime::Event { fn listen_any_spawn( app: &AppHandle, - handler: impl Fn(Self, AppHandle) -> Fut + Send + 'static + Clone, - ) -> tauri::EventId + handler: impl Fn(Self, AppHandle) -> Fut + Send + Sync + 'static + Clone, + ) -> cap_desktop_runtime::EventId where Fut: Future + Send, Self: serde::de::DeserializeOwned + Send + 'static, @@ -6766,14 +6687,17 @@ trait EventExt: tauri_specta::Event { } } -impl EventExt for T {} +impl EventExt for T {} fn show_import_error_dialog(app: &AppHandle, message: String) { - app.dialog() - .message(message) - .title("Import Error") - .kind(tauri_plugin_dialog::MessageDialogKind::Error) - .show(|_| {}); + let _ = app.native_operation( + "dialog.message", + serde_json::json!({ + "message": message, + "title": "Import Error", + "kind": "error" + }), + ); } fn hide_main_window(app: &AppHandle) { @@ -6856,8 +6780,7 @@ fn open_project_from_path(path: &Path, app: AppHandle) -> Result<(), String> { if mp4_path.exists() && mp4_path.is_file() { let _ = app - .opener() - .open_path(mp4_path.to_str().unwrap_or_default(), None::); + .native_operation("opener.openPath", serde_json::json!({ "path": mp4_path })); if let Some(main_window) = CapWindowId::Main.get(&app) { main_window.hide().ok(); } diff --git a/apps/desktop/src-tauri/src/logging.rs b/apps/desktop/src-backend/src/logging.rs similarity index 99% rename from apps/desktop/src-tauri/src/logging.rs rename to apps/desktop/src-backend/src/logging.rs index f70d042c836..22fb8c35195 100644 --- a/apps/desktop/src-tauri/src/logging.rs +++ b/apps/desktop/src-backend/src/logging.rs @@ -2,13 +2,13 @@ use crate::{ ArcLock, feeds::microphone::MicrophoneFeed, general_settings::GeneralSettingsStore, permissions, web_api::ManagerExt, }; +use cap_desktop_runtime::{AppHandle, Manager}; use cap_recording::diagnostics::{ CameraDiagnostics, CameraFormatInfo, DisplayDiagnostics, HardwareInfo, MicrophoneDiagnostics, StorageInfo, }; use serde::Serialize; use std::{fs, path::PathBuf}; -use tauri::{AppHandle, Manager}; async fn get_latest_log_file(app: &AppHandle) -> Option { let logs_dir = app diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-backend/src/main.rs similarity index 92% rename from apps/desktop/src-tauri/src/main.rs rename to apps/desktop/src-backend/src/main.rs index ab1dc76e715..ff39e18d94f 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-backend/src/main.rs @@ -9,6 +9,9 @@ use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt}; const TOKIO_WORKER_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024; fn main() { + #[cfg(target_os = "macos")] + configure_macos_sidecar_activation_policy(); + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] if std::env::var_os("ORT_DYLIB_PATH").is_none() && let Some(path) = cap_camera_effects::onnx_runtime_library_path() @@ -172,6 +175,21 @@ fn main() { .block_on(cap_desktop_lib::run(handle, logs_dir)); } +#[cfg(target_os = "macos")] +fn configure_macos_sidecar_activation_policy() { + use cocoa::appkit::{ + NSApp, NSApplication, + NSApplicationActivationPolicy::NSApplicationActivationPolicyProhibited, + }; + + // Electron owns Cap's windows, menu bar, and Dock presence. Some of the native + // capture crates initialize AppKit in this process, which otherwise makes macOS + // register the Rust sidecar as a second foreground copy of Cap. + unsafe { + NSApp().setActivationPolicy_(NSApplicationActivationPolicyProhibited); + } +} + fn install_panic_hook(logs_dir: std::path::PathBuf) { let prev = std::panic::take_hook(); let panics_log = logs_dir.join("panics.log"); diff --git a/apps/desktop/src-tauri/src/notifications.rs b/apps/desktop/src-backend/src/notifications.rs similarity index 91% rename from apps/desktop/src-tauri/src/notifications.rs rename to apps/desktop/src-backend/src/notifications.rs index d872fd8380f..51d449e8678 100644 --- a/apps/desktop/src-tauri/src/notifications.rs +++ b/apps/desktop/src-backend/src/notifications.rs @@ -1,5 +1,4 @@ use crate::{AppSounds, general_settings::GeneralSettingsStore}; -use tauri_plugin_notification::NotificationExt; #[allow(unused)] pub enum NotificationType { @@ -83,12 +82,15 @@ impl NotificationType { } } - pub fn send(self, app: &tauri::AppHandle) { + pub fn send(self, app: &cap_desktop_runtime::AppHandle) { send_notification(app, self); } } -pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType) { +pub fn send_notification( + app: &cap_desktop_runtime::AppHandle, + notification_type: NotificationType, +) { let enable_notifications = GeneralSettingsStore::get(app) .map(|settings| settings.is_some_and(|s| s.enable_notifications)) .unwrap_or(false); @@ -99,12 +101,10 @@ pub fn send_notification(app: &tauri::AppHandle, notification_type: Notification let (title, body, _is_error) = notification_type.details(); - app.notification() - .builder() - .title(title) - .body(body) - .show() - .ok(); + let _ = app.native_operation( + "notification.show", + serde_json::json!({ "title": title, "body": body }), + ); let skip_sound = matches!( notification_type, diff --git a/apps/desktop/src-tauri/src/panel_manager.rs b/apps/desktop/src-backend/src/panel_manager.rs similarity index 82% rename from apps/desktop/src-tauri/src/panel_manager.rs rename to apps/desktop/src-backend/src/panel_manager.rs index 68bc7eff07d..b855985c629 100644 --- a/apps/desktop/src-tauri/src/panel_manager.rs +++ b/apps/desktop/src-backend/src/panel_manager.rs @@ -193,7 +193,7 @@ impl PanelOperationGuard { } #[cfg(target_os = "macos")] -pub fn is_window_handle_valid(window: &tauri::WebviewWindow) -> bool { +pub fn is_window_handle_valid(window: &cap_desktop_runtime::WebviewWindow) -> bool { match window.inner_size() { Ok(_) => true, Err(e) => { @@ -202,42 +202,3 @@ pub fn is_window_handle_valid(window: &tauri::WebviewWindow) -> bool { } } } - -#[cfg(target_os = "macos")] -pub type NSPanel = tauri_nspanel::objc_id::Id< - tauri_nspanel::raw_nspanel::RawNSPanel, - tauri_nspanel::objc_id::Shared, ->; - -#[cfg(target_os = "macos")] -pub fn try_to_panel(window: &tauri::WebviewWindow) -> Result { - use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; - - if !is_window_handle_valid(window) { - return Err(PanelConversionError::InvalidHandle); - } - - window - .to_panel() - .map_err(|e| PanelConversionError::ConversionFailed(format!("{e:?}"))) -} - -#[cfg(target_os = "macos")] -#[derive(Debug)] -pub enum PanelConversionError { - InvalidHandle, - ConversionFailed(String), -} - -#[cfg(target_os = "macos")] -impl std::fmt::Display for PanelConversionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidHandle => write!(f, "Window handle is invalid or unavailable"), - Self::ConversionFailed(msg) => write!(f, "Panel conversion failed: {msg}"), - } - } -} - -#[cfg(target_os = "macos")] -impl std::error::Error for PanelConversionError {} diff --git a/apps/desktop/src-tauri/src/permissions.rs b/apps/desktop/src-backend/src/permissions.rs similarity index 94% rename from apps/desktop/src-tauri/src/permissions.rs rename to apps/desktop/src-backend/src/permissions.rs index 8f66b9b998b..8e10ce22d39 100644 --- a/apps/desktop/src-tauri/src/permissions.rs +++ b/apps/desktop/src-backend/src/permissions.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; #[cfg(target_os = "macos")] use crate::{general_settings::GeneralSettingsStore, windows::CapWindowId}; #[cfg(target_os = "macos")] +use cap_desktop_runtime::Manager; +#[cfg(target_os = "macos")] use cidre::{av, sc}; #[cfg(target_os = "macos")] use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; @@ -13,8 +15,6 @@ use std::{ sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering}, time::Duration, }; -#[cfg(target_os = "macos")] -use tauri::Manager; use tracing::instrument; #[cfg(target_os = "macos")] @@ -48,7 +48,7 @@ const MACOS_SCK_OUTCOME_FAILED: u8 = 2; #[cfg(target_os = "macos")] pub(crate) struct MacosPanelWindowActivationGuard { - app: tauri::AppHandle, + app: cap_desktop_runtime::AppHandle, } #[cfg(target_os = "macos")] @@ -93,7 +93,7 @@ fn macos_prompt_accessibility_access() { #[cfg(target_os = "macos")] fn macos_run_on_main_thread( - app: &tauri::AppHandle, + app: &cap_desktop_runtime::AppHandle, callback: impl FnOnce() -> R + Send + 'static, ) -> Option { use std::sync::mpsc; @@ -137,7 +137,7 @@ fn macos_permission_needs_settings_fallback(permission: &OSPermission) -> bool { } #[cfg(target_os = "macos")] -fn macos_focus_permission_window(app: &tauri::AppHandle) { +fn macos_focus_permission_window(app: &cap_desktop_runtime::AppHandle) { if let Some(window) = ["onboarding", "main", "settings"] .into_iter() .find_map(|label| app.get_webview_window(label)) @@ -148,12 +148,12 @@ fn macos_focus_permission_window(app: &tauri::AppHandle) { } #[cfg(target_os = "macos")] -fn macos_activate_permission_request(app: &tauri::AppHandle) { +fn macos_activate_permission_request(app: &cap_desktop_runtime::AppHandle) { if let Err(err) = app.set_dock_visibility(true) { tracing::warn!("Failed to show dock icon for permission request: {err}"); } - if let Err(err) = app.set_activation_policy(tauri::ActivationPolicy::Regular) { + if let Err(err) = app.set_activation_policy(cap_desktop_runtime::ActivationPolicy::Regular) { tracing::warn!("Failed to set activation policy to Regular: {err}"); } @@ -170,11 +170,11 @@ fn macos_activate_permission_request(app: &tauri::AppHandle) { } #[cfg(target_os = "macos")] -fn macos_sync_activation_policy(app: &tauri::AppHandle, should_show_dock: bool) { +fn macos_sync_activation_policy(app: &cap_desktop_runtime::AppHandle, should_show_dock: bool) { let policy = if should_show_dock { - tauri::ActivationPolicy::Regular + cap_desktop_runtime::ActivationPolicy::Regular } else { - tauri::ActivationPolicy::Accessory + cap_desktop_runtime::ActivationPolicy::Accessory }; if let Err(err) = app.set_activation_policy(policy) { @@ -187,7 +187,7 @@ fn macos_sync_activation_policy(app: &tauri::AppHandle, should_show_dock: bool) // NSException that aborts the process when it unwinds into Rust. Callers must // leave the policy alone until fullscreen exits. #[cfg(target_os = "macos")] -fn macos_any_window_fullscreen(app: &tauri::AppHandle) -> bool { +fn macos_any_window_fullscreen(app: &cap_desktop_runtime::AppHandle) -> bool { app.webview_windows() .values() .any(|window| window.is_fullscreen().unwrap_or(false)) @@ -195,12 +195,14 @@ fn macos_any_window_fullscreen(app: &tauri::AppHandle) -> bool { #[cfg(target_os = "macos")] pub(crate) fn prepare_macos_panel_window( - app: &tauri::AppHandle, + app: &cap_desktop_runtime::AppHandle, ) -> MacosPanelWindowActivationGuard { let prev = MACOS_PENDING_PANEL_WINDOWS.fetch_add(1, Ordering::AcqRel); if prev == 0 && !macos_any_window_fullscreen(app) { - if let Err(err) = app.set_activation_policy(tauri::ActivationPolicy::Accessory) { + if let Err(err) = + app.set_activation_policy(cap_desktop_runtime::ActivationPolicy::Accessory) + { tracing::warn!("Failed to prepare macOS panel activation policy: {err}"); } } @@ -209,7 +211,7 @@ pub(crate) fn prepare_macos_panel_window( } #[cfg(target_os = "macos")] -pub(crate) fn sync_macos_dock_visibility(app: &tauri::AppHandle) { +pub(crate) fn sync_macos_dock_visibility(app: &cap_desktop_runtime::AppHandle) { if MACOS_PENDING_PANEL_WINDOWS.load(Ordering::Acquire) > 0 { return; } @@ -249,10 +251,10 @@ pub(crate) fn sync_macos_dock_visibility(app: &tauri::AppHandle) { } #[cfg(target_os = "macos")] -pub(crate) fn schedule_macos_dock_visibility_sync(app: &tauri::AppHandle) { +pub(crate) fn schedule_macos_dock_visibility_sync(app: &cap_desktop_runtime::AppHandle) { let generation = MACOS_DOCK_VISIBILITY_SYNC_GENERATION.fetch_add(1, Ordering::AcqRel) + 1; let app = app.clone(); - tauri::async_runtime::spawn(async move { + cap_desktop_runtime::async_runtime::spawn(async move { tokio::time::sleep(Duration::from_millis(100)).await; if MACOS_DOCK_VISIBILITY_SYNC_GENERATION.load(Ordering::Acquire) == generation { sync_macos_dock_visibility(&app); @@ -394,7 +396,7 @@ fn macos_spawn_sck_displays_validation() { if MACOS_SCK_DISPLAYS_VALIDATION_IN_FLIGHT.swap(true, Ordering::AcqRel) { return; } - tauri::async_runtime::spawn_blocking(|| { + cap_desktop_runtime::async_runtime::spawn_blocking(|| { let _reset = MacosSckValidationInFlightReset; macos_validate_sck_displays(); }); @@ -419,7 +421,7 @@ fn macos_validate_sck_displays() -> bool { *last_attempt = Some(std::time::Instant::now()); let validated = objc2::rc::autoreleasepool(|_| { - tauri::async_runtime::block_on(async { + cap_desktop_runtime::async_runtime::block_on(async { // Bounded: a wedged replayd (post-sleep, or right after a TCC // grant before relaunch) can leave this future unresolved forever, // which would park whichever thread is validating. @@ -484,7 +486,7 @@ fn macos_validate_sck_displays() -> bool { } #[cfg(target_os = "macos")] -fn macos_request_permission(app: &tauri::AppHandle, permission: &OSPermission) { +fn macos_request_permission(app: &cap_desktop_runtime::AppHandle, permission: &OSPermission) { match permission { OSPermission::ScreenRecording => { if macos_run_on_main_thread(app, macos_prompt_screen_recording_access).is_none() { @@ -558,7 +560,7 @@ async fn macos_wait_for_permission_update(permission: &OSPermission) -> bool { } #[cfg(target_os = "macos")] -fn macos_open_permission_settings(app: &tauri::AppHandle, permission: &OSPermission) { +fn macos_open_permission_settings(app: &cap_desktop_runtime::AppHandle, permission: &OSPermission) { use std::process::Command; let process = Command::new("open") @@ -598,9 +600,9 @@ pub enum OSPermission { Accessibility, } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] -pub fn open_permission_settings(_app: tauri::AppHandle, _permission: OSPermission) { +pub fn open_permission_settings(_app: cap_desktop_runtime::AppHandle, _permission: OSPermission) { #[cfg(target_os = "macos")] { macos_activate_permission_request(&_app); @@ -608,17 +610,17 @@ pub fn open_permission_settings(_app: tauri::AppHandle, _permission: OSPermissio } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(_app))] -pub async fn request_permission(_app: tauri::AppHandle, _permission: OSPermission) { +pub async fn request_permission(_app: cap_desktop_runtime::AppHandle, _permission: OSPermission) { #[cfg(target_os = "macos")] { macos_activate_permission_request(&_app); let permission = _permission; let app = _app.clone(); - tauri::async_runtime::spawn_blocking(move || { + cap_desktop_runtime::async_runtime::spawn_blocking(move || { macos_request_permission(&app, &permission); }) .await @@ -670,7 +672,7 @@ impl OSPermissionsCheck { } } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] pub fn do_permissions_check(_initial_check: bool) -> OSPermissionsCheck { // Pool-wrapped because this runs on tokio/tauri worker threads (which have diff --git a/apps/desktop/src-tauri/src/platform/macos/delegates.rs b/apps/desktop/src-backend/src/platform/macos/delegates.rs similarity index 99% rename from apps/desktop/src-tauri/src/platform/macos/delegates.rs rename to apps/desktop/src-backend/src/platform/macos/delegates.rs index 5b6536b7f73..12d2a988d44 100644 --- a/apps/desktop/src-tauri/src/platform/macos/delegates.rs +++ b/apps/desktop/src-backend/src/platform/macos/delegates.rs @@ -1,4 +1,5 @@ // TODO(Ilya): Re-write all macos code to use `objc2` crates n +use cap_desktop_runtime::{Emitter, LogicalPosition, Runtime, Window}; /// /// Credit to @haasal, @charrondev, Hoppscotch app, Electron, Zed Editor /// @@ -12,7 +13,6 @@ /// (Electron) https://github.com/electron/electron/blob/38512efd25a159ddc64a54c22ef9eb6dd60064ec/shell/browser/native_window_mac.mm#L1454 /// use objc::{class, msg_send, sel, sel_impl}; -use tauri::{Emitter, LogicalPosition, Runtime, Window}; pub struct UnsafeWindowHandle(pub *mut std::ffi::c_void); unsafe impl Send for UnsafeWindowHandle {} @@ -380,7 +380,7 @@ pub fn setup(window: Window, controls_inset: LogicalPosition // // NOTE: `static CLASS` below is a single process-wide instance shared across every // monomorphization of this function, not one per `R`. `setup` is only ever called - // with `R = tauri::Wry` in this app, so this is fine in practice; if it were ever + // with `R = cap_desktop_runtime::Wry` in this app, so this is fine in practice; if it were ever // called with a different `R`, the first call's `on_*::` method pointers would // be baked into the shared class for all `R`. fn get_or_register_delegate_class() -> &'static Class { diff --git a/apps/desktop/src-backend/src/platform/macos/mod.rs b/apps/desktop/src-backend/src/platform/macos/mod.rs new file mode 100644 index 00000000000..dc7fd44cd0a --- /dev/null +++ b/apps/desktop/src-backend/src/platform/macos/mod.rs @@ -0,0 +1,20 @@ +mod sc_shareable_content; + +pub use sc_shareable_content::*; + +pub fn constrain_main_window_to_visible_top( + _window: &cap_desktop_runtime::Window, + _position: cap_desktop_runtime::PhysicalPosition, +) -> Option> { + None +} + +pub fn teardown_all_liquid_glass_on_main(app: &cap_desktop_runtime::AppHandle) -> usize { + let count = app.webview_windows().len(); + let _ = app.native_operation("window.teardownVisualEffects", serde_json::json!({})); + count +} + +pub async fn teardown_all_liquid_glass(app: &cap_desktop_runtime::AppHandle) -> Result<(), String> { + app.native_operation("window.teardownVisualEffects", serde_json::json!({})) +} diff --git a/apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs b/apps/desktop/src-backend/src/platform/macos/sc_shareable_content.rs similarity index 100% rename from apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs rename to apps/desktop/src-backend/src/platform/macos/sc_shareable_content.rs diff --git a/apps/desktop/src-tauri/src/platform/mod.rs b/apps/desktop/src-backend/src/platform/mod.rs similarity index 96% rename from apps/desktop/src-tauri/src/platform/mod.rs rename to apps/desktop/src-backend/src/platform/mod.rs index e51d3e709a3..c3b9e6a2055 100644 --- a/apps/desktop/src-tauri/src/platform/mod.rs +++ b/apps/desktop/src-backend/src/platform/mod.rs @@ -30,7 +30,7 @@ pub enum HapticPerformanceTime { DrawCompleted = 2, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub fn perform_haptic_feedback( @@ -58,7 +58,7 @@ pub fn perform_haptic_feedback( /// Check if system audio capture is supported on the current platform and OS version. /// On macOS, system audio capture requires macOS 13.0 or later. /// On Windows/Linux, this may have different requirements. -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub fn is_system_audio_capture_supported() -> bool { diff --git a/apps/desktop/src-tauri/src/platform/win.rs b/apps/desktop/src-backend/src/platform/win.rs similarity index 100% rename from apps/desktop/src-tauri/src/platform/win.rs rename to apps/desktop/src-backend/src/platform/win.rs diff --git a/apps/desktop/src-tauri/src/power_observer.rs b/apps/desktop/src-backend/src/power_observer.rs similarity index 98% rename from apps/desktop/src-tauri/src/power_observer.rs rename to apps/desktop/src-backend/src/power_observer.rs index 71e354470bb..f1057ce1239 100644 --- a/apps/desktop/src-tauri/src/power_observer.rs +++ b/apps/desktop/src-backend/src/power_observer.rs @@ -1,5 +1,5 @@ +use cap_desktop_runtime::AppHandle; use std::sync::atomic::{AtomicBool, Ordering}; -use tauri::AppHandle; use tracing::info; static SYSTEM_ASLEEP: AtomicBool = AtomicBool::new(false); @@ -56,6 +56,7 @@ pub fn uninstall(app: &AppHandle) { #[cfg(target_os = "macos")] mod macos { use super::{on_system_did_wake, on_system_will_sleep}; + use cap_desktop_runtime::AppHandle; use objc2::{ AnyThread, DeclaredClass, define_class, msg_send, rc::{Retained, autoreleasepool}, @@ -64,7 +65,6 @@ mod macos { use objc2_app_kit::NSWorkspace; use objc2_foundation::{NSNotification, ns_string}; use std::sync::{Mutex, OnceLock}; - use tauri::AppHandle; use tracing::warn; struct ObserverIvars { @@ -190,9 +190,9 @@ mod windows { use ::windows::Win32::UI::WindowsAndMessaging::{ DEVICE_NOTIFY_CALLBACK, PBT_APMRESUMEAUTOMATIC, PBT_APMSUSPEND, }; + use cap_desktop_runtime::AppHandle; use std::ffi::c_void; use std::sync::{Mutex, OnceLock}; - use tauri::AppHandle; use tracing::warn; struct RegistrationHandle { diff --git a/apps/desktop/src-tauri/src/presets.rs b/apps/desktop/src-backend/src/presets.rs similarity index 97% rename from apps/desktop/src-tauri/src/presets.rs rename to apps/desktop/src-backend/src/presets.rs index c3d8d2f3c8c..cb54eaafc18 100644 --- a/apps/desktop/src-tauri/src/presets.rs +++ b/apps/desktop/src-backend/src/presets.rs @@ -1,9 +1,8 @@ +use cap_desktop_runtime::{AppHandle, Wry}; use cap_project::{ProjectConfiguration, TimelineConfiguration}; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; -use tauri::{AppHandle, Wry}; -use tauri_plugin_store::StoreExt; use tracing::error; #[derive(Serialize, Deserialize, Type, Debug, Default)] diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-backend/src/recording.rs similarity index 98% rename from apps/desktop/src-tauri/src/recording.rs rename to apps/desktop/src-backend/src/recording.rs index 31c97af3525..3ea538aecaa 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-backend/src/recording.rs @@ -1,4 +1,6 @@ use anyhow::anyhow; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager, path::BaseDirectory}; use cap_fail::fail; use cap_media_info::ffmpeg_sample_format_for; use cap_project::CursorMoveEvent; @@ -46,10 +48,6 @@ use std::{ sync::Arc, time::Duration, }; -use tauri::{AppHandle, Manager, path::BaseDirectory}; -use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder}; -use tauri_plugin_global_shortcut::GlobalShortcutExt; -use tauri_specta::Event; use tracing::*; use crate::camera::{CameraPreviewManager, CameraPreviewShape}; @@ -175,7 +173,7 @@ async fn store_current_desktop_background_snapshot( .map_err(|err| format!("Desktop background snapshot task failed: {err}"))? } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn import_current_desktop_background(project_path: String) -> Result { @@ -837,7 +835,7 @@ impl CompletedRecording { } } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] pub async fn list_capture_displays() -> Vec { screen_capture::list_displays() @@ -846,9 +844,9 @@ pub async fn list_capture_displays() -> Vec { .collect() } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] -pub async fn list_capture_windows(window: tauri::Window) -> Vec { +pub async fn list_capture_windows(window: cap_desktop_runtime::Window) -> Vec { let windows = if window.label() == CapWindowId::Settings.label() { screen_capture::list_excludable_windows() } else { @@ -858,7 +856,7 @@ pub async fn list_capture_windows(window: tauri::Window) -> Vec { windows.into_iter().map(|(v, _)| v).collect() } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] pub fn list_cameras() -> Vec { if !permissions::do_permissions_check(false).camera.permitted() { @@ -945,7 +943,7 @@ fn get_best_format(formats: &[CameraFormatInfo]) -> Option { ideal_formats.into_iter().next().cloned() } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] pub fn get_camera_formats(device_id: String) -> Option { if !permissions::do_permissions_check(false).camera.permitted() { @@ -994,7 +992,7 @@ pub struct MicrophoneFormatInfo { pub channels: u16, } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] pub fn get_microphone_info(name: String) -> Option { if !permissions::do_permissions_check(false) @@ -1053,14 +1051,14 @@ fn microphone_format_infos(device: &cpal::Device) -> Vec { .collect() } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn list_displays_with_thumbnails() -> Result, String> { run_non_send_thumbnail_future(collect_displays_with_thumbnails()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument] pub async fn list_windows_with_thumbnails() -> Result, String> { @@ -1073,9 +1071,9 @@ where { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { if tokio::runtime::Handle::try_current().is_ok() { - tokio::task::block_in_place(|| tauri::async_runtime::block_on(future)) + tokio::task::block_in_place(|| cap_desktop_runtime::async_runtime::block_on(future)) } else { - tauri::async_runtime::block_on(future) + cap_desktop_runtime::async_runtime::block_on(future) } })); @@ -1123,7 +1121,7 @@ pub enum RecordingInputKind { Camera, } -#[derive(tauri_specta::Event, specta::Type, Clone, Debug, serde::Serialize)] +#[derive(cap_desktop_runtime::Event, specta::Type, Clone, Debug, serde::Serialize)] #[serde(tag = "variant")] pub enum RecordingEvent { Countdown { value: u32 }, @@ -1479,7 +1477,7 @@ pub fn format_project_name<'a>( result.into_owned() } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[tracing::instrument(name = "recording", skip_all)] pub async fn start_recording( @@ -2199,18 +2197,14 @@ pub async fn start_recording( } .emit(&app); - let mut dialog = MessageDialogBuilder::new( - app.dialog().clone(), - "An error occurred".to_string(), - error.clone(), - ) - .kind(tauri_plugin_dialog::MessageDialogKind::Error); - - if let Some(window) = CapWindowId::RecordingControls.get(&app) { - dialog = dialog.parent(&window); - } - - dialog.blocking_show(); + let _ = app.native_operation( + "dialog.message", + serde_json::json!({ + "title": "An error occurred", + "message": error, + "kind": "error" + }), + ); handle_recording_end(app, Err(error), &mut state, project_file_path) .await @@ -2415,7 +2409,7 @@ pub async fn start_recording( Ok(RecordingAction::Started) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] pub async fn pause_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { @@ -2429,7 +2423,7 @@ pub async fn pause_recording(app: AppHandle, state: MutableState<'_, App>) -> Re Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] pub async fn resume_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { @@ -2443,7 +2437,7 @@ pub async fn resume_recording(app: AppHandle, state: MutableState<'_, App>) -> R Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(state))] pub async fn set_mic_recording_muted( @@ -2476,7 +2470,7 @@ pub async fn set_mic_recording_muted( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] pub async fn toggle_pause_recording( @@ -2521,18 +2515,14 @@ async fn handle_spawn_failure( message.contains("no longer available") || message.contains("DeviceNotFound"); if !is_device_not_found { - let mut dialog = MessageDialogBuilder::new( - app.dialog().clone(), - "An error occurred".to_string(), - message.clone(), - ) - .kind(tauri_plugin_dialog::MessageDialogKind::Error); - - if let Some(window) = CapWindowId::RecordingControls.get(app) { - dialog = dialog.parent(&window); - } - - dialog.blocking_show(); + let _ = app.native_operation( + "dialog.message", + serde_json::json!({ + "title": "An error occurred", + "message": message, + "kind": "error" + }), + ); } let mut state = state_mtx.write().await; @@ -2727,7 +2717,7 @@ async fn discard_recording(app: &AppHandle, recording: InProgressRecording) -> R local_delete } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { @@ -2770,7 +2760,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] pub async fn restart_recording( @@ -2805,7 +2795,7 @@ pub async fn restart_recording( start_recording(app.clone(), state, inputs).await } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] #[instrument(skip(app, state))] pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { @@ -2846,7 +2836,7 @@ pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> R Ok(()) } -#[tauri::command(async)] +#[cap_desktop_runtime::command(async)] #[specta::specta] #[tracing::instrument(name = "take_screenshot", skip(app))] pub async fn take_screenshot( @@ -2995,7 +2985,7 @@ pub async fn take_screenshot( let app_handle = app.clone(); let pending_state = PendingScreenshots(pending_screenshots.0.clone()); - tauri::async_runtime::spawn(async move { + cap_desktop_runtime::async_runtime::spawn(async move { let encode_result = tokio::task::spawn_blocking(move || -> Result<(), String> { let file = std::fs::File::create(&image_path_for_write) .map_err(|e| format!("Failed to create screenshot file: {e}"))?; @@ -3187,7 +3177,7 @@ async fn handle_recording_end( #[cfg(not(windows))] hide_overlay(&window); if let Some(ref fm) = focus_manager { - fm.destroy(&display_id, handle.global_shortcut()); + fm.destroy(&display_id, &handle); } } } diff --git a/apps/desktop/src-tauri/src/recording_settings.rs b/apps/desktop/src-backend/src/recording_settings.rs similarity index 97% rename from apps/desktop/src-tauri/src/recording_settings.rs rename to apps/desktop/src-backend/src/recording_settings.rs index c8935289818..35b2c3afc5d 100644 --- a/apps/desktop/src-tauri/src/recording_settings.rs +++ b/apps/desktop/src-backend/src/recording_settings.rs @@ -1,3 +1,4 @@ +use cap_desktop_runtime::{AppHandle, Wry}; use cap_recording::{ RecordingMode, feeds::{ @@ -7,8 +8,6 @@ use cap_recording::{ sources::screen_capture::ScreenCaptureTarget, }; use std::collections::HashMap; -use tauri::{AppHandle, Wry}; -use tauri_plugin_store::StoreExt; use crate::tray; @@ -87,7 +86,7 @@ pub fn camera_key(id: &DeviceOrModelID) -> String { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub fn set_recording_mode(app: AppHandle, mode: RecordingMode) -> Result<(), String> { RecordingSettingsStore::set_mode(&app, mode)?; diff --git a/apps/desktop/src-tauri/src/recording_telemetry.rs b/apps/desktop/src-backend/src/recording_telemetry.rs similarity index 100% rename from apps/desktop/src-tauri/src/recording_telemetry.rs rename to apps/desktop/src-backend/src/recording_telemetry.rs diff --git a/apps/desktop/src-tauri/src/recordings_locations.rs b/apps/desktop/src-backend/src/recordings_locations.rs similarity index 99% rename from apps/desktop/src-tauri/src/recordings_locations.rs rename to apps/desktop/src-backend/src/recordings_locations.rs index 13f3c57ea55..e1948385a90 100644 --- a/apps/desktop/src-tauri/src/recordings_locations.rs +++ b/apps/desktop/src-backend/src/recordings_locations.rs @@ -3,10 +3,10 @@ use std::{ path::{Path, PathBuf}, }; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager, Wry}; use serde::{Deserialize, Serialize}; use specta::Type; -use tauri::{AppHandle, Manager, Wry}; -use tauri_specta::Event; use tracing::{info, warn}; use crate::{ @@ -147,7 +147,7 @@ fn migratable_projects(app: &AppHandle) -> Vec { projects } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn count_recordings_to_migrate(app: AppHandle) -> Result { let app = app.clone(); @@ -168,7 +168,7 @@ impl Drop for MigrationLockGuard { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn migrate_recordings_to_current_dir( app: AppHandle, diff --git a/apps/desktop/src-tauri/src/recovery.rs b/apps/desktop/src-backend/src/recovery.rs similarity index 97% rename from apps/desktop/src-tauri/src/recovery.rs rename to apps/desktop/src-backend/src/recovery.rs index 20d00e0df62..ec851eef281 100644 --- a/apps/desktop/src-tauri/src/recovery.rs +++ b/apps/desktop/src-backend/src/recovery.rs @@ -1,10 +1,10 @@ +use cap_desktop_runtime::AppHandle; use cap_project::StudioRecordingMeta; use cap_recording::recovery::RecoveryManager; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use specta::Type; use std::path::PathBuf; -use tauri::AppHandle; use tracing::info; use crate::create_screenshot; @@ -39,7 +39,7 @@ pub struct IncompleteRecordingInfo { pub estimated_duration_secs: f64, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn find_incomplete_recordings( app: AppHandle, @@ -68,7 +68,7 @@ pub async fn find_incomplete_recordings( Ok(result) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn recover_recording(app: AppHandle, project_path: String) -> Result { let path = PathBuf::from(&project_path); @@ -144,7 +144,7 @@ pub async fn recover_recording(app: AppHandle, project_path: String) -> Result Result<(), String> { let path = PathBuf::from(&project_path); diff --git a/apps/desktop/src-tauri/src/resource.rs b/apps/desktop/src-backend/src/resource.rs similarity index 100% rename from apps/desktop/src-tauri/src/resource.rs rename to apps/desktop/src-backend/src/resource.rs diff --git a/apps/desktop/src-tauri/src/screenshot_editor.rs b/apps/desktop/src-backend/src/screenshot_editor.rs similarity index 99% rename from apps/desktop/src-tauri/src/screenshot_editor.rs rename to apps/desktop/src-backend/src/screenshot_editor.rs index 222da938b07..957303c7426 100644 --- a/apps/desktop/src-tauri/src/screenshot_editor.rs +++ b/apps/desktop/src-backend/src/screenshot_editor.rs @@ -2,6 +2,10 @@ use crate::PendingScreenshots; use crate::frame_ws::{WSFrame, create_watch_frame_ws}; use crate::gpu_context; use crate::windows::{CapWindowId, ScreenshotEditorWindowIds}; +use cap_desktop_runtime::{ + AppHandle, CommandContext, Manager, Window, + ipc::{CommandArg, InvokeError}, +}; use cap_project::{ ProjectConfiguration, RecordingMeta, RecordingMetaInner, SingleSegment, StudioRecordingMeta, VideoMeta, @@ -20,10 +24,6 @@ use std::io::Cursor; use std::str::FromStr; use std::time::Instant; use std::{collections::HashMap, ops::Deref, path::PathBuf, sync::Arc}; -use tauri::{ - AppHandle, Manager, Runtime, Window, - ipc::{CommandArg, InvokeError}, -}; use tokio::sync::{RwLock, watch}; use tokio_util::sync::CancellationToken; @@ -83,9 +83,9 @@ impl Deref for WindowScreenshotEditorInstance { } } -impl<'de, R: Runtime> CommandArg<'de, R> for WindowScreenshotEditorInstance { - fn from_command(command: tauri::ipc::CommandItem<'de, R>) -> Result { - let window = Window::from_command(command)?; +impl CommandArg for WindowScreenshotEditorInstance { + fn from_command(context: &CommandContext) -> Result { + let window = context.window(); let instances = window.state::(); let instance = futures::executor::block_on(instances.0.read()); @@ -738,7 +738,7 @@ struct ScreenshotOcrImage { height: u32, } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn create_screenshot_editor_instance( window: Window, @@ -906,7 +906,7 @@ pub async fn prewarm_screenshot_renderer() { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn prewarm_screenshot_background(path: String) -> Result<(), String> { let Some(gpu) = gpu_context::get_shared_gpu().await else { @@ -925,7 +925,7 @@ pub async fn prewarm_screenshot_background(path: String) -> Result<(), String> { Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn update_screenshot_config( instance: WindowScreenshotEditorInstance, @@ -961,7 +961,7 @@ pub async fn update_screenshot_config( Ok(()) } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn recognize_screenshot_text( instance: WindowScreenshotEditorInstance, @@ -1450,7 +1450,7 @@ fn clamp_f32_to_u32(value: f32) -> u32 { } } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn render_screenshot_for_export( instance: WindowScreenshotEditorInstance, @@ -1458,7 +1458,7 @@ pub async fn render_screenshot_for_export( render_screenshot_png(&instance).await } -#[tauri::command] +#[cap_desktop_runtime::command] #[specta::specta] pub async fn render_screenshot_project_for_export( app: AppHandle, diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-backend/src/target_select_overlay.rs similarity index 90% rename from apps/desktop/src-tauri/src/target_select_overlay.rs rename to apps/desktop/src-backend/src/target_select_overlay.rs index f48963b398e..6013a2a7123 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-backend/src/target_select_overlay.rs @@ -15,19 +15,18 @@ use crate::{ window_exclusion::WindowExclusion, windows::{CapWindowId, ShowCapWindow, hide_overlay, show_overlay}, }; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager, WebviewWindow}; use scap_targets::{ Display, DisplayId, Window, WindowId, bounds::{LogicalBounds, LogicalSize, PhysicalSize}, }; use serde::Serialize; use specta::Type; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_plugin_global_shortcut::{GlobalShortcut, GlobalShortcutExt}; -use tauri_specta::Event; use tokio::task::JoinHandle; use tracing::{debug, error, instrument}; -#[derive(tauri_specta::Event, Serialize, Type, Clone)] +#[derive(cap_desktop_runtime::Event, Serialize, Type, Clone)] pub struct TargetUnderCursor { display_id: Option, window: Option, @@ -50,11 +49,11 @@ pub struct DisplayInformation { } #[specta::specta] -#[tauri::command] +#[cap_desktop_runtime::command] #[instrument(skip(app, state))] pub async fn open_target_select_overlays( app: AppHandle, - state: tauri::State<'_, WindowFocusManager>, + state: cap_desktop_runtime::State<'_, WindowFocusManager>, focused_target: Option, specific_display_id: Option, target_mode: Option, @@ -99,7 +98,7 @@ pub async fn open_target_select_overlays( .any(|display_id| display_id == &existing_id) { hide_overlay(&window); - state.destroy(&existing_id, app.global_shortcut()); + state.destroy(&existing_id, &app); } } @@ -210,7 +209,7 @@ pub async fn open_target_select_overlays( { task.abort(); } - state.register_escape(app.global_shortcut()); + state.register_escape(&app); Ok(()) } @@ -260,7 +259,7 @@ fn should_skip_window(window: &Window, exclusions: &[WindowExclusion]) -> bool { } #[specta::specta] -#[tauri::command] +#[cap_desktop_runtime::command] #[instrument(skip(app))] pub async fn update_camera_overlay_bounds( app: AppHandle, @@ -277,16 +276,20 @@ pub async fn update_camera_overlay_bounds( let height_u32 = height as u32; window - .set_size(tauri::Size::Physical(tauri::PhysicalSize { - width: width_u32, - height: height_u32, - })) + .set_size(cap_desktop_runtime::Size::Physical( + cap_desktop_runtime::PhysicalSize { + width: width_u32, + height: height_u32, + }, + )) .map_err(|e| e.to_string())?; window - .set_position(tauri::Position::Physical(tauri::PhysicalPosition { - x: x as i32, - y: y as i32, - })) + .set_position(cap_desktop_runtime::Position::Physical( + cap_desktop_runtime::PhysicalPosition { + x: x as i32, + y: y as i32, + }, + )) .map_err(|e| e.to_string())?; let scale_factor = window.scale_factor().unwrap_or(1.0); @@ -303,11 +306,11 @@ pub async fn update_camera_overlay_bounds( } #[specta::specta] -#[tauri::command] +#[cap_desktop_runtime::command] #[instrument(skip(app, _state))] pub async fn close_target_select_overlays( app: AppHandle, - _state: tauri::State<'_, WindowFocusManager>, + _state: cap_desktop_runtime::State<'_, WindowFocusManager>, ) -> Result<(), String> { close_target_select_overlay_windows(&app); @@ -332,7 +335,7 @@ pub fn close_target_select_overlay_windows(app: &AppHandle) { #[cfg(not(windows))] hide_overlay(&window); if let Some(state) = state.as_ref() { - state.destroy(&display_id, app.global_shortcut()); + state.destroy(&display_id, app); } } } @@ -343,7 +346,7 @@ pub fn close_target_select_overlay_windows(app: &AppHandle) { } #[specta::specta] -#[tauri::command] +#[cap_desktop_runtime::command] #[instrument] pub async fn get_window_icon(window_id: &str) -> Result, String> { let window_id = window_id @@ -357,7 +360,7 @@ pub async fn get_window_icon(window_id: &str) -> Result, String> } #[specta::specta] -#[tauri::command] +#[cap_desktop_runtime::command] #[instrument] pub async fn display_information(display_id: &str) -> Result { let display_id = display_id @@ -375,7 +378,7 @@ pub async fn display_information(display_id: &str) -> Result Result<(), String> { let window = Window::from_id(&window_id).ok_or("Window not found")?; @@ -480,8 +483,7 @@ impl WindowFocusManager { if main_window_was_seen && !main_window_available && !settings_window_available { hide_overlay(&window); - app.state::() - .finish(&display_id, app.global_shortcut()); + app.state::().finish(&display_id, app); break; } @@ -543,19 +545,19 @@ impl WindowFocusManager { labels.clear(); } - fn finish(&self, id: &DisplayId, global_shortcut: &GlobalShortcut) { + fn finish(&self, id: &DisplayId, app: &AppHandle) { let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); tasks.remove(&id.to_string()); drop(tasks); - self.finish_if_idle(global_shortcut); + self.finish_if_idle(app); } - fn finish_if_idle(&self, global_shortcut: &GlobalShortcut) { + fn finish_if_idle(&self, app: &AppHandle) { let tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); if tasks.is_empty() { drop(tasks); - self.unregister_escape(global_shortcut); + self.unregister_escape(app); if let Some(task) = self .task @@ -569,23 +571,23 @@ impl WindowFocusManager { } /// Called when a specific overlay window is destroyed to cleanup it's resources - pub fn destroy(&self, id: &DisplayId, global_shortcut: &GlobalShortcut) { + pub fn destroy(&self, id: &DisplayId, app: &AppHandle) { let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); if let Some(task) = tasks.remove(&id.to_string()) { task.abort(); } drop(tasks); - self.finish_if_idle(global_shortcut); + self.finish_if_idle(app); } pub fn shutdown(&self, app: &AppHandle) { self.abort_all_tasks(); - self.unregister_escape(app.global_shortcut()); + self.unregister_escape(app); } - fn register_escape(&self, global_shortcut: &GlobalShortcut) { + fn register_escape(&self, app: &AppHandle) { let mut registered = self .escape_registered .lock() @@ -594,13 +596,16 @@ impl WindowFocusManager { return; } - match global_shortcut.register("Escape") { + match app.native_operation( + "hotkeys.escapeEnabled", + serde_json::json!({ "enabled": true }), + ) { Ok(()) => *registered = true, Err(err) => error!("Error registering global keyboard shortcut for Escape: {err}"), } } - fn unregister_escape(&self, global_shortcut: &GlobalShortcut) { + fn unregister_escape(&self, app: &AppHandle) { { let mut registered = self .escape_registered @@ -612,7 +617,10 @@ impl WindowFocusManager { *registered = false; } - if let Err(err) = global_shortcut.unregister("Escape") { + if let Err(err) = app.native_operation( + "hotkeys.escapeEnabled", + serde_json::json!({ "enabled": false }), + ) { debug!("Error unregistering global keyboard shortcut for Escape: {err}"); } } diff --git a/apps/desktop/src-tauri/src/telemetry.rs b/apps/desktop/src-backend/src/telemetry.rs similarity index 99% rename from apps/desktop/src-tauri/src/telemetry.rs rename to apps/desktop/src-backend/src/telemetry.rs index 10d1d908667..6ea30f03718 100644 --- a/apps/desktop/src-tauri/src/telemetry.rs +++ b/apps/desktop/src-backend/src/telemetry.rs @@ -1,3 +1,4 @@ +use cap_desktop_runtime::AppHandle; use serde::Serialize; use serde_json::{Map, Value}; use std::{ @@ -7,7 +8,6 @@ use std::{ }, time::Duration, }; -use tauri::AppHandle; use tracing::debug; use crate::auth::AuthStore; @@ -18,7 +18,7 @@ const CLIENT_ID: Option<&str> = option_env!("VITE_OPENPANEL_CLIENT_ID"); /// same auth path the webview SDK uses — so no credential ships in the binary. /// Must stay on the OpenPanel client's CORS allowlist alongside the webview /// origins. -const ORIGIN: &str = "tauri://localhost"; +const ORIGIN: &str = "cap://app"; const API_URL: Option<&str> = option_env!("VITE_OPENPANEL_API_URL"); const DEFAULT_API_URL: &str = "https://api.openpanel.dev"; diff --git a/apps/desktop/src-tauri/src/thumbnails/linux.rs b/apps/desktop/src-backend/src/thumbnails/linux.rs similarity index 100% rename from apps/desktop/src-tauri/src/thumbnails/linux.rs rename to apps/desktop/src-backend/src/thumbnails/linux.rs diff --git a/apps/desktop/src-tauri/src/thumbnails/mac.rs b/apps/desktop/src-backend/src/thumbnails/mac.rs similarity index 100% rename from apps/desktop/src-tauri/src/thumbnails/mac.rs rename to apps/desktop/src-backend/src/thumbnails/mac.rs diff --git a/apps/desktop/src-tauri/src/thumbnails/mod.rs b/apps/desktop/src-backend/src/thumbnails/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/thumbnails/mod.rs rename to apps/desktop/src-backend/src/thumbnails/mod.rs diff --git a/apps/desktop/src-tauri/src/thumbnails/windows.rs b/apps/desktop/src-backend/src/thumbnails/windows.rs similarity index 100% rename from apps/desktop/src-tauri/src/thumbnails/windows.rs rename to apps/desktop/src-backend/src/thumbnails/windows.rs diff --git a/apps/desktop/src-backend/src/tray.rs b/apps/desktop/src-backend/src/tray.rs new file mode 100644 index 00000000000..b6f1276261b --- /dev/null +++ b/apps/desktop/src-backend/src/tray.rs @@ -0,0 +1,204 @@ +use cap_desktop_runtime::{AppHandle, Event}; +use cap_recording::RecordingMode; +use serde_json::json; + +use crate::{ + RecordingStarted, RecordingStopped, RequestOpenSettings, recording, + recording_settings::{RecordingSettingsStore, RecordingTargetMode}, + windows::ShowCapWindow, +}; + +fn current_mode(app: &AppHandle) -> RecordingMode { + RecordingSettingsStore::get(app) + .ok() + .flatten() + .and_then(|settings| settings.mode) + .unwrap_or_default() +} + +fn menu(app: &AppHandle) -> serde_json::Value { + let mode = current_mode(app); + let mut items = vec![ + json!({ "id": "open_cap", "text": "Open Cap" }), + json!({ "type": "separator" }), + json!({ "id": "record_display", "text": "Record Display" }), + json!({ "id": "record_window", "text": "Record Window" }), + json!({ "id": "record_area", "text": "Record Area" }), + json!({ "id": "take_screenshot", "text": "Take Screenshot" }), + json!({ "id": "import_video", "text": "Import Media" }), + json!({ + "text": "Recording Mode", + "items": [ + { "id": "mode_studio", "text": "Studio", "checked": mode == RecordingMode::Studio }, + { "id": "mode_instant", "text": "Instant", "checked": mode == RecordingMode::Instant }, + { "id": "mode_screenshot", "text": "Screenshot", "checked": mode == RecordingMode::Screenshot } + ] + }), + json!({ "type": "separator" }), + json!({ "id": "view_all_recordings", "text": "View Recordings" }), + json!({ "id": "view_all_screenshots", "text": "View Screenshots" }), + json!({ "id": "open_settings", "text": "Settings" }), + ]; + if !crate::permissions::do_permissions_check(false).necessary_granted() { + items.push(json!({ "id": "request_permissions", "text": "Grant Permissions" })); + } + items.extend([ + json!({ "id": "upload_logs", "text": "Upload Logs" }), + json!({ "type": "separator" }), + json!({ "id": "quit", "text": "Quit Cap" }), + ]); + json!({ "items": items, "mode": mode }) +} + +pub(crate) fn refresh_tray_menu_for_app(app: &AppHandle) { + let _ = app.native_operation("tray.configure", menu(app)); +} + +pub fn update_tray_icon_for_mode(app: &AppHandle, mode: RecordingMode) { + let _ = app.native_operation("tray.setMode", json!({ "mode": mode })); + refresh_tray_menu_for_app(app); +} + +pub fn create_tray(app: &AppHandle) -> cap_desktop_runtime::Result<()> { + refresh_tray_menu_for_app(app); + + let app_handle = app.clone(); + app.listen("tray://click", move |event| { + let Ok(id) = serde_json::from_str::(event.payload()) else { + return; + }; + dispatch(app_handle.clone(), id); + }); + + let app_handle = app.clone(); + RecordingStarted::listen_any(app, move |_| { + let _ = app_handle.native_operation("tray.setRecording", json!({ "recording": true })); + }); + let app_handle = app.clone(); + RecordingStopped::listen_any(app, move |_| { + let _ = app_handle.native_operation("tray.setRecording", json!({ "recording": false })); + update_tray_icon_for_mode(&app_handle, current_mode(&app_handle)); + }); + Ok(()) +} + +fn dispatch(app: AppHandle, id: String) { + tokio::spawn(async move { + match id.as_str() { + "open_cap" => { + let _ = ShowCapWindow::Main { + init_target_mode: None, + } + .show(&app) + .await; + } + "record_display" => crate::open_target_picker(&app, RecordingTargetMode::Display).await, + "record_window" => crate::open_target_picker(&app, RecordingTargetMode::Window).await, + "record_area" => crate::open_target_picker(&app, RecordingTargetMode::Area).await, + "take_screenshot" => { + use cap_recording::screen_capture::ScreenCaptureTarget; + use scap_targets::Display; + let display = Display::get_containing_cursor().unwrap_or_else(Display::primary); + let target = ScreenCaptureTarget::Display { id: display.id() }; + match recording::take_screenshot(app.clone(), target.clone()).await { + Ok(path) if crate::automation::should_open_screenshot_editor(&app, &target) => { + let _ = ShowCapWindow::ScreenshotEditor { path }.show(&app).await; + } + Ok(_) => {} + Err(error) => tracing::error!(%error, "Failed to take screenshot from tray"), + } + } + "stop_recording" => { + let _ = recording::stop_recording(app.clone(), app.state()).await; + } + "import_video" => import_media(&app).await, + "view_all_recordings" => { + let _ = RequestOpenSettings { + page: "recordings".to_string(), + } + .emit(&app); + } + "view_all_screenshots" => { + let _ = RequestOpenSettings { + page: "screenshots".to_string(), + } + .emit(&app); + } + "open_settings" => { + let _ = ShowCapWindow::Settings { page: None }.show(&app).await; + } + "upload_logs" => { + let result = crate::logging::upload_log_file(&app).await; + let message = if result.is_ok() { + "Logs uploaded successfully" + } else { + "Failed to upload logs" + }; + let _ = app.native_operation("dialog.message", json!({ "message": message })); + } + "mode_studio" => set_mode(&app, RecordingMode::Studio), + "mode_instant" => set_mode(&app, RecordingMode::Instant), + "mode_screenshot" => set_mode(&app, RecordingMode::Screenshot), + "request_permissions" => { + let _ = ShowCapWindow::Onboarding.show(&app).await; + } + "quit" => crate::request_app_exit(app.clone()).await, + _ => {} + } + }); +} + +fn set_mode(app: &AppHandle, mode: RecordingMode) { + if let Err(error) = RecordingSettingsStore::set_mode(app, mode) { + tracing::error!(%error, "Failed to set recording mode from tray"); + return; + } + update_tray_icon_for_mode(app, mode); +} + +async fn import_media(app: &AppHandle) { + let path = match app + .native_request::, _>( + "dialog.open", + json!({ + "title": "Import Media", + "filters": [{ + "name": "Media Files", + "extensions": ["mp4", "mov", "avi", "mkv", "webm", "wmv", "m4v", "flv", "png", "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff"] + }] + }), + ) + .await + { + Ok(Some(path)) => std::path::PathBuf::from(path), + Ok(None) => return, + Err(error) => { + tracing::error!(%error, "Failed to open import dialog"); + return; + } + }; + + let result = if crate::import::is_supported_video_import_path(&path) { + crate::import::start_video_import(app.clone(), path) + .await + .map(|project_path| ShowCapWindow::Editor { project_path }) + } else if crate::import::is_supported_image_import_path(&path) { + crate::import::start_image_import(app.clone(), path) + .await + .map(|path| ShowCapWindow::ScreenshotEditor { path }) + } else { + Err("Unsupported media file".to_string()) + }; + + match result { + Ok(window) => { + let _ = window.show(app).await; + } + Err(error) => { + let _ = app.native_operation( + "dialog.message", + json!({ "title": "Import Error", "message": error, "kind": "error" }), + ); + } + } +} diff --git a/apps/desktop/src-tauri/src/update_project_names.rs b/apps/desktop/src-backend/src/update_project_names.rs similarity index 99% rename from apps/desktop/src-tauri/src/update_project_names.rs rename to apps/desktop/src-backend/src/update_project_names.rs index 8d008617064..d7d9a5a501f 100644 --- a/apps/desktop/src-tauri/src/update_project_names.rs +++ b/apps/desktop/src-backend/src/update_project_names.rs @@ -4,10 +4,9 @@ use std::{ sync::Arc, }; +use cap_desktop_runtime::AppHandle; use cap_project::RecordingMeta; use futures::StreamExt; -use tauri::AppHandle; -use tauri_plugin_store::StoreExt; use tokio::{fs, sync::Mutex}; use crate::recordings_path; diff --git a/apps/desktop/src-backend/src/updates.rs b/apps/desktop/src-backend/src/updates.rs new file mode 100644 index 00000000000..c37d55c2d09 --- /dev/null +++ b/apps/desktop/src-backend/src/updates.rs @@ -0,0 +1,82 @@ +use cap_desktop_runtime::AppHandle; +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::general_settings::GeneralSettingsStore; + +#[derive(Serialize, Deserialize, Type, Clone, Copy, PartialEq, Eq, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub enum UpdateChannel { + #[default] + Stable, + Nightly, +} + +#[derive(Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct UpdateCheckResult { + pub version: String, + pub notes: Option, + pub channel: UpdateChannel, +} + +#[derive(Serialize, Type, cap_desktop_runtime::Event, Clone, Debug)] +pub struct UpdateDownloadProgress { + pub downloaded: u32, + pub total: Option, +} + +#[derive(Serialize, Type, cap_desktop_runtime::Event, Clone, Debug)] +pub struct UpdateReady { + pub version: String, + pub installed: bool, +} + +#[derive(Default)] +pub struct UpdatesState; + +fn current_channel(app: &AppHandle) -> UpdateChannel { + GeneralSettingsStore::get(app) + .ok() + .flatten() + .map(|settings| settings.update_channel) + .unwrap_or_default() +} + +#[cap_desktop_runtime::command] +#[specta::specta] +pub async fn updates_check(app: AppHandle) -> Result, String> { + let channel = current_channel(&app); + app.native_request("updater.check", serde_json::json!({ "channel": channel })) + .await +} + +#[cap_desktop_runtime::command] +#[specta::specta] +pub async fn updates_download_and_install(app: AppHandle) -> Result<(), String> { + app.native_request::( + "updater.downloadAndInstall", + serde_json::json!({ "channel": current_channel(&app) }), + ) + .await + .map(|_| ()) +} + +#[cap_desktop_runtime::command] +#[specta::specta] +pub fn updates_channel_changed(app: AppHandle) -> Result<(), String> { + app.native_operation( + "updater.setChannel", + serde_json::json!({ "channel": current_channel(&app) }), + ) +} + +pub fn spawn_background_loop(app: AppHandle) { + if cfg!(debug_assertions) { + return; + } + let _ = app.native_operation( + "updater.configure", + serde_json::json!({ "channel": current_channel(&app) }), + ); +} diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-backend/src/upload.rs similarity index 99% rename from apps/desktop/src-tauri/src/upload.rs rename to apps/desktop/src-backend/src/upload.rs index 8d197da43c1..cb1d7c0b8d3 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-backend/src/upload.rs @@ -9,6 +9,8 @@ use crate::{ }; use async_stream::{stream, try_stream}; use bytes::Bytes; +use cap_desktop_runtime::Event; +use cap_desktop_runtime::{AppHandle, Manager, ipc::Channel}; use cap_project::{RecordingMeta, S3UploadMeta, UploadMeta}; use cap_utils::spawn_actor; use ffmpeg::ffi::AV_TIME_BASE; @@ -27,9 +29,6 @@ use std::{ sync::{Arc, Mutex, PoisonError}, time::Duration, }; -use tauri::{AppHandle, Manager, ipc::Channel}; -use tauri_plugin_clipboard_manager::ClipboardExt; -use tauri_specta::Event; use tokio::{ fs::File, io::{AsyncReadExt, AsyncSeekExt, BufReader}, @@ -47,7 +46,7 @@ pub struct UploadedItem { // pub config: S3UploadMeta, } -#[derive(Clone, Serialize, Type, tauri_specta::Event)] +#[derive(Clone, Serialize, Type, cap_desktop_runtime::Event)] pub struct UploadProgressEvent { video_id: String, uploaded: String, @@ -690,7 +689,10 @@ impl InstantMultipartUpload { .save_for_project() .map_err(|err| format!("Error reading project meta from {recording_dir:?}: {err}"))?; - let _ = app.clipboard().write_text(pre_created_video.link.clone()); + let _ = app.native_operation( + "clipboard.writeText", + serde_json::json!({ "text": pre_created_video.link }), + ); Ok(metadata) } @@ -1634,7 +1636,10 @@ impl SegmentUploader { .save_for_project() .map_err(|err| format!("Error saving project meta for {recording_dir:?}: {err}"))?; - let _ = app.clipboard().write_text(pre_created_video.link.clone()); + let _ = app.native_operation( + "clipboard.writeText", + serde_json::json!({ "text": pre_created_video.link }), + ); let total_bytes = state .lock() diff --git a/apps/desktop/src-tauri/src/web_api.rs b/apps/desktop/src-backend/src/web_api.rs similarity index 99% rename from apps/desktop/src-tauri/src/web_api.rs rename to apps/desktop/src-backend/src/web_api.rs index a1bf685c4d7..4cf1252ea99 100644 --- a/apps/desktop/src-tauri/src/web_api.rs +++ b/apps/desktop/src-backend/src/web_api.rs @@ -1,5 +1,5 @@ +use cap_desktop_runtime::{Emitter, Manager, Runtime}; use reqwest::StatusCode; -use tauri::{Emitter, Manager, Runtime}; use thiserror::Error; use tracing::{debug, error, warn}; diff --git a/apps/desktop/src-tauri/src/window_exclusion.rs b/apps/desktop/src-backend/src/window_exclusion.rs similarity index 94% rename from apps/desktop/src-tauri/src/window_exclusion.rs rename to apps/desktop/src-backend/src/window_exclusion.rs index 23ef1e373f5..dfa0eafffb4 100644 --- a/apps/desktop/src-tauri/src/window_exclusion.rs +++ b/apps/desktop/src-backend/src/window_exclusion.rs @@ -120,12 +120,12 @@ pub fn resolve_window_ids(exclusions: &[WindowExclusion]) -> Vec { #[cfg(target_os = "macos")] pub fn append_matching_webview_window_ids( ids: &mut Vec, - app: &tauri::AppHandle, + app: &cap_desktop_runtime::AppHandle, exclusions: &[WindowExclusion], ) { use crate::windows::CapWindowId; + use cap_desktop_runtime::Manager; use std::str::FromStr; - use tauri::Manager; for (label, window) in app.webview_windows() { let Ok(window_id) = CapWindowId::from_str(&label) else { @@ -139,7 +139,7 @@ pub fn append_matching_webview_window_ids( warn!( label = %label, title = %title, - "Excluded Tauri webview window has no native window id" + "Excluded Electron window has no native window id" ); continue; }; @@ -149,14 +149,14 @@ pub fn append_matching_webview_window_ids( window_id = %native_id, label = %label, title = %title, - "Excluded Tauri webview window id is not visible to CGWindowList" + "Excluded Electron window id is not visible to CGWindowList" ); } else { debug!( window_id = %native_id, label = %label, title = %title, - "Skipping hidden excluded Tauri webview window" + "Skipping hidden excluded Electron window" ); } continue; @@ -166,7 +166,7 @@ pub fn append_matching_webview_window_ids( window_id = %native_id, label = %label, title = %title, - "Excluded Tauri webview window id already resolved" + "Excluded Electron window id already resolved" ); continue; } @@ -174,22 +174,21 @@ pub fn append_matching_webview_window_ids( window_id = %native_id, label = %label, title = %title, - "Resolved excluded Tauri webview window" + "Resolved excluded Electron window" ); ids.push(native_id); } } #[cfg(target_os = "macos")] -fn webview_window_id(window: &tauri::WebviewWindow) -> Option { - let ns_window = window.ns_window().ok()? as *const objc2_app_kit::NSWindow; - let number = unsafe { (*ns_window).windowNumber() }; - - if number <= 0 { - return None; - } - - number.to_string().parse().ok() +fn webview_window_id(window: &cap_desktop_runtime::WebviewWindow) -> Option { + window + .native_window_id() + .ok() + .flatten()? + .to_string() + .parse() + .ok() } #[cfg(test)] diff --git a/apps/desktop/src-tauri/src/window_position_persistence.rs b/apps/desktop/src-backend/src/window_position_persistence.rs similarity index 98% rename from apps/desktop/src-tauri/src/window_position_persistence.rs rename to apps/desktop/src-backend/src/window_position_persistence.rs index 2831a1bb5af..f499d0576d9 100644 --- a/apps/desktop/src-tauri/src/window_position_persistence.rs +++ b/apps/desktop/src-backend/src/window_position_persistence.rs @@ -1,7 +1,7 @@ +use cap_desktop_runtime::{AppHandle, Manager}; use scap_targets::DisplayId; use std::sync::{Arc, Mutex}; use std::time::Duration; -use tauri::{AppHandle, Manager}; use tokio::sync::Notify; use crate::general_settings::{GeneralSettingsStore, WindowPosition}; diff --git a/apps/desktop/src-backend/src/windows.rs b/apps/desktop/src-backend/src/windows.rs new file mode 100644 index 00000000000..a04864ee6b0 --- /dev/null +++ b/apps/desktop/src-backend/src/windows.rs @@ -0,0 +1,860 @@ +use std::{ + ops::Deref, + path::{Path, PathBuf}, + str::FromStr, + sync::{ + Arc, Mutex, + atomic::{AtomicU32, Ordering}, + }, + time::Duration, +}; + +use cap_desktop_runtime::{ + AppHandle, DesktopOperation, Event, LogicalPosition, Position, WebviewWindow, Window, + WindowOptions, WindowState, +}; +use cap_recording::{feeds, sources::screen_capture::ScreenCaptureTarget}; +use scap_targets::DisplayId; +use serde::Deserialize; +use specta::Type; +use tracing::{error, warn}; + +use crate::{ + App, ArcLock, NewNotification, camera_preview_error_message, + editor_window::PendingEditorInstances, + emit_camera_preview_clear, emit_camera_preview_error, + recording::{RecordingEvent, RecordingInputKind}, + recording_settings::{RecordingSettingsStore, RecordingTargetMode}, + screenshot_editor::PendingScreenshotEditorInstances, +}; + +pub fn hide_overlay(window: &WebviewWindow) { + let _ = window.set_ignore_cursor_events(true); + let _ = window.hide(); +} + +pub fn show_overlay(window: &WebviewWindow) { + let _ = window.set_ignore_cursor_events(false); + let _ = window.show(); +} + +pub fn logical_point_position(pos_x: f64, pos_y: f64) -> Position { + LogicalPosition::new(pos_x, pos_y).into() +} + +pub(crate) async fn ensure_camera_input_active(app_state: &mut App) { + if let Some(id) = app_state.selected_camera_id.clone() + && !app_state.camera_in_use + { + let settings = RecordingSettingsStore::camera_settings_for(&app_state.handle, &id); + match app_state + .camera_feed + .ask(feeds::camera::SetInput { id, settings }) + .await + { + Ok(ready_future) => { + if let Err(error) = ready_future.await { + error!(%error, "Camera failed to initialize"); + return; + } + } + Err(error) => { + error!(%error, "Failed to send SetInput to camera feed"); + return; + } + } + app_state.camera_in_use = true; + app_state.camera_cleanup_done = false; + } +} + +pub(crate) async fn restore_main_window_inputs(app: &AppHandle) { + let Some(state) = app.try_state::>() else { + warn!("App state unavailable while restoring main window inputs"); + return; + }; + if !state + .try_read() + .map(|state| !state.is_recording_active_or_pending()) + .unwrap_or(false) + { + return; + } + + let settings = RecordingSettingsStore::get(app) + .ok() + .flatten() + .unwrap_or_default(); + if let Err(error) = crate::set_mic_input(state.clone(), settings.mic_name).await { + warn!(%error, "Failed to restore microphone input for main window"); + } + + let camera_to_restore = state + .try_read() + .ok() + .and_then(|state| { + (!state.camera_cleanup_done && !state.camera_in_use) + .then(|| { + state + .selected_camera_id + .clone() + .or(settings.camera_id.clone()) + }) + .flatten() + }) + .filter(crate::is_camera_available); + + let Some(camera_id) = camera_to_restore else { + return; + }; + emit_camera_preview_clear(app); + let camera_settings = RecordingSettingsStore::camera_settings_for(app, &camera_id); + let camera_feed = { + let app_state = &mut *state.write().await; + app_state.selected_camera_id = Some(camera_id.clone()); + app_state.camera_in_use = true; + app_state.camera_cleanup_done = false; + app_state.camera_feed.clone() + }; + + let mut attempts = 0; + let result = loop { + attempts += 1; + match camera_feed + .ask(feeds::camera::SetInput { + id: camera_id.clone(), + settings: camera_settings, + }) + .await + { + Ok(ready) => match ready.await { + Ok(_) => break Ok(()), + Err(error) if attempts < 3 => { + warn!(%error, attempts, "Camera restore attempt failed"); + tokio::time::sleep(Duration::from_millis(500)).await; + } + Err(error) => break Err(error.to_string()), + }, + Err(error) if attempts < 3 => { + warn!(%error, attempts, "Camera restore request failed"); + tokio::time::sleep(Duration::from_millis(500)).await; + } + Err(error) => break Err(error.to_string()), + } + }; + + if let Err(error) = result { + let message = camera_preview_error_message(&error); + let _ = camera_feed.ask(feeds::camera::RemoveInput).await; + let emit_input_lost = { + let app_state = &mut *state.write().await; + app_state.selected_camera_id = None; + app_state.camera_in_use = false; + app_state + .disconnected_inputs + .insert(RecordingInputKind::Camera) + }; + if emit_input_lost { + let _ = RecordingEvent::InputLost { + input: RecordingInputKind::Camera, + } + .emit(app); + } + emit_camera_preview_error(app, message.clone()); + let _ = NewNotification { + title: "Camera unavailable".to_string(), + body: message, + is_error: true, + } + .emit(app); + } +} + +pub(crate) async fn cleanup_camera_window( + app: &AppHandle, + window: Option<&WebviewWindow>, + _reset_panel: bool, + wait_for_removal: bool, +) -> bool { + let windows = window + .cloned() + .map(|window| vec![window]) + .unwrap_or_else(|| { + app.webview_windows() + .into_iter() + .filter(|(label, _)| is_camera_window_label(label)) + .map(|(_, window)| window) + .collect() + }); + for window in windows { + let _ = window.destroy(); + } + if wait_for_removal { + tokio::time::sleep(Duration::from_millis(100)).await; + } + !app.webview_windows() + .keys() + .any(|label| is_camera_window_label(label)) +} + +fn is_camera_window_label(label: &str) -> bool { + label == "camera" + || label + .strip_prefix("camera-") + .is_some_and(|suffix| suffix.parse::().is_ok()) +} + +#[derive(Clone, Deserialize, Type)] +pub enum CapWindowId { + Main, + Settings, + Editor { id: u32 }, + RecordingsOverlay, + WindowCaptureOccluder { screen_id: DisplayId }, + TargetSelectOverlay { display_id: DisplayId }, + CaptureArea, + Camera, + RecordingControls, + Upgrade, + ModeSelect, + Debug, + ScreenshotEditor { id: u32 }, + Onboarding, + Teleprompter, +} + +impl FromStr for CapWindowId { + type Err = String; + + fn from_str(value: &str) -> Result { + Ok(match value { + "main" => Self::Main, + "settings" => Self::Settings, + value if is_camera_window_label(value) => Self::Camera, + "capture-area" => Self::CaptureArea, + "in-progress-recording" => Self::RecordingControls, + "recordings-overlay" => Self::RecordingsOverlay, + "upgrade" => Self::Upgrade, + "mode-select" => Self::ModeSelect, + "debug" => Self::Debug, + "onboarding" => Self::Onboarding, + "teleprompter" => Self::Teleprompter, + value if value.starts_with("editor-") => Self::Editor { + id: value + .strip_prefix("editor-") + .expect("guarded by starts_with") + .parse::() + .map_err(|error| error.to_string())?, + }, + value if value.starts_with("screenshot-editor-") => Self::ScreenshotEditor { + id: value + .strip_prefix("screenshot-editor-") + .expect("guarded by starts_with") + .parse::() + .map_err(|error| error.to_string())?, + }, + value if value.starts_with("window-capture-occluder-") => Self::WindowCaptureOccluder { + screen_id: value + .strip_prefix("window-capture-occluder-") + .expect("guarded by starts_with") + .parse::() + .map_err(|error| error.to_string())?, + }, + value if value.starts_with("target-select-overlay-") => Self::TargetSelectOverlay { + display_id: value + .strip_prefix("target-select-overlay-") + .expect("guarded by starts_with") + .parse::() + .map_err(|error| error.to_string())?, + }, + _ => return Err(format!("unknown window label: {value}")), + }) + } +} + +impl std::fmt::Display for CapWindowId { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Main => write!(formatter, "main"), + Self::Settings => write!(formatter, "settings"), + Self::Editor { id } => write!(formatter, "editor-{id}"), + Self::RecordingsOverlay => write!(formatter, "recordings-overlay"), + Self::WindowCaptureOccluder { screen_id } => { + write!(formatter, "window-capture-occluder-{screen_id}") + } + Self::TargetSelectOverlay { display_id } => { + write!(formatter, "target-select-overlay-{display_id}") + } + Self::CaptureArea => write!(formatter, "capture-area"), + Self::Camera => write!(formatter, "camera"), + Self::RecordingControls => write!(formatter, "in-progress-recording"), + Self::Upgrade => write!(formatter, "upgrade"), + Self::ModeSelect => write!(formatter, "mode-select"), + Self::Debug => write!(formatter, "debug"), + Self::ScreenshotEditor { id } => write!(formatter, "screenshot-editor-{id}"), + Self::Onboarding => write!(formatter, "onboarding"), + Self::Teleprompter => write!(formatter, "teleprompter"), + } + } +} + +impl CapWindowId { + pub fn label(&self) -> String { + self.to_string() + } + + #[cfg(target_os = "macos")] + pub fn activates_dock(&self) -> bool { + matches!( + self, + Self::Main + | Self::Editor { .. } + | Self::ScreenshotEditor { .. } + | Self::Settings + | Self::Upgrade + | Self::ModeSelect + | Self::Onboarding + ) + } + + pub fn title(&self) -> String { + match self { + Self::Settings => "Cap Settings", + Self::WindowCaptureOccluder { .. } => "Cap Window Capture Occluder", + Self::CaptureArea => "Cap Capture Area", + Self::RecordingControls => "Cap Recording Controls", + Self::Editor { .. } => "Cap Editor", + Self::ScreenshotEditor { .. } => "Cap Screenshot Editor", + Self::ModeSelect => "Cap Mode Selection", + Self::Onboarding => "Welcome to Cap", + Self::Camera => "Cap Camera", + Self::RecordingsOverlay => "Cap Recordings Overlay", + Self::TargetSelectOverlay { .. } => "Cap Target Select", + Self::Teleprompter => "Cap Teleprompter", + _ => "Cap", + } + .to_string() + } + + pub fn get(&self, app: &AppHandle) -> Option { + if matches!(self, Self::Camera) { + return app + .webview_windows() + .into_iter() + .filter(|(label, _)| is_camera_window_label(label)) + .max_by_key(|(label, _)| label.clone()) + .map(|(_, window)| window); + } + app.get_webview_window(&self.label()) + } + + fn min_size(&self) -> Option<(f64, f64)> { + Some(match self { + Self::Main => (330.0, 395.0), + Self::Editor { .. } => (1275.0, 800.0), + Self::ScreenshotEditor { .. } => (800.0, 600.0), + Self::Settings => (780.0, 560.0), + Self::Camera => (200.0, 200.0), + Self::Upgrade => (950.0, 850.0), + Self::ModeSelect => (580.0, 340.0), + Self::Onboarding => (860.0, 690.0), + _ => return None, + }) + } +} + +#[derive(Debug, Clone, Type, Deserialize)] +pub enum ShowCapWindow { + Main { + init_target_mode: Option, + }, + Settings { + page: Option, + }, + Editor { + project_path: PathBuf, + }, + RecordingsOverlay, + WindowCaptureOccluder { + screen_id: DisplayId, + }, + TargetSelectOverlay { + display_id: DisplayId, + target_mode: Option, + }, + CaptureArea { + screen_id: DisplayId, + }, + Camera { + centered: bool, + }, + InProgressRecording { + countdown: Option, + #[serde(default)] + capture_target: Option, + }, + Upgrade, + ModeSelect, + ScreenshotEditor { + path: PathBuf, + }, + Onboarding, +} + +impl ShowCapWindow { + pub async fn show(&self, app: &AppHandle) -> cap_desktop_runtime::Result { + self.prepare(app).await; + let id = self.id(app); + if let Some(window) = id.get(app) { + window.show()?; + window.set_focus()?; + return Ok(window); + } + + let (route, width, height, always_on_top, skip_taskbar, focus, visible) = match self { + Self::Main { init_target_mode } => ( + format!("/{}", target_mode_query(init_target_mode.as_ref())), + 352.0, + 520.0, + false, + false, + true, + true, + ), + Self::Settings { page } => ( + format!("/settings/{}", page.as_deref().unwrap_or_default()), + 782.0, + 775.0, + false, + false, + true, + true, + ), + Self::Editor { .. } => ( + "/editor".to_string(), + 1275.0, + 800.0, + false, + false, + true, + false, + ), + Self::ScreenshotEditor { .. } => ( + "/screenshot-editor".to_string(), + 1100.0, + 760.0, + false, + false, + true, + false, + ), + Self::RecordingsOverlay => ( + "/recordings-overlay".to_string(), + 440.0, + 560.0, + true, + true, + false, + false, + ), + Self::WindowCaptureOccluder { .. } => ( + "/window-capture-occluder".to_string(), + 800.0, + 600.0, + true, + true, + false, + true, + ), + Self::TargetSelectOverlay { + display_id, + target_mode, + } => ( + format!( + "/target-select-overlay?displayId={display_id}{}", + target_mode_param(target_mode.as_ref()) + ), + 800.0, + 600.0, + true, + true, + false, + true, + ), + Self::CaptureArea { screen_id } => ( + format!("/capture-area?screenId={screen_id}"), + 800.0, + 600.0, + true, + true, + true, + true, + ), + Self::Camera { .. } => ("/camera".to_string(), 320.0, 320.0, true, true, false, true), + Self::InProgressRecording { countdown, .. } => ( + format!( + "/in-progress-recording?countdown={}", + countdown.unwrap_or_default() + ), + 360.0, + 120.0, + true, + true, + false, + true, + ), + Self::Upgrade => ( + "/upgrade".to_string(), + 950.0, + 850.0, + false, + false, + true, + true, + ), + Self::ModeSelect => ( + "/mode-select".to_string(), + 580.0, + 340.0, + false, + false, + true, + true, + ), + Self::Onboarding => ( + "/onboarding".to_string(), + 860.0, + 690.0, + false, + false, + true, + true, + ), + }; + let (min_width, min_height) = id + .min_size() + .map_or((None, None), |(width, height)| (Some(width), Some(height))); + let initialization = self.initialization(app).await?; + let label = id.label(); + app.send(cap_desktop_runtime::BackendMessage::CreateWindow { + options: WindowOptions { + label: label.clone(), + route, + title: id.title(), + x: None, + y: None, + width, + height, + min_width, + min_height, + transparent: matches!( + id, + CapWindowId::Main + | CapWindowId::Onboarding + | CapWindowId::Camera + | CapWindowId::CaptureArea + | CapWindowId::TargetSelectOverlay { .. } + | CapWindowId::RecordingControls + | CapWindowId::RecordingsOverlay + | CapWindowId::WindowCaptureOccluder { .. } + ), + decorations: false, + resizable: !matches!( + id, + CapWindowId::TargetSelectOverlay { .. } + | CapWindowId::RecordingControls + | CapWindowId::RecordingsOverlay + ), + always_on_top, + visible_on_all_workspaces: always_on_top, + skip_taskbar, + content_protected: true, + focus, + visible, + initialization, + }, + })?; + app.update_window_state(label.clone(), WindowState::default()); + Ok(Window::new(app.clone(), label)) + } + + async fn initialization( + &self, + app: &AppHandle, + ) -> cap_desktop_runtime::Result { + match self { + Self::Main { init_target_mode } => Ok(serde_json::json!({ + "initialTargetMode": init_target_mode, + })), + Self::TargetSelectOverlay { .. } => Ok(serde_json::json!({ + "cameraWsPort": camera_ws_port(app).await?, + })), + Self::Camera { centered } => Ok(serde_json::json!({ + "cameraWsPort": camera_ws_port(app).await?, + "cameraOnlyMode": centered, + "enableNativeCameraPreview": + crate::general_settings::GeneralSettingsStore::native_camera_preview_enabled(app), + })), + Self::InProgressRecording { countdown, .. } => Ok(serde_json::json!({ + "countdown": countdown.unwrap_or_default(), + })), + _ => Ok(serde_json::json!({})), + } + } + + async fn prepare(&self, app: &AppHandle) { + match self { + Self::Editor { project_path } => { + let state = app.state::(); + let id = state.id_for(project_path); + PendingEditorInstances::start_prewarm( + app, + CapWindowId::Editor { id }.label(), + project_path.clone(), + ) + .await; + } + Self::ScreenshotEditor { path } => { + let state = app.state::(); + let id = state.id_for(path); + PendingScreenshotEditorInstances::start_prewarm( + app, + CapWindowId::ScreenshotEditor { id }.label(), + path.clone(), + ) + .await; + } + _ => {} + } + } + + pub fn id(&self, app: &AppHandle) -> CapWindowId { + match self { + Self::Main { .. } => CapWindowId::Main, + Self::Settings { .. } => CapWindowId::Settings, + Self::Editor { project_path } => CapWindowId::Editor { + id: app.state::().id_for(project_path), + }, + Self::RecordingsOverlay => CapWindowId::RecordingsOverlay, + Self::WindowCaptureOccluder { screen_id } => CapWindowId::WindowCaptureOccluder { + screen_id: screen_id.clone(), + }, + Self::TargetSelectOverlay { display_id, .. } => CapWindowId::TargetSelectOverlay { + display_id: display_id.clone(), + }, + Self::CaptureArea { .. } => CapWindowId::CaptureArea, + Self::Camera { .. } => CapWindowId::Camera, + Self::InProgressRecording { .. } => CapWindowId::RecordingControls, + Self::Upgrade => CapWindowId::Upgrade, + Self::ModeSelect => CapWindowId::ModeSelect, + Self::ScreenshotEditor { path } => CapWindowId::ScreenshotEditor { + id: app.state::().id_for(path), + }, + Self::Onboarding => CapWindowId::Onboarding, + } + } +} + +async fn camera_ws_port(app: &AppHandle) -> cap_desktop_runtime::Result { + let state = app + .try_state::>() + .ok_or_else(|| "App state unavailable while creating camera window".to_string())?; + #[allow(deprecated)] + let port = state.read().await.camera_ws_port; + Ok(port) +} + +fn target_mode_query(mode: Option<&RecordingTargetMode>) -> String { + mode.map(|mode| format!("?targetMode={}", mode_name(mode))) + .unwrap_or_default() +} + +fn target_mode_param(mode: Option<&RecordingTargetMode>) -> String { + mode.map(|mode| format!("&targetMode={}", mode_name(mode))) + .unwrap_or_default() +} + +fn mode_name(mode: &RecordingTargetMode) -> &'static str { + match mode { + RecordingTargetMode::Display => "display", + RecordingTargetMode::Window => "window", + RecordingTargetMode::Area => "area", + RecordingTargetMode::Camera => "camera", + } +} + +pub fn update_window_rasterization_scale(_window: &WebviewWindow, _scale_factor: f64) {} + +#[cap_desktop_runtime::command] +pub fn set_theme(window: Window, theme: crate::general_settings::AppTheme) { + let theme = match theme { + crate::general_settings::AppTheme::System => "system", + crate::general_settings::AppTheme::Light => "light", + crate::general_settings::AppTheme::Dark => "dark", + }; + let _ = window.operation(DesktopOperation::SetTheme { + theme: theme.to_string(), + }); +} + +#[cap_desktop_runtime::command] +pub fn position_traffic_lights(window: Window, controls_inset: Option<(f64, f64)>) { + let (x, y) = controls_inset + .map(|(x, y)| (Some(x), Some(y))) + .unwrap_or((None, None)); + let _ = window.operation(DesktopOperation::SetTrafficLightPosition { x, y }); +} + +#[cap_desktop_runtime::command] +pub fn set_teleprompter_window_level(window: Window, always_on_top: bool) { + let _ = window.set_always_on_top(always_on_top); +} + +#[cap_desktop_runtime::command] +pub fn set_teleprompter_window_opacity(window: Window, opacity: f64) { + let _ = window.set_opacity(opacity); +} + +#[cfg(target_os = "windows")] +pub fn capture_exclusion_hides_ui() -> bool { + true +} + +#[cfg(not(target_os = "windows"))] +pub fn capture_exclusion_hides_ui() -> bool { + false +} + +pub fn apply_content_protection(app: &AppHandle, enabled: bool) { + for window in app.webview_windows().into_values() { + let _ = window.set_content_protected(enabled); + } +} + +#[cap_desktop_runtime::command] +pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), String> { + apply_content_protection(&app, true); + Ok(()) +} + +#[cap_desktop_runtime::command] +pub async fn apply_macos_liquid_glass_background( + _window: Window, + enabled: bool, + radius: f64, +) -> Result { + let _ = (enabled, radius); + Ok(false) +} + +#[cap_desktop_runtime::command] +pub fn set_window_transparent(_window: Window, value: bool) { + let _ = value; +} + +#[derive(Default)] +pub struct EditorWindowIds { + pub ids: Arc>>, + counter: AtomicU32, +} + +impl EditorWindowIds { + fn id_for(&self, path: &Path) -> u32 { + let mut ids = self.ids.lock().expect("editor window ids lock poisoned"); + if let Some((_, id)) = ids.iter().find(|(current, _)| current == path) { + return *id; + } + let id = self.counter.fetch_add(1, Ordering::SeqCst); + ids.push((path.to_path_buf(), id)); + id + } + + pub fn get(app: &AppHandle) -> cap_desktop_runtime::State<'_, Self> { + app.state() + } +} + +#[derive(Default)] +pub struct ScreenshotEditorWindowIds { + pub ids: Arc>>, + counter: AtomicU32, +} + +impl ScreenshotEditorWindowIds { + fn id_for(&self, path: &Path) -> u32 { + let mut ids = self + .ids + .lock() + .expect("screenshot editor window ids lock poisoned"); + if let Some((_, id)) = ids.iter().find(|(current, _)| current == path) { + return *id; + } + let id = self.counter.fetch_add(1, Ordering::SeqCst); + ids.push((path.to_path_buf(), id)); + id + } + + pub fn get(app: &AppHandle) -> cap_desktop_runtime::State<'_, Self> { + app.state() + } +} + +#[derive(Clone, Default)] +pub struct EditorRecordingTarget(pub Arc>>); + +impl EditorRecordingTarget { + pub fn get(app: &AppHandle) -> Self { + app.state::().deref().clone() + } + + pub fn set(app: &AppHandle, path: Option) { + *Self::get(app).0.lock().unwrap() = path; + } + + pub fn current(app: &AppHandle) -> Option { + Self::get(app).0.lock().unwrap().clone() + } + + pub fn take(app: &AppHandle) -> Option { + Self::get(app).0.lock().unwrap().take() + } +} + +impl Deref for EditorRecordingTarget { + type Target = Arc>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub fn editor_window_for_path(app: &AppHandle, path: &Path) -> Option { + let ids = EditorWindowIds::get(app); + let id = ids + .ids + .lock() + .ok()? + .iter() + .find(|(candidate, _)| candidate == path) + .map(|(_, id)| *id)?; + CapWindowId::Editor { id }.get(app) +} + +#[cfg(test)] +mod tests { + use super::CapWindowId; + use std::str::FromStr; + + #[test] + fn parses_dynamic_window_labels_without_fixed_byte_offsets() { + for label in [ + "editor-42", + "screenshot-editor-7", + "window-capture-occluder-1", + "target-select-overlay-1", + ] { + let parsed = CapWindowId::from_str(label).unwrap(); + assert_eq!(parsed.to_string(), label); + } + } +} diff --git a/apps/desktop/src-tauri/tests/exit_shutdown.rs b/apps/desktop/src-backend/tests/exit_shutdown.rs similarity index 100% rename from apps/desktop/src-tauri/tests/exit_shutdown.rs rename to apps/desktop/src-backend/tests/exit_shutdown.rs diff --git a/apps/desktop/src-tauri/tests/web_api_startup.rs b/apps/desktop/src-backend/tests/web_api_startup.rs similarity index 72% rename from apps/desktop/src-tauri/tests/web_api_startup.rs rename to apps/desktop/src-backend/tests/web_api_startup.rs index c9cf9f24809..646d212ecaf 100644 --- a/apps/desktop/src-tauri/tests/web_api_startup.rs +++ b/apps/desktop/src-backend/tests/web_api_startup.rs @@ -2,13 +2,18 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Arc; use cap_desktop_lib::{App, web_api::ManagerExt}; -use tauri::{Manager, test::mock_app}; +use cap_desktop_runtime::{AppHandle, AppPaths}; use tokio::sync::RwLock; +fn mock_app() -> AppHandle { + let (outbound, _messages) = tokio::sync::mpsc::unbounded_channel(); + let paths = AppPaths::discover("so.cap.desktop.test", std::env::temp_dir()).unwrap(); + AppHandle::new(outbound, paths) +} + #[test] fn make_app_url_before_manage_does_not_panic_and_uses_default_server_url() { - let app = mock_app(); - let handle = app.handle().clone(); + let handle = mock_app(); let raw_state_access = catch_unwind(AssertUnwindSafe(|| { let _ = handle.state::>>(); @@ -26,18 +31,13 @@ fn make_app_url_before_manage_does_not_panic_and_uses_default_server_url() { "/api/upload/multipart/initiate" ) ); - - std::mem::forget(app); } #[test] fn is_server_url_custom_before_manage_is_safe_and_false() { - let app = mock_app(); - let handle = app.handle().clone(); + let handle = mock_app(); let is_custom = futures::executor::block_on(handle.is_server_url_custom()); assert!(!is_custom); - - std::mem::forget(app); } diff --git a/apps/desktop/src-tauri/.gitignore b/apps/desktop/src-tauri/.gitignore deleted file mode 100644 index b21bd681d99..00000000000 --- a/apps/desktop/src-tauri/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ - -# Generated by Tauri -# will have schema files for capabilities auto-completion -/gen/schemas diff --git a/apps/desktop/src-tauri/Info.plist b/apps/desktop/src-tauri/Info.plist deleted file mode 100644 index dd740dcf952..00000000000 --- a/apps/desktop/src-tauri/Info.plist +++ /dev/null @@ -1,33 +0,0 @@ - - - - - CFBundleDocumentTypes - - - CFBundleTypeExtensions - - cap - - CFBundleTypeIconFile - icon.icns - CFBundleTypeName - Cap Recording - CFBundleTypeRole - Editor - LSHandlerRank - Default - LSTypeIsPackage - - - - NSCameraUsageDescription - Cap uses your camera to record you in your Caps - NSMicrophoneUsageDescription - Cap uses your microphone to record you in your Caps - NSScreenCaptureUsageDescription - Cap captures your screen as part of recording your Caps - NSCameraUseContinuityCameraDeviceType - - - diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs deleted file mode 100644 index 261851f6b60..00000000000 --- a/apps/desktop/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build(); -} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json deleted file mode 100644 index 745254f6eb4..00000000000 --- a/apps/desktop/src-tauri/capabilities/default.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["*"], - "permissions": [ - "fs:default", - "fs:allow-resource-read", - "fs:allow-resource-read-recursive", - "fs:write-all", - "fs:read-all", - "fs:allow-appdata-read", - "fs:allow-appdata-write", - "fs:allow-picture-read-recursive", - { - "identifier": "fs:scope", - "allow": [ - { "path": "$APPDATA/**" }, - { "path": "$HOME/**" }, - { "path": "$RESOURCE/**" } - ] - }, - "core:path:allow-resolve-directory", - "core:path:default", - "core:event:default", - "core:menu:default", - "core:resources:allow-close", - "core:window:default", - "core:window:allow-close", - "core:window:allow-destroy", - "core:window:allow-hide", - "core:window:allow-show", - "core:window:allow-center", - "core:window:allow-minimize", - "core:window:allow-unminimize", - "core:window:allow-maximize", - "core:window:allow-unmaximize", - "core:window:allow-set-size", - "core:window:allow-set-focus", - "core:window:allow-start-dragging", - "core:window:allow-set-position", - "core:window:allow-set-theme", - "core:window:allow-set-progress-bar", - "core:window:allow-set-effects", - "core:window:allow-set-ignore-cursor-events", - "core:window:allow-set-visible-on-all-workspaces", - "core:window:deny-set-visible-on-all-workspaces", - "core:window:allow-set-content-protected", - "core:window:allow-set-always-on-bottom", - "core:window:allow-set-always-on-top", - "core:webview:default", - "core:webview:allow-create-webview-window", - "core:app:allow-version", - "shell:default", - "core:image:default", - "dialog:default", - "store:default", - "process:default", - "oauth:allow-start", - "updater:default", - "notification:default", - "deep-link:default", - { - "identifier": "http:default", - "allow": [ - { "url": "http://*" }, - { "url": "https://*" }, - { "url": "http://localhost:*" } - ] - }, - "clipboard-manager:allow-write-text", - "opener:allow-reveal-item-in-dir" - ] -} diff --git a/apps/desktop/src-tauri/src/flags.rs b/apps/desktop/src-tauri/src/flags.rs deleted file mode 100644 index 0d763f50210..00000000000 --- a/apps/desktop/src-tauri/src/flags.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub mod plugin { - use cap_flags::FLAGS; - use tauri::{ - Runtime, - plugin::{Builder, TauriPlugin}, - }; - - pub fn init() -> TauriPlugin { - Builder::new("cap-flags") - .js_init_script(format!( - "window.FLAGS = {}", - serde_json::to_string_pretty(&FLAGS).unwrap() - )) - .build() - } -} diff --git a/apps/desktop/src-tauri/src/platform/macos/mod.rs b/apps/desktop/src-tauri/src/platform/macos/mod.rs deleted file mode 100644 index 0dc6904bcc3..00000000000 --- a/apps/desktop/src-tauri/src/platform/macos/mod.rs +++ /dev/null @@ -1,655 +0,0 @@ -// use std::ffi::c_void; - -// use cocoa::{ -// base::{id, nil}, -// foundation::NSString, -// }; -// use core_graphics::{ -// base::boolean_t, -// display::{CFDictionaryRef, CGRect}, -// }; -// use objc::{class, msg_send, sel, sel_impl}; - -pub mod delegates; -mod sc_shareable_content; - -pub use sc_shareable_content::*; - -fn constrain_position_to_visible_top( - position: tauri::PhysicalPosition, - monitor_top: i32, - scale_factor: f64, - visible_frame_top_inset: f64, - safe_area_top_inset: f64, -) -> Option> { - let top_inset = visible_frame_top_inset.max(safe_area_top_inset).max(0.0); - let minimum_y = monitor_top.saturating_add((top_inset * scale_factor).round() as i32); - - (position.y < minimum_y).then(|| tauri::PhysicalPosition::new(position.x, minimum_y)) -} - -pub fn constrain_main_window_to_visible_top( - window: &tauri::Window, - position: tauri::PhysicalPosition, -) -> Option> { - use objc2::{runtime::NSObjectProtocol, sel}; - use objc2_app_kit::NSWindow; - - let monitor = window.current_monitor().ok().flatten()?; - let ns_window = window.ns_window().ok()? as *const NSWindow; - let screen = unsafe { (*ns_window).screen() }?; - let frame = screen.frame(); - let visible_frame = screen.visibleFrame(); - let safe_area_top_inset = if screen.respondsToSelector(sel!(safeAreaInsets)) { - unsafe { screen.safeAreaInsets().top } - } else { - 0.0 - }; - // Tauri runtime 2.8 omits this macOS top offset from Monitor::work_area().position. - let visible_frame_top_inset = - frame.origin.y + frame.size.height - visible_frame.origin.y - visible_frame.size.height; - - constrain_position_to_visible_top( - position, - monitor.position().y, - monitor.scale_factor(), - visible_frame_top_inset, - safe_area_top_inset, - ) -} - -pub fn set_window_level(window: tauri::Window, level: objc2_app_kit::NSWindowLevel) { - let c_window = window.clone(); - _ = window.run_on_main_thread(move || unsafe { - let Ok(ns_win) = c_window.ns_window() else { - return; - }; - let ns_win = ns_win as *const objc2_app_kit::NSWindow; - (*ns_win).setLevel(level); - }); -} - -pub fn set_window_opacity(window: tauri::Window, opacity: f64) { - let opacity = opacity.clamp(0.45, 1.0); - let c_window = window.clone(); - _ = window.run_on_main_thread(move || unsafe { - use cocoa::base::id; - use objc::{msg_send, sel, sel_impl}; - - let Ok(ns_win) = c_window.ns_window() else { - return; - }; - let ns_win = ns_win as id; - let _: () = msg_send![ns_win, setAlphaValue: opacity]; - }); -} - -pub fn apply_squircle_corners(window: &tauri::WebviewWindow, radius: f64) { - use cocoa::base::{id, nil}; - use cocoa::foundation::NSString; - use objc::{msg_send, sel, sel_impl}; - - let Ok(ns_win) = window.ns_window() else { - return; - }; - - unsafe { - let ns_win = ns_win as id; - let content_view: id = msg_send![ns_win, contentView]; - - if content_view != nil { - let _: () = msg_send![content_view, setWantsLayer: true]; - - let layer: id = msg_send![content_view, layer]; - if layer != nil { - let _: () = msg_send![layer, setCornerRadius: radius]; - let _: () = msg_send![layer, setMasksToBounds: true]; - - let continuous = NSString::alloc(nil).init_str("continuous"); - let _: () = msg_send![layer, setCornerCurve: continuous]; - } - } - } -} - -const TAURI_VIBRANCY_VIEW_TAG: isize = 91376254; -const LIQUID_GLASS_IDENTIFIER: &str = "so.cap.liquid-glass-background"; -const NS_GLASS_EFFECT_VIEW_STYLE_REGULAR: isize = 0; - -#[derive(Clone, Copy, PartialEq, Eq)] -enum LiquidGlassActivity { - SystemManaged, - AlwaysActive, -} - -pub fn apply_main_window_liquid_glass_background( - window: &tauri::Window, - enabled: bool, - radius: f64, -) -> Result { - apply_liquid_glass_background_inner(window, enabled, radius, LiquidGlassActivity::SystemManaged) -} - -unsafe fn remove_tagged_subview(container: cocoa::base::id, tag: isize) { - use objc::{msg_send, sel, sel_impl}; - - unsafe { - let responds_to_view_with_tag: bool = - msg_send![container, respondsToSelector: sel!(viewWithTag:)]; - if !responds_to_view_with_tag { - return; - } - - let view: cocoa::base::id = msg_send![container, viewWithTag: tag]; - if view != cocoa::base::nil { - let _: () = msg_send![view, removeFromSuperview]; - } - } -} - -unsafe fn remove_liquid_glass_subviews( - container: cocoa::base::id, - identifier: cocoa::base::id, - glass_class: *const objc::runtime::Class, -) { - use cocoa::base::{id, nil}; - use objc::{msg_send, sel, sel_impl}; - - unsafe { - let subviews: id = msg_send![container, subviews]; - if subviews == nil { - return; - } - - let count: usize = msg_send![subviews, count]; - for index in (0..count).rev() { - let view: id = msg_send![subviews, objectAtIndex: index]; - if view == nil { - continue; - } - - let is_glass_view: bool = msg_send![view, isKindOfClass: glass_class]; - if is_glass_view { - let _: () = msg_send![view, removeFromSuperview]; - // Balance the `alloc` ownership claim taken when this view was - // created in `apply_liquid_glass_background`. Without this, - // removing it from the hierarchy only drops the superview's - // retain, leaving it stuck at refcount 1 forever — a leaked - // GPU/IOSurface-backed NSGlassEffectView on every re-apply. - let _: () = msg_send![view, release]; - continue; - } - - let responds_to_identifier: bool = - msg_send![view, respondsToSelector: sel!(identifier)]; - if !responds_to_identifier { - continue; - } - - let view_identifier: id = msg_send![view, identifier]; - if view_identifier == nil { - continue; - } - - let matches_identifier: bool = msg_send![view_identifier, isEqualToString: identifier]; - if matches_identifier { - let _: () = msg_send![view, removeFromSuperview]; - } - } - } -} - -pub fn apply_liquid_glass_background( - window: &tauri::Window, - enabled: bool, - radius: f64, -) -> Result { - apply_liquid_glass_background_inner(window, enabled, radius, LiquidGlassActivity::AlwaysActive) -} - -fn apply_liquid_glass_background_inner( - window: &tauri::Window, - enabled: bool, - radius: f64, - activity: LiquidGlassActivity, -) -> Result { - use cocoa::{ - base::{id, nil}, - foundation::{NSRect, NSString}, - }; - use objc::{class, msg_send, runtime::Class, sel, sel_impl}; - - unsafe { - let ns_window = window - .ns_window() - .map_err(|error| format!("Failed to get native window handle: {error}"))? - as id; - let content_view: id = msg_send![ns_window, contentView]; - - if content_view == nil { - return Err("Window has no content view".into()); - } - - let Some(glass_class) = Class::get("NSGlassEffectView") else { - // No NSGlassEffectView (pre-macOS-26) — the caller uses vibrancy. - if enabled { - crate::crash_sentinel::set_liquid_glass_outcome("unsupported"); - } - return Ok(false); - }; - let glass_class = glass_class as *const Class; - let glass_identifier = NSString::alloc(nil).init_str(LIQUID_GLASS_IDENTIFIER); - - if !enabled { - teardown_liquid_glass_ns(ns_window); - return Ok(false); - } - - remove_liquid_glass_subviews(content_view, glass_identifier, glass_class); - - // Clip the content view itself to the same continuous (squircle) curve we apply - // to the NSGlassEffectView below. Without this, WebKit's CSS border-radius on - // `.cap-window-shell` and the window's Core Animation corner are rasterised by - // different engines and don't align pixel-perfectly, producing the visible inner - // "second border" inside the rounded corner. This is plain Core Animation (no - // private SPI / no WindowServer risk), so we apply it up front — it must clip - // BOTH the glass view and the vibrancy fallback, otherwise the artifact returns - // on the fallback path (most visibly on the larger-radius settings window). - let _: () = msg_send![content_view, setWantsLayer: true]; - let content_layer: id = msg_send![content_view, layer]; - if content_layer != nil { - let _: () = msg_send![content_layer, setCornerRadius: radius]; - let _: () = msg_send![content_layer, setMasksToBounds: true]; - let continuous = NSString::alloc(nil).init_str("continuous"); - let _: () = msg_send![content_layer, setCornerCurve: continuous]; - } - - let bounds: NSRect = msg_send![content_view, bounds]; - let glass_view: id = msg_send![glass_class, alloc]; - let glass_view: id = msg_send![glass_view, initWithFrame: bounds]; - - if glass_view == nil { - crate::crash_sentinel::set_liquid_glass_outcome("fallback"); - return Ok(false); - } - - let responds_to_set_identifier: bool = - msg_send![glass_view, respondsToSelector: sel!(setIdentifier:)]; - if responds_to_set_identifier { - let _: () = msg_send![glass_view, setIdentifier: glass_identifier]; - } - - let responds_to_set_corner_radius: bool = - msg_send![glass_view, respondsToSelector: sel!(setCornerRadius:)]; - if responds_to_set_corner_radius { - let _: () = msg_send![glass_view, setCornerRadius: radius]; - } - - if activity == LiquidGlassActivity::SystemManaged { - let _: () = msg_send![glass_view, setStyle: NS_GLASS_EFFECT_VIEW_STYLE_REGULAR]; - } - - // Pin the glass to its "always-active" representation so it keeps re-rendering - // the live backdrop regardless of which window currently has key state. The - // default for an NSVisualEffectView-derived view is FollowsWindowActiveState, - // which dims the material whenever another Cap window (camera, settings, etc.) - // becomes key and masks the backdrop reactivity that's the whole point of - // Liquid Glass. The always-active state is private SPI, so we probe multiple - // state knobs (`setState:`, `setActive:`) instead of assuming one inheritance - // path. The main window does not enter this branch and remains system-managed. - // - // macOS 26.3 shipped an NSGlassEffectView that responds to neither selector, - // so the pin silently fails. In that state the material can't be relied on - // (it dims whenever another window becomes key), so abandon the private SPI - // entirely and let the caller fall back to NSVisualEffectView vibrancy - // (Ok(false)). - if activity == LiquidGlassActivity::AlwaysActive - && !force_glass_view_always_active(glass_view) - { - // Never entered the view hierarchy; balance the alloc and bail. The - // content-layer squircle clip applied above is kept (plain Core Animation, - // not a WindowServer/occlusion mutation) so the vibrancy fallback still gets - // the aligned rounded corner. - let _: () = msg_send![glass_view, release]; - crate::crash_sentinel::set_liquid_glass_outcome("fallback"); - return Ok(false); - } - - remove_tagged_subview(content_view, TAURI_VIBRANCY_VIEW_TAG); - - let clear_color: id = msg_send![class!(NSColor), clearColor]; - let _: () = msg_send![ns_window, setOpaque: false]; - let _: () = msg_send![ns_window, setBackgroundColor: clear_color]; - - // Deliberately DO NOT disable window/WKWebView occlusion detection here. - // Earlier builds called the private `_setWindowOcclusionDetectionEnabled:` / - // `_setWebViewWindowOcclusionDetectionEnabled:` SPI so the glass backdrop kept - // sampling while Cap was unfocused, but an occlusion-suppressed surface is one - // WindowServer can never quiesce — on macOS 26 that wedged the compositor when - // the window was later hidden or the app quit (often after sleep/lid-close), - // watchdog-stalling WindowServer and soft-restarting the whole login session. - // Gating it on the always-active pin was not enough: the main window's - // close-to-tray path hides the window without any teardown, so any future - // macOS build where the pin succeeds would silently re-arm the wedge. The - // only field-verified-safe configuration is to leave occlusion detection - // alone entirely; the trade-off is merely cosmetic (backdrop sampling may - // pause on the last frame while the app is deactivated). - - let _: () = msg_send![glass_view, setAutoresizingMask: 18usize]; - let _: () = msg_send![ - content_view, - addSubview: glass_view - positioned: -1isize - relativeTo: nil - ]; - - if activity == LiquidGlassActivity::AlwaysActive { - // Re-apply after the view enters the hierarchy: some private AppKit views - // reset state machine fields on `viewDidMoveToWindow:`, so the post-add pass - // is what actually sticks. - force_glass_view_always_active(glass_view); - } - - crate::crash_sentinel::set_liquid_glass_outcome("applied"); - Ok(true) - } -} - -/// Pin the glass material to its always-active representation. Returns whether the -/// view actually responded to a known pinning selector. A `false` return means this -/// macOS build's NSGlassEffectView SPI differs from what we understand (notably 26.3, -/// where it responds to neither `setState:` nor `setActive:`) and the material must -/// NOT be relied upon — see `apply_liquid_glass_background`. -unsafe fn force_glass_view_always_active(glass_view: cocoa::base::id) -> bool { - use cocoa::base::id; - use objc::{msg_send, sel, sel_impl}; - - unsafe { - let responds_to_set_state: bool = - msg_send![glass_view, respondsToSelector: sel!(setState:)]; - if responds_to_set_state { - // NSVisualEffectStateActive == 1 - let _: () = msg_send![glass_view, setState: 1isize]; - tracing::info!(target: "cap_desktop_lib::liquid_glass", "NSGlassEffectView responds to setState:"); - } - - let responds_to_set_active: bool = - msg_send![glass_view, respondsToSelector: sel!(setActive:)]; - if responds_to_set_active { - let _: () = msg_send![glass_view, setActive: true]; - tracing::info!(target: "cap_desktop_lib::liquid_glass", "NSGlassEffectView responds to setActive:"); - } - - if !responds_to_set_state && !responds_to_set_active { - tracing::debug!( - target: "cap_desktop_lib::liquid_glass", - "NSGlassEffectView responds to neither setState: nor setActive: — \ - cannot pin material to always-active; falling back to vibrancy" - ); - return false; - } - - // If the glass view internally wraps an NSVisualEffectView (rather than - // subclassing it), the state property on the outer view won't propagate. - // Walk one level of subviews and pin any visual-effect-like state we find. - let subviews: id = msg_send![glass_view, subviews]; - if subviews != cocoa::base::nil { - let count: usize = msg_send![subviews, count]; - for index in 0..count { - let subview: id = msg_send![subviews, objectAtIndex: index]; - if subview == cocoa::base::nil { - continue; - } - - let subview_responds_to_set_state: bool = - msg_send![subview, respondsToSelector: sel!(setState:)]; - if subview_responds_to_set_state { - let _: () = msg_send![subview, setState: 1isize]; - } - } - } - - true - } -} - -unsafe fn enable_window_occlusion_detection(ns_window: cocoa::base::id) { - use objc::{msg_send, sel, sel_impl}; - - // Defense-in-depth heal: current builds never disable occlusion detection, but - // re-assert the OS default via whichever private SPI variant this AppKit build - // responds to, so the OS can always quiesce the surface. - unsafe { - let responds: bool = msg_send![ - ns_window, - respondsToSelector: sel!(_setWindowOcclusionDetectionEnabled:) - ]; - if responds { - let _: () = msg_send![ns_window, _setWindowOcclusionDetectionEnabled: true]; - return; - } - - let responds: bool = msg_send![ - ns_window, - respondsToSelector: sel!(setWindowOcclusionDetectionEnabled:) - ]; - if responds { - let _: () = msg_send![ns_window, setWindowOcclusionDetectionEnabled: true]; - return; - } - - let responds: bool = msg_send![ - ns_window, - respondsToSelector: sel!(_setOcclusionDetectionEnabled:) - ]; - if responds { - let _: () = msg_send![ns_window, _setOcclusionDetectionEnabled: true]; - return; - } - - let responds: bool = msg_send![ - ns_window, - respondsToSelector: sel!(setOcclusionDetectionEnabled:) - ]; - if responds { - let _: () = msg_send![ns_window, setOcclusionDetectionEnabled: true]; - } - } -} - -unsafe fn enable_webview_occlusion_detection(content_view: cocoa::base::id) { - use cocoa::base::id; - use objc::{msg_send, runtime::Class, sel, sel_impl}; - - // Re-enumerate the live WKWebView subview (its identity/index can change between - // apply and teardown) and restore occlusion detection, mirroring the disable path. - unsafe { - let Some(wkwebview_class) = Class::get("WKWebView") else { - return; - }; - let wkwebview_class = wkwebview_class as *const Class; - - let subviews: id = msg_send![content_view, subviews]; - if subviews == cocoa::base::nil { - return; - } - - let count: usize = msg_send![subviews, count]; - for index in 0..count { - let subview: id = msg_send![subviews, objectAtIndex: index]; - if subview == cocoa::base::nil { - continue; - } - - let is_webview: bool = msg_send![subview, isKindOfClass: wkwebview_class]; - if !is_webview { - continue; - } - - let responds: bool = msg_send![ - subview, - respondsToSelector: sel!(_setWebViewWindowOcclusionDetectionEnabled:) - ]; - if responds { - let _: () = msg_send![subview, _setWebViewWindowOcclusionDetectionEnabled: true]; - return; - } - - let responds: bool = msg_send![ - subview, - respondsToSelector: sel!(_setWindowOcclusionDetectionEnabled:) - ]; - if responds { - let _: () = msg_send![subview, _setWindowOcclusionDetectionEnabled: true]; - return; - } - - return; - } - } -} - -/// Reverse every WindowServer-visible mutation `apply_liquid_glass_background` makes: -/// remove the NSGlassEffectView, then restore window/WKWebView occlusion detection and -/// window opacity. MUST run on the AppKit main thread. -unsafe fn teardown_liquid_glass_ns(ns_window: cocoa::base::id) { - use cocoa::{ - base::{id, nil}, - foundation::NSString, - }; - use objc::{msg_send, runtime::Class, sel, sel_impl}; - - unsafe { - let content_view: id = msg_send![ns_window, contentView]; - if content_view == nil { - return; - } - - if let Some(glass_class) = Class::get("NSGlassEffectView") { - let glass_identifier = NSString::alloc(nil).init_str(LIQUID_GLASS_IDENTIFIER); - remove_liquid_glass_subviews( - content_view, - glass_identifier, - glass_class as *const Class, - ); - } - - enable_window_occlusion_detection(ns_window); - enable_webview_occlusion_detection(content_view); - let _: () = msg_send![ns_window, setOpaque: true]; - } -} - -/// Tear down liquid glass on every webview window. MUST be called on the AppKit main -/// thread (e.g. from the `RunEvent::Exit` handler, which runs on main). Returns the -/// number of windows processed. -pub fn teardown_all_liquid_glass_on_main(app: &tauri::AppHandle) -> usize { - use tauri::Manager; - - let mut count = 0usize; - for (_label, window) in app.webview_windows() { - if let Ok(ns_window) = window.ns_window() { - unsafe { - teardown_liquid_glass_ns(ns_window as cocoa::base::id); - } - count += 1; - } - } - count -} - -/// Dispatch liquid-glass teardown onto the main thread and await it. Safe to call -/// from a tokio worker during exit: the AppKit run loop is still pumping because the -/// macOS terminate handler returned NSTerminateCancel before spawning the async exit. -pub async fn teardown_all_liquid_glass(app: &tauri::AppHandle) -> Result<(), String> { - let app_for_closure = app.clone(); - let (tx, rx) = tokio::sync::oneshot::channel(); - - app.run_on_main_thread(move || { - let count = teardown_all_liquid_glass_on_main(&app_for_closure); - let _ = tx.send(count); - }) - .map_err(|error| error.to_string())?; - - let count = rx - .await - .map_err(|_| "liquid glass teardown task was cancelled".to_string())?; - tracing::info!( - target: "cap_desktop_lib::liquid_glass", - windows = count, - "Tore down liquid glass before exit" - ); - Ok(()) -} - -// pub fn get_ns_window_number(ns_window: *mut c_void) -> isize { -// let ns_window = ns_window as *const objc2_app_kit::NSWindow; - -// unsafe { (*ns_window).windowNumber() } -// } - -// #[link(name = "CoreGraphics", kind = "framework")] -// unsafe extern "C" { -// pub fn CGRectMakeWithDictionaryRepresentation( -// dict: CFDictionaryRef, -// rect: *mut CGRect, -// ) -> boolean_t; -// } - -// /// Makes the background of the WKWebView layer transparent. -// /// This differs from Tauri's implementation as it does not change the window background which causes performance performance issues and artifacts when shadows are enabled on the window. -// /// Use Tauri's implementation to make the window itself transparent. -// pub fn make_webview_transparent(target: &tauri::WebviewWindow) -> tauri::Result<()> { -// target.with_webview(|webview| unsafe { -// let wkwebview = webview.inner() as id; -// let no: id = msg_send![class!(NSNumber), numberWithBool:0]; -// // [https://developer.apple.com/documentation/webkit/webview/1408486-drawsbackground] -// let _: id = msg_send![wkwebview, setValue:no forKey: NSString::alloc(nil).init_str("drawsBackground")]; -// }) -// } - -#[cfg(test)] -mod tests { - use super::constrain_position_to_visible_top; - use tauri::PhysicalPosition; - - #[test] - fn moves_a_window_below_a_notched_visible_frame() { - let position = PhysicalPosition::new(640, 0); - - assert_eq!( - constrain_position_to_visible_top(position, 0, 2.0, 19.0, 18.0), - Some(PhysicalPosition::new(640, 38)) - ); - } - - #[test] - fn uses_the_safe_area_when_the_menu_bar_is_hidden() { - let position = PhysicalPosition::new(200, 0); - - assert_eq!( - constrain_position_to_visible_top(position, 0, 2.0, 0.0, 37.0), - Some(PhysicalPosition::new(200, 74)) - ); - } - - #[test] - fn preserves_negative_coordinates_on_a_monitor_above_the_primary() { - let position = PhysicalPosition::new(-600, -1800); - - assert_eq!( - constrain_position_to_visible_top(position, -1800, 1.5, 25.0, 0.0), - Some(PhysicalPosition::new(-600, -1762)) - ); - } - - #[test] - fn leaves_an_accessible_position_unchanged() { - let position = PhysicalPosition::new(300, 100); - - assert_eq!( - constrain_position_to_visible_top(position, 0, 2.0, 19.0, 18.0), - None - ); - } -} diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs deleted file mode 100644 index 4d0d8f7e3e7..00000000000 --- a/apps/desktop/src-tauri/src/tray.rs +++ /dev/null @@ -1,1125 +0,0 @@ -use crate::{ - NewScreenshotAdded, NewStudioRecordingAdded, RecordingStarted, RecordingStopped, - RequestOpenSettings, recording, - recording_settings::{RecordingSettingsStore, RecordingTargetMode}, - windows::ShowCapWindow, -}; -use cap_recording::RecordingMode; - -use cap_project::{RecordingMeta, RecordingMetaInner}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::{ - path::PathBuf, - sync::{Arc, Mutex}, -}; -use tauri::Manager; -use tauri::menu::{IconMenuItem, MenuId, PredefinedMenuItem, Submenu}; -use tauri::{ - AppHandle, - image::Image, - menu::{Menu, MenuItem}, - tray::{TrayIcon, TrayIconBuilder}, -}; -use tauri_plugin_dialog::DialogExt; -use tauri_plugin_opener::OpenerExt; -use tauri_specta::Event; - -const PREVIOUS_ITEM_PREFIX: &str = "previous_item_"; -const MAX_PREVIOUS_ITEMS: usize = 6; -const MAX_TITLE_LENGTH: usize = 30; -const THUMBNAIL_SIZE: u32 = 32; - -#[cfg(target_os = "linux")] -#[derive(Clone, Copy)] -enum LinuxTrayIcon { - Default, - Instant, - Screenshot, - Studio, - Stop, -} - -#[cfg(target_os = "linux")] -impl LinuxTrayIcon { - fn name(self) -> &'static str { - match self { - Self::Default => "so.cap.desktop-tray-default-symbolic", - Self::Instant => "so.cap.desktop-tray-instant-symbolic", - Self::Screenshot => "so.cap.desktop-tray-screenshot-symbolic", - Self::Studio => "so.cap.desktop-tray-studio-symbolic", - Self::Stop => "so.cap.desktop-tray-stop-symbolic", - } - } - - fn svg(self) -> &'static str { - match self { - Self::Default => { - include_str!("../icons/linux/so.cap.desktop-tray-default-symbolic.svg") - } - Self::Instant => { - include_str!("../icons/linux/so.cap.desktop-tray-instant-symbolic.svg") - } - Self::Screenshot => { - include_str!("../icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg") - } - Self::Studio => include_str!("../icons/linux/so.cap.desktop-tray-studio-symbolic.svg"), - Self::Stop => include_str!("../icons/linux/so.cap.desktop-tray-stop-symbolic.svg"), - } - } -} - -#[derive(Debug)] -pub enum TrayItem { - OpenCap, - RecordDisplay, - RecordWindow, - RecordArea, - TakeScreenshot, - ImportVideo, - ViewAllRecordings, - ViewAllScreenshots, - OpenSettings, - UploadLogs, - Quit, - PreviousItem(String), - ModeStudio, - ModeInstant, - ModeScreenshot, - RequestPermissions, -} - -impl From for MenuId { - fn from(value: TrayItem) -> Self { - match value { - TrayItem::OpenCap => "open_cap", - TrayItem::RecordDisplay => "record_display", - TrayItem::RecordWindow => "record_window", - TrayItem::RecordArea => "record_area", - TrayItem::TakeScreenshot => "take_screenshot", - TrayItem::ImportVideo => "import_video", - TrayItem::ViewAllRecordings => "view_all_recordings", - TrayItem::ViewAllScreenshots => "view_all_screenshots", - TrayItem::OpenSettings => "open_settings", - TrayItem::UploadLogs => "upload_logs", - TrayItem::Quit => "quit", - TrayItem::PreviousItem(id) => { - return format!("{PREVIOUS_ITEM_PREFIX}{id}").into(); - } - TrayItem::ModeStudio => "mode_studio", - TrayItem::ModeInstant => "mode_instant", - TrayItem::ModeScreenshot => "mode_screenshot", - TrayItem::RequestPermissions => "request_permissions", - } - .into() - } -} - -impl TryFrom for TrayItem { - type Error = String; - - fn try_from(value: MenuId) -> Result { - let id_str = value.0.as_str(); - - if let Some(path) = id_str.strip_prefix(PREVIOUS_ITEM_PREFIX) { - return Ok(TrayItem::PreviousItem(path.to_string())); - } - - match id_str { - "open_cap" => Ok(TrayItem::OpenCap), - "record_display" => Ok(TrayItem::RecordDisplay), - "record_window" => Ok(TrayItem::RecordWindow), - "record_area" => Ok(TrayItem::RecordArea), - "take_screenshot" => Ok(TrayItem::TakeScreenshot), - "import_video" => Ok(TrayItem::ImportVideo), - "view_all_recordings" => Ok(TrayItem::ViewAllRecordings), - "view_all_screenshots" => Ok(TrayItem::ViewAllScreenshots), - "open_settings" => Ok(TrayItem::OpenSettings), - "upload_logs" => Ok(TrayItem::UploadLogs), - "quit" => Ok(TrayItem::Quit), - "mode_studio" => Ok(TrayItem::ModeStudio), - "mode_instant" => Ok(TrayItem::ModeInstant), - "mode_screenshot" => Ok(TrayItem::ModeScreenshot), - "request_permissions" => Ok(TrayItem::RequestPermissions), - value => Err(format!("Invalid tray item id {value}")), - } - } -} - -#[derive(Debug, Clone)] -enum PreviousItemType { - StudioRecording, - InstantRecording { - #[allow(dead_code)] - link: Option, - }, - Screenshot, -} - -#[derive(Clone)] -struct CachedPreviousItem { - path: PathBuf, - pretty_name: String, - thumbnail: Option>, - thumbnail_width: u32, - thumbnail_height: u32, - item_type: PreviousItemType, - created_at: std::time::SystemTime, -} - -#[derive(Default)] -struct PreviousItemsCache { - items: Vec, -} - -fn screenshots_path(app: &AppHandle) -> PathBuf { - let path = app.path().app_data_dir().unwrap().join("screenshots"); - std::fs::create_dir_all(&path).unwrap_or_default(); - path -} - -fn truncate_title(title: &str) -> String { - if title.chars().count() <= MAX_TITLE_LENGTH { - title.to_string() - } else { - let truncate_at = MAX_TITLE_LENGTH - 1; - let byte_index = title - .char_indices() - .nth(truncate_at) - .map(|(i, _)| i) - .unwrap_or(title.len()); - format!("{}…", &title[..byte_index]) - } -} - -fn load_thumbnail_data(path: &PathBuf) -> Option<(Vec, u32, u32)> { - use image::imageops::FilterType; - use image::{GenericImageView, RgbaImage}; - - let image_data = std::fs::read(path).ok()?; - let img = image::load_from_memory(&image_data).ok()?; - - let (orig_w, orig_h) = img.dimensions(); - let size = THUMBNAIL_SIZE; - - let scale = (size as f32 / orig_w as f32).max(size as f32 / orig_h as f32); - let scaled_w = (orig_w as f32 * scale).round() as u32; - let scaled_h = (orig_h as f32 * scale).round() as u32; - - let scaled = img.resize_exact(scaled_w, scaled_h, FilterType::Triangle); - - let x_offset = (scaled_w.saturating_sub(size)) / 2; - let y_offset = (scaled_h.saturating_sub(size)) / 2; - - let mut result = RgbaImage::new(size, size); - for y in 0..size { - for x in 0..size { - let src_x = x + x_offset; - let src_y = y + y_offset; - if src_x < scaled_w && src_y < scaled_h { - result.put_pixel(x, y, scaled.get_pixel(src_x, src_y)); - } - } - } - - Some((result.into_raw(), size, size)) -} - -fn load_single_item( - path: &PathBuf, - screenshots_dir: &PathBuf, - load_thumbnail: bool, -) -> Option { - if !path.is_dir() { - return None; - } - - let meta = RecordingMeta::load_for_project(path).ok()?; - let created_at = path - .metadata() - .and_then(|m| m.created()) - .unwrap_or_else(|_| std::time::SystemTime::now()); - - let is_screenshot = path.extension().and_then(|s| s.to_str()) == Some("cap") - && path.parent().map(|p| p == screenshots_dir).unwrap_or(false); - - let (thumbnail_path, item_type) = if is_screenshot { - let png_path = std::fs::read_dir(path).ok().and_then(|dir| { - dir.flatten() - .find(|e| e.path().extension().and_then(|s| s.to_str()) == Some("png")) - .map(|e| e.path()) - }); - (png_path, PreviousItemType::Screenshot) - } else { - let thumb = path.join("screenshots/display.jpg"); - let thumb_path = if thumb.exists() { Some(thumb) } else { None }; - let item_type = match &meta.inner { - RecordingMetaInner::Studio(_) => PreviousItemType::StudioRecording, - RecordingMetaInner::Instant(_) => PreviousItemType::InstantRecording { - link: meta.sharing.as_ref().map(|s| s.link.clone()), - }, - }; - (thumb_path, item_type) - }; - - let (thumbnail, thumbnail_width, thumbnail_height) = if load_thumbnail { - thumbnail_path - .as_ref() - .and_then(load_thumbnail_data) - .map(|(data, w, h)| (Some(data), w, h)) - .unwrap_or((None, 0, 0)) - } else { - (None, 0, 0) - }; - - Some(CachedPreviousItem { - path: path.clone(), - pretty_name: meta.pretty_name, - thumbnail, - thumbnail_width, - thumbnail_height, - item_type, - created_at, - }) -} - -fn load_all_previous_items(app: &AppHandle, load_thumbnails: bool) -> Vec { - let mut items = Vec::new(); - let screenshots_dir = screenshots_path(app); - - for recordings_dir in crate::recordings_locations::known_recordings_dirs(app) { - let Ok(entries) = std::fs::read_dir(&recordings_dir) else { - continue; - }; - for entry in entries.flatten() { - if let Some(item) = load_single_item(&entry.path(), &screenshots_dir, load_thumbnails) { - items.push(item); - } - } - } - - if screenshots_dir.exists() - && let Ok(entries) = std::fs::read_dir(&screenshots_dir) - { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|s| s.to_str()) == Some("cap") - && let Some(item) = load_single_item(&path, &screenshots_dir, load_thumbnails) - { - items.push(item); - } - } - } - - items.sort_by_key(|b| std::cmp::Reverse(b.created_at)); - items.truncate(MAX_PREVIOUS_ITEMS); - items -} - -fn create_previous_submenu( - app: &AppHandle, - cache: &PreviousItemsCache, -) -> tauri::Result> { - if cache.items.is_empty() { - let submenu = Submenu::with_id(app, "previous", "Previous", false)?; - submenu.append(&MenuItem::with_id( - app, - "previous_empty", - "No recent items", - false, - None::<&str>, - )?)?; - return Ok(submenu); - } - - let submenu = Submenu::with_id(app, "previous", "Previous", true)?; - - for item in &cache.items { - let id = TrayItem::PreviousItem(item.path.to_string_lossy().to_string()); - let title = truncate_title(&item.pretty_name); - - let type_indicator = match &item.item_type { - PreviousItemType::StudioRecording => "🎬 ", - PreviousItemType::InstantRecording { .. } => "⚡ ", - PreviousItemType::Screenshot => "📷 ", - }; - let display_title = format!("{type_indicator}{title}"); - - let icon = item.thumbnail.as_ref().map(|data| { - Image::new_owned(data.clone(), item.thumbnail_width, item.thumbnail_height) - }); - - let menu_item = IconMenuItem::with_id(app, id, display_title, true, icon, None::<&str>)?; - submenu.append(&menu_item)?; - } - - Ok(submenu) -} - -fn get_current_mode(app: &AppHandle) -> RecordingMode { - RecordingSettingsStore::get(app) - .ok() - .flatten() - .and_then(|s| s.mode) - .unwrap_or_default() -} - -fn should_use_minimal_onboarding_tray_menu(app: &AppHandle) -> bool { - if !app.webview_windows().contains_key("onboarding") { - return false; - } - !crate::permissions::do_permissions_check(false).necessary_granted() -} - -pub(crate) struct TrayMenuCache { - cache: Arc>, -} - -pub(crate) fn refresh_tray_menu_for_app(app: &AppHandle) { - let Some(state) = app.try_state::() else { - return; - }; - refresh_tray_menu(app, &state.cache); -} - -fn create_mode_submenu(app: &AppHandle) -> tauri::Result> { - let current_mode = get_current_mode(app); - - let submenu = Submenu::with_id(app, "select_mode", "Select Mode", true)?; - - let modes = [ - (TrayItem::ModeStudio, RecordingMode::Studio, "Studio"), - (TrayItem::ModeInstant, RecordingMode::Instant, "Instant"), - ( - TrayItem::ModeScreenshot, - RecordingMode::Screenshot, - "Screenshot", - ), - ]; - - for (tray_item, mode, label) in modes { - let is_selected = current_mode == mode; - let display_label = if is_selected { - format!("✓ {label}") - } else { - format!(" {label}") - }; - - let menu_item = MenuItem::with_id(app, tray_item, display_label, true, None::<&str>)?; - submenu.append(&menu_item)?; - } - - Ok(submenu) -} - -fn build_tray_menu(app: &AppHandle, cache: &PreviousItemsCache) -> tauri::Result> { - if should_use_minimal_onboarding_tray_menu(app) { - return Menu::with_items( - app, - &[ - &MenuItem::with_id( - app, - TrayItem::RequestPermissions, - "Request Permissions", - true, - None::<&str>, - )?, - &PredefinedMenuItem::separator(app)?, - &MenuItem::with_id( - app, - "version", - format!("Cap v{}", env!("CARGO_PKG_VERSION")), - false, - None::<&str>, - )?, - &MenuItem::with_id(app, TrayItem::Quit, "Quit Cap", true, None::<&str>)?, - ], - ); - } - - let previous_submenu = create_previous_submenu(app, cache)?; - let mode_submenu = create_mode_submenu(app)?; - let current_mode = get_current_mode(app); - let is_screenshot_mode = current_mode == RecordingMode::Screenshot; - - let menu = Menu::new(app)?; - - menu.append(&MenuItem::with_id( - app, - TrayItem::OpenCap, - "Open Main Window", - true, - None::<&str>, - )?)?; - - if is_screenshot_mode { - menu.append(&MenuItem::with_id( - app, - TrayItem::RecordDisplay, - "Screenshot Display", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::RecordWindow, - "Screenshot Window", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::RecordArea, - "Screenshot Area", - true, - None::<&str>, - )?)?; - } else { - menu.append(&MenuItem::with_id( - app, - TrayItem::RecordDisplay, - "Record Display", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::RecordWindow, - "Record Window", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::RecordArea, - "Record Area", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::TakeScreenshot, - "Take a Screenshot", - true, - None::<&str>, - )?)?; - } - - menu.append(&MenuItem::with_id( - app, - TrayItem::ImportVideo, - "Import Media...", - true, - None::<&str>, - )?)?; - - menu.append(&PredefinedMenuItem::separator(app)?)?; - menu.append(&mode_submenu)?; - menu.append(&previous_submenu)?; - menu.append(&PredefinedMenuItem::separator(app)?)?; - - menu.append(&MenuItem::with_id( - app, - TrayItem::ViewAllRecordings, - "View all recordings", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::ViewAllScreenshots, - "View all screenshots", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::OpenSettings, - "Settings", - true, - None::<&str>, - )?)?; - - menu.append(&PredefinedMenuItem::separator(app)?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::UploadLogs, - "Upload Logs", - true, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - "version", - format!("Cap v{}", env!("CARGO_PKG_VERSION")), - false, - None::<&str>, - )?)?; - menu.append(&MenuItem::with_id( - app, - TrayItem::Quit, - "Quit Cap", - true, - None::<&str>, - )?)?; - - Ok(menu) -} - -fn add_new_item_to_cache(cache: &Arc>, app: &AppHandle, path: PathBuf) { - let screenshots_dir = screenshots_path(app); - - let Some(new_item) = load_single_item(&path, &screenshots_dir, true) else { - return; - }; - - let mut cache_guard = cache.lock().unwrap(); - - cache_guard.items.retain(|item| item.path != path); - - cache_guard.items.insert(0, new_item); - - cache_guard.items.truncate(MAX_PREVIOUS_ITEMS); -} - -fn refresh_tray_menu(app: &AppHandle, cache: &Arc>) { - let app_clone = app.clone(); - let cache_clone = cache.clone(); - - let _ = app.run_on_main_thread(move || { - let Some(tray) = app_clone.tray_by_id("tray") else { - return; - }; - - let cache_guard = cache_clone.lock().unwrap(); - if let Ok(menu) = build_tray_menu(&app_clone, &cache_guard) { - let _ = tray.set_menu(Some(menu)); - } - }); -} - -fn handle_previous_item_click(app: &AppHandle, path_str: &str) { - let path = PathBuf::from(path_str); - - let screenshots_dir = screenshots_path(app); - let is_screenshot = path.extension().and_then(|s| s.to_str()) == Some("cap") - && path.parent().map(|p| p == screenshots_dir).unwrap_or(false); - - if is_screenshot { - let app = app.clone(); - let screenshot_path = path; - tokio::spawn(async move { - let _ = ShowCapWindow::ScreenshotEditor { - path: screenshot_path, - } - .show(&app) - .await; - }); - return; - } - - let meta = match RecordingMeta::load_for_project(&path) { - Ok(m) => m, - Err(e) => { - tracing::error!("Failed to load recording meta for previous item: {e}"); - return; - } - }; - - match &meta.inner { - RecordingMetaInner::Studio(_) => { - let app = app.clone(); - let project_path = path.clone(); - tokio::spawn(async move { - let _ = ShowCapWindow::Editor { project_path }.show(&app).await; - }); - } - RecordingMetaInner::Instant(_) => { - if let Some(sharing) = &meta.sharing { - let _ = app.opener().open_url(&sharing.link, None::); - } else { - let mp4_path = path.join("content/output.mp4"); - if mp4_path.exists() { - let _ = app - .opener() - .open_path(mp4_path.to_str().unwrap_or_default(), None::); - } - } - } - } -} - -pub fn get_tray_icon() -> &'static [u8] { - include_bytes!("../icons/tray-default-icon.png") -} - -pub fn get_mode_icon(mode: RecordingMode) -> &'static [u8] { - if cfg!(target_os = "windows") { - return get_tray_icon(); - } - match mode { - RecordingMode::Studio => include_bytes!("../icons/tray-default-icon-studio.png"), - RecordingMode::Instant => include_bytes!("../icons/tray-default-icon-instant.png"), - RecordingMode::Screenshot => include_bytes!("../icons/tray-default-icon-screenshot.png"), - } -} - -#[cfg(target_os = "linux")] -fn linux_tray_icon_for_mode(mode: RecordingMode) -> LinuxTrayIcon { - match mode { - RecordingMode::Studio => LinuxTrayIcon::Studio, - RecordingMode::Instant => LinuxTrayIcon::Instant, - RecordingMode::Screenshot => LinuxTrayIcon::Screenshot, - } -} - -#[cfg(target_os = "linux")] -fn write_linux_tray_symbolic_icon(icon: LinuxTrayIcon) -> std::io::Result { - let dir = dirs::runtime_dir() - .unwrap_or_else(std::env::temp_dir) - .join("cap-tray-icons"); - std::fs::create_dir_all(&dir)?; - - let path = dir.join(format!("{}.svg", icon.name())); - let svg = icon.svg().as_bytes(); - if std::fs::read(&path).ok().as_deref() != Some(svg) { - std::fs::write(path, svg)?; - } - - Ok(dir) -} - -#[cfg(target_os = "linux")] -fn set_linux_tray_icon(tray: &TrayIcon, icon: LinuxTrayIcon) -> tauri::Result<()> { - let icon_dir = write_linux_tray_symbolic_icon(icon).map_err(tauri::Error::Io)?; - let icon_name = icon.name().to_string(); - - tray.with_inner_tray_icon(move |inner| unsafe { - let indicator = inner.app_indicator() as *mut libappindicator::AppIndicator; - if let Some(indicator) = indicator.as_mut() { - indicator.set_icon_theme_path(&icon_dir.to_string_lossy()); - indicator.set_icon_full(&icon_name, "Cap tray icon"); - } - })?; - - Ok(()) -} - -#[cfg(target_os = "linux")] -fn set_tray_icon_for_mode(tray: &TrayIcon, mode: RecordingMode) -> tauri::Result<()> { - set_linux_tray_icon(tray, linux_tray_icon_for_mode(mode)) -} - -#[cfg(not(target_os = "linux"))] -fn set_tray_icon_for_mode(tray: &TrayIcon, mode: RecordingMode) -> tauri::Result<()> { - let icon = Image::from_bytes(get_mode_icon(mode))?; - tray.set_icon(Some(icon))?; - tray.set_icon_as_template(true)?; - Ok(()) -} - -#[cfg(target_os = "linux")] -fn set_tray_stop_icon(tray: &TrayIcon) -> tauri::Result<()> { - set_linux_tray_icon(tray, LinuxTrayIcon::Stop) -} - -#[cfg(not(target_os = "linux"))] -fn set_tray_stop_icon(tray: &TrayIcon) -> tauri::Result<()> { - let icon = Image::from_bytes(include_bytes!("../icons/tray-stop-icon.png"))?; - tray.set_icon(Some(icon))?; - tray.set_icon_as_template(true)?; - Ok(()) -} - -pub fn update_tray_icon_for_mode(app: &AppHandle, mode: RecordingMode) { - if cfg!(target_os = "windows") { - return; - } - - let Some(tray) = app.tray_by_id("tray") else { - return; - }; - - if let Err(error) = set_tray_icon_for_mode(&tray, mode) { - tracing::warn!("Failed to update tray icon: {error}"); - } -} - -fn handle_mode_selection( - app: &AppHandle, - mode: RecordingMode, - cache: &Arc>, -) { - if let Err(e) = RecordingSettingsStore::set_mode(app, mode) { - tracing::error!("Failed to set recording mode: {e}"); - return; - } - - update_tray_icon_for_mode(app, mode); - refresh_tray_menu(app, cache); -} - -pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { - let items = load_all_previous_items(app, false); - let cache = Arc::new(Mutex::new(PreviousItemsCache { items })); - - app.manage(TrayMenuCache { - cache: cache.clone(), - }); - - let menu = { - let cache_guard = cache.lock().unwrap(); - build_tray_menu(app, &cache_guard)? - }; - let app = app.clone(); - let is_recording = Arc::new(AtomicBool::new(false)); - - let current_mode = get_current_mode(&app); - let initial_icon = Image::from_bytes(get_mode_icon(current_mode))?; - - let _ = TrayIconBuilder::with_id("tray") - .icon(initial_icon) - .icon_as_template(cfg!(target_os = "macos")) - .menu(&menu) - .show_menu_on_left_click(true) - .on_menu_event({ - let app_handle = app.clone(); - let cache = cache.clone(); - move |app: &AppHandle, event| match TrayItem::try_from(event.id) { - Ok(TrayItem::OpenCap) => { - let app = app.clone(); - tokio::spawn(async move { - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(&app) - .await; - }); - } - Ok(TrayItem::RecordDisplay) => { - let app = app.clone(); - tokio::spawn(async move { - crate::open_target_picker(&app, RecordingTargetMode::Display).await; - }); - } - Ok(TrayItem::RecordWindow) => { - let app = app.clone(); - tokio::spawn(async move { - crate::open_target_picker(&app, RecordingTargetMode::Window).await; - }); - } - Ok(TrayItem::RecordArea) => { - let app = app.clone(); - tokio::spawn(async move { - crate::open_target_picker(&app, RecordingTargetMode::Area).await; - }); - } - Ok(TrayItem::TakeScreenshot) => { - let app = app.clone(); - tokio::spawn(async move { - use cap_recording::screen_capture::ScreenCaptureTarget; - use scap_targets::Display; - - let display = - Display::get_containing_cursor().unwrap_or_else(Display::primary); - let target = ScreenCaptureTarget::Display { id: display.id() }; - - match recording::take_screenshot(app.clone(), target.clone()).await { - Ok(path) => { - if crate::automation::should_open_screenshot_editor(&app, &target) { - let _ = - ShowCapWindow::ScreenshotEditor { path }.show(&app).await; - } - } - Err(e) => { - tracing::error!("Failed to take screenshot: {e}"); - } - } - }); - } - Ok(TrayItem::ImportVideo) => { - let app = app.clone(); - tokio::spawn(async move { - let file_path = app - .dialog() - .file() - .add_filter( - "Media Files", - &[ - "mp4", "mov", "avi", "mkv", "webm", "wmv", "m4v", "flv", "png", - "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff", - ], - ) - .add_filter( - "Video Files", - &["mp4", "mov", "avi", "mkv", "webm", "wmv", "m4v", "flv"], - ) - .add_filter( - "Image Files", - &["png", "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff"], - ) - .blocking_pick_file(); - - if let Some(file_path) = file_path { - let path = match file_path.into_path() { - Ok(p) => p, - Err(e) => { - tracing::error!("Invalid file path: {e}"); - return; - } - }; - - if crate::import::is_supported_video_import_path(&path) { - match crate::import::start_video_import(app.clone(), path).await { - Ok(project_path) => { - let _ = - ShowCapWindow::Editor { project_path }.show(&app).await; - } - Err(e) => { - tracing::error!("Failed to import video: {e}"); - app.dialog() - .message(format!("Failed to import video: {e}")) - .title("Import Error") - .kind(tauri_plugin_dialog::MessageDialogKind::Error) - .blocking_show(); - } - } - } else if crate::import::is_supported_image_import_path(&path) { - match crate::import::start_image_import(app.clone(), path).await { - Ok(path) => { - let _ = ShowCapWindow::ScreenshotEditor { path } - .show(&app) - .await; - } - Err(e) => { - tracing::error!("Failed to import image: {e}"); - app.dialog() - .message(format!("Failed to import image: {e}")) - .title("Import Error") - .kind(tauri_plugin_dialog::MessageDialogKind::Error) - .blocking_show(); - } - } - } else { - tracing::error!("Unsupported media import path: {:?}", path); - app.dialog() - .message("Unsupported media file.") - .title("Import Error") - .kind(tauri_plugin_dialog::MessageDialogKind::Error) - .blocking_show(); - } - } - }); - } - Ok(TrayItem::ViewAllRecordings) => { - let _ = RequestOpenSettings { - page: "recordings".to_string(), - } - .emit(&app_handle); - } - Ok(TrayItem::ViewAllScreenshots) => { - let _ = RequestOpenSettings { - page: "screenshots".to_string(), - } - .emit(&app_handle); - } - Ok(TrayItem::OpenSettings) => { - let app = app.clone(); - tokio::spawn( - async move { ShowCapWindow::Settings { page: None }.show(&app).await }, - ); - } - Ok(TrayItem::UploadLogs) => { - let app = app.clone(); - tokio::spawn(async move { - match crate::logging::upload_log_file(&app).await { - Ok(_) => { - tracing::info!("Successfully uploaded logs"); - app.dialog() - .message("Logs uploaded successfully") - .show(|_| {}); - } - Err(e) => { - tracing::error!("Failed to upload logs: {e:#}"); - app.dialog().message("Failed to upload logs").show(|_| {}); - } - } - }); - } - Ok(TrayItem::Quit) => { - let app = app.clone(); - tokio::spawn(async move { - crate::request_app_exit(app).await; - }); - } - Ok(TrayItem::PreviousItem(path)) => { - handle_previous_item_click(app, &path); - } - Ok(TrayItem::ModeStudio) => { - handle_mode_selection(app, RecordingMode::Studio, &cache); - } - Ok(TrayItem::ModeInstant) => { - handle_mode_selection(app, RecordingMode::Instant, &cache); - } - Ok(TrayItem::ModeScreenshot) => { - handle_mode_selection(app, RecordingMode::Screenshot, &cache); - } - Ok(TrayItem::RequestPermissions) => { - let app = app.clone(); - tokio::spawn(async move { - let _ = ShowCapWindow::Onboarding.show(&app).await; - }); - } - _ => {} - } - }) - .on_tray_icon_event({ - let is_recording = Arc::clone(&is_recording); - let app_handle = app.clone(); - move |tray, event| { - if let tauri::tray::TrayIconEvent::Click { .. } = event { - if is_recording.load(Ordering::Relaxed) { - let app = app_handle.clone(); - tokio::spawn(async move { - let _ = recording::stop_recording(app.clone(), app.state()).await; - }); - } else { - let _ = tray.set_visible(true); - } - } - } - }) - .build(&app); - - #[cfg(target_os = "linux")] - if let Some(tray) = app.tray_by_id("tray") - && let Err(error) = set_tray_icon_for_mode(&tray, current_mode) - { - tracing::warn!("Failed to initialize Linux tray icon: {error}"); - } - - { - let app_clone = app.clone(); - let cache_clone = cache.clone(); - std::thread::spawn(move || { - let screenshots_dir = screenshots_path(&app_clone); - let items_needing_thumbnails: Vec = { - let cache_guard = cache_clone.lock().unwrap(); - cache_guard - .items - .iter() - .filter(|item| item.thumbnail.is_none()) - .map(|item| item.path.clone()) - .collect() - }; - - if items_needing_thumbnails.is_empty() { - return; - } - - for path in items_needing_thumbnails { - if let Some(updated_item) = load_single_item(&path, &screenshots_dir, true) { - let mut cache_guard = cache_clone.lock().unwrap(); - if let Some(existing) = cache_guard.items.iter_mut().find(|i| i.path == path) { - existing.thumbnail = updated_item.thumbnail; - existing.thumbnail_width = updated_item.thumbnail_width; - existing.thumbnail_height = updated_item.thumbnail_height; - } - } - } - - let app_for_refresh = app_clone.clone(); - let cache_for_refresh = cache_clone.clone(); - let _ = app_clone.run_on_main_thread(move || { - if let Some(tray) = app_for_refresh.tray_by_id("tray") { - let cache_guard = cache_for_refresh.lock().unwrap(); - if let Ok(menu) = build_tray_menu(&app_for_refresh, &cache_guard) { - let _ = tray.set_menu(Some(menu)); - } - } - }); - }); - } - - RecordingStarted::listen_any(&app, { - let app = app.clone(); - let is_recording = is_recording.clone(); - move |_| { - is_recording.store(true, Ordering::Relaxed); - - if cfg!(target_os = "windows") { - return; - } - - let Some(tray) = app.tray_by_id("tray") else { - return; - }; - - if let Err(error) = set_tray_stop_icon(&tray) { - tracing::warn!("Failed to update tray icon for recording start: {error}"); - } - } - }); - - RecordingStopped::listen_any(&app, { - let app_handle = app.clone(); - let is_recording = is_recording.clone(); - move |_| { - is_recording.store(false, Ordering::Relaxed); - - if cfg!(target_os = "windows") { - return; - } - - let Some(tray) = app_handle.tray_by_id("tray") else { - return; - }; - - let current_mode = get_current_mode(&app_handle); - if let Err(error) = set_tray_icon_for_mode(&tray, current_mode) { - tracing::warn!("Failed to update tray icon for recording stop: {error}"); - } - } - }); - - NewStudioRecordingAdded::listen_any(&app, { - let app_handle = app.clone(); - let cache_clone = cache.clone(); - move |event| { - add_new_item_to_cache(&cache_clone, &app_handle, event.payload.path.clone()); - refresh_tray_menu(&app_handle, &cache_clone); - } - }); - - NewScreenshotAdded::listen_any(&app, { - let app_handle = app.clone(); - let cache_clone = cache.clone(); - move |event| { - let path = if event.payload.path.extension().and_then(|s| s.to_str()) == Some("png") { - event.payload.path.parent().map(|p| p.to_path_buf()) - } else { - Some(event.payload.path.clone()) - }; - - if let Some(path) = path { - add_new_item_to_cache(&cache_clone, &app_handle, path); - refresh_tray_menu(&app_handle, &cache_clone); - } - } - }); - - crate::recordings_locations::RecordingsMigrationProgress::listen_any(&app, { - let app_handle = app.clone(); - let cache_clone = cache.clone(); - move |event| { - // Rebuild once when a storage-folder migration finishes: moved - // projects changed paths, so cached entries are stale. - if event.payload.done == event.payload.total { - let items = load_all_previous_items(&app_handle, false); - cache_clone.lock().unwrap().items = items; - refresh_tray_menu(&app_handle, &cache_clone); - } - } - }); - - Ok(()) -} diff --git a/apps/desktop/src-tauri/src/updates.rs b/apps/desktop/src-tauri/src/updates.rs deleted file mode 100644 index ac2e1cb37f8..00000000000 --- a/apps/desktop/src-tauri/src/updates.rs +++ /dev/null @@ -1,380 +0,0 @@ -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::time::Duration; -use tauri::{AppHandle, Manager, Url}; -use tauri_plugin_updater::{Update, UpdaterExt}; -use tauri_specta::Event; -use tokio::sync::{Mutex, Notify}; -use tracing::{info, warn}; - -use crate::general_settings::GeneralSettingsStore; - -const UPDATE_ENDPOINT: &str = - "https://cdn.crabnebula.app/update/cap/cap/{{target}}/{{current_version}}"; - -const FIRST_CHECK_DELAY: Duration = Duration::from_secs(60); -const CHECK_INTERVAL: Duration = Duration::from_secs(2 * 60 * 60); -const BUSY_RETRY_DELAY: Duration = Duration::from_secs(5 * 60); - -#[derive(Serialize, Deserialize, Type, Clone, Copy, PartialEq, Eq, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub enum UpdateChannel { - #[default] - Stable, - Nightly, -} - -#[derive(Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct UpdateCheckResult { - pub version: String, - pub notes: Option, - pub channel: UpdateChannel, -} - -#[derive(Serialize, Type, tauri_specta::Event, Clone, Debug)] -pub struct UpdateDownloadProgress { - pub downloaded: u32, - pub total: Option, -} - -#[derive(Serialize, Type, tauri_specta::Event, Clone, Debug)] -pub struct UpdateReady { - pub version: String, - pub installed: bool, -} - -#[derive(Clone)] -struct PendingUpdate { - update: Update, - version: String, - installed: bool, -} - -#[derive(Default)] -pub struct UpdatesState { - pending: Mutex>, - announced_version: Mutex>, - notify: Notify, -} - -fn current_channel(app: &AppHandle) -> UpdateChannel { - GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|s| s.update_channel) - .unwrap_or_default() -} - -// Mirrors `updaterTarget()` in src/utils/updater.ts; the plugin's built-in -// target reports "macos"/"linux" while CrabNebula releases are keyed on -// "darwin-*" / "linux-*-deb". -fn updater_target() -> String { - let arch = if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - "x86_64" - }; - - if cfg!(target_os = "macos") { - format!("darwin-{arch}") - } else if cfg!(target_os = "linux") { - format!("linux-{arch}-deb") - } else { - format!("windows-{arch}") - } -} - -fn endpoint(channel: UpdateChannel) -> Result { - let url = match channel { - UpdateChannel::Stable => UPDATE_ENDPOINT.to_string(), - UpdateChannel::Nightly => format!("{UPDATE_ENDPOINT}?channel=nightly"), - }; - Url::parse(&url).map_err(|e| e.to_string()) -} - -async fn check_channel( - app: &AppHandle, - channel: UpdateChannel, - allow_stable_downgrade: bool, -) -> Result, String> { - let builder = app - .updater_builder() - .target(updater_target()) - .endpoints(vec![endpoint(channel)?]) - .map_err(|e| e.to_string())?; - - // A user on a nightly prerelease who switches back to Stable should land - // on the newest stable build even though it is semver-lower than their - // current version, so any differing non-prerelease remote counts as an - // update (deliberate downgrade-on-channel-switch semantics). This must - // ONLY apply when the user's configured channel is Stable: a nightly user - // on the latest nightly would otherwise see the older stable release - // qualify and flip-flop between the two channels forever. - let builder = if allow_stable_downgrade { - builder.version_comparator(|current, remote| { - remote.version > current - || (!current.pre.is_empty() - && remote.version.pre.is_empty() - && remote.version != current) - }) - } else { - builder.version_comparator(|current, remote| remote.version > current) - }; - - builder - .build() - .map_err(|e| e.to_string())? - .check() - .await - .map_err(|e| e.to_string()) -} - -fn pick_higher_version(a: Option, b: Option) -> Option { - match (a, b) { - (Some(a), Some(b)) => { - match ( - semver::Version::parse(&a.version), - semver::Version::parse(&b.version), - ) { - (Ok(a_version), Ok(b_version)) => Some(if b_version > a_version { b } else { a }), - (Ok(_), Err(_)) => Some(a), - _ => Some(b), - } - } - (a, b) => a.or(b), - } -} - -pub async fn check(app: &AppHandle) -> Result, String> { - let channel = current_channel(app); - - let stable = check_channel(app, UpdateChannel::Stable, channel == UpdateChannel::Stable).await; - - let update = if channel == UpdateChannel::Nightly { - let nightly = check_channel(app, UpdateChannel::Nightly, false).await; - - // Both channels can produce a candidate when a stable release was - // promoted after the last nightly; the higher version wins. - match (stable, nightly) { - (Ok(stable), Ok(nightly)) => pick_higher_version(stable, nightly), - (Ok(update), Err(err)) | (Err(err), Ok(update)) => { - warn!("Update check failed for one channel: {err}"); - update - } - (Err(err), Err(_)) => return Err(err), - } - } else { - stable? - }; - - let state = app.state::(); - let mut pending = state.pending.lock().await; - *pending = update.as_ref().map(|update| PendingUpdate { - // Keep the installed flag if the background loop already installed - // this version silently. - installed: pending - .as_ref() - .is_some_and(|p| p.version == update.version && p.installed), - version: update.version.clone(), - update: update.clone(), - }); - - Ok(update) -} - -async fn download_with_progress(app: &AppHandle, update: &Update) -> Result, String> { - let mut downloaded: u32 = 0; - update - .download( - |chunk, total| { - downloaded = downloaded.saturating_add(chunk as u32); - let _ = UpdateDownloadProgress { - downloaded, - total: total.and_then(|t| u32::try_from(t).ok()), - } - .emit(app); - }, - || {}, - ) - .await - .map_err(|e| e.to_string()) -} - -async fn is_busy(app: &AppHandle) -> bool { - if crate::export::export_session_active() { - return true; - } - - let Some(state) = app.try_state::>() else { - return true; - }; - - state.read().await.is_recording_active_or_pending() -} - -#[tauri::command] -#[specta::specta] -pub async fn updates_check(app: AppHandle) -> Result, String> { - let channel = current_channel(&app); - Ok(check(&app).await?.map(|update| UpdateCheckResult { - version: update.version.clone(), - notes: update.body.clone(), - channel, - })) -} - -#[tauri::command] -#[specta::specta] -pub async fn updates_download_and_install(app: AppHandle) -> Result<(), String> { - let state = app.state::(); - - let pending = match state.pending.lock().await.clone() { - Some(pending) => pending, - None => { - let Some(update) = check(&app).await? else { - return Err("No update available".to_string()); - }; - PendingUpdate { - version: update.version.clone(), - update, - installed: false, - } - } - }; - - // The macOS background loop may have already installed this version - // silently; restarting is all that's left to do. - if pending.installed { - return Ok(()); - } - - let bytes = download_with_progress(&app, &pending.update).await?; - - info!("Installing update {}", pending.version); - pending.update.install(bytes).map_err(|e| e.to_string())?; - - let mut guard = state.pending.lock().await; - if let Some(p) = guard.as_mut() - && p.version == pending.version - { - p.installed = true; - } - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -pub fn updates_channel_changed(app: AppHandle) -> Result<(), String> { - app.state::().notify.notify_one(); - Ok(()) -} - -pub fn spawn_background_loop(app: AppHandle) { - // Never auto-update dev builds. - if cfg!(debug_assertions) { - return; - } - - tokio::spawn(async move { - let mut delay = FIRST_CHECK_DELAY; - - loop { - { - let state = app.state::(); - tokio::select! { - _ = tokio::time::sleep(delay) => {} - _ = state.notify.notified() => {} - } - } - - delay = CHECK_INTERVAL; - - // Stable updates stay frontend-driven; this loop only owns nightly. - if current_channel(&app) != UpdateChannel::Nightly { - continue; - } - - if is_busy(&app).await { - delay = BUSY_RETRY_DELAY; - continue; - } - - let update = match check(&app).await { - Ok(Some(update)) => update, - Ok(None) => continue, - Err(err) => { - warn!("Nightly update check failed: {err}"); - continue; - } - }; - - let version = update.version.clone(); - let state = app.state::(); - - if state.announced_version.lock().await.as_deref() == Some(version.as_str()) { - continue; - } - - let installed = if cfg!(target_os = "macos") { - let already_installed = state - .pending - .lock() - .await - .as_ref() - .is_some_and(|p| p.version == version && p.installed); - - if already_installed { - true - } else { - let bytes = match update.download(|_, _| {}, || {}).await { - Ok(bytes) => bytes, - Err(err) => { - warn!("Failed to download nightly update {version}: {err}"); - continue; - } - }; - - // A recording or export may have started mid-download; - // don't touch the install while one is running. - if is_busy(&app).await { - delay = BUSY_RETRY_DELAY; - continue; - } - - // Safe while Cap runs: the .app bundle is swapped in place - // and takes effect on relaunch. - if let Err(err) = update.install(bytes) { - warn!("Failed to install nightly update {version}: {err}"); - continue; - } - - let mut pending = state.pending.lock().await; - if let Some(p) = pending.as_mut() - && p.version == version - { - p.installed = true; - } - - info!("Nightly update {version} installed; restart to apply"); - true - } - } else { - // Windows (NSIS) install exits the app mid-session and Linux - // (deb) prompts for privileges, so never auto-install there; - // announce and let the user trigger the install. - info!("Nightly update {version} available"); - false - }; - - let _ = UpdateReady { - version: version.clone(), - installed, - } - .emit(&app); - - *state.announced_version.lock().await = Some(version); - } - }); -} diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs deleted file mode 100644 index 5deba345c4e..00000000000 --- a/apps/desktop/src-tauri/src/windows.rs +++ /dev/null @@ -1,3707 +0,0 @@ -#![allow(unused_mut)] -#![allow(unused_imports)] - -use anyhow::anyhow; -use futures::pin_mut; -use scap_targets::{Display, DisplayId}; -use serde::Deserialize; -use specta::Type; -use std::{ - ops::Deref, - path::PathBuf, - str::FromStr, - sync::{ - Arc, Mutex, - atomic::{AtomicU32, AtomicU64, Ordering}, - }, - time::Duration, -}; -use tauri::{ - AppHandle, LogicalPosition, LogicalSize, Manager, Monitor, PhysicalPosition, PhysicalSize, - WebviewUrl, WebviewWindow, WebviewWindowBuilder, Wry, -}; -use tauri_specta::Event; -use tokio::sync::RwLock; -use tracing::{debug, error, info, instrument, warn}; - -#[cfg(target_os = "macos")] -use crate::panel_manager::{PanelManager, PanelState, PanelWindowType, is_window_handle_valid}; - -use crate::{ - App, ArcLock, CameraWindowCloseGate, CameraWindowPositionGuard, MainWindowReadyState, - NewNotification, RequestSetTargetMode, camera_preview_error_message, - editor_window::PendingEditorInstances, - emit_camera_preview_clear, emit_camera_preview_error, fake_window, - general_settings::{self, AppTheme, GeneralSettingsStore}, - permissions, - recording::{RecordingEvent, RecordingInputKind}, - recording_settings::RecordingTargetMode, - screenshot_editor::PendingScreenshotEditorInstances, - target_select_overlay::WindowFocusManager, - window_exclusion::WindowExclusion, -}; -use cap_recording::{feeds, sources::screen_capture::ScreenCaptureTarget}; - -#[cfg(target_os = "macos")] -const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition = LogicalPosition::new(12.0, 12.0); - -#[cfg(target_os = "macos")] -const MAIN_PANEL_LEVEL: i32 = 100; - -#[cfg(target_os = "macos")] -const TELEPROMPTER_PANEL_LEVEL: objc2_app_kit::NSWindowLevel = MAIN_PANEL_LEVEL as isize + 1; - -const DEFAULT_FALLBACK_DISPLAY_WIDTH: f64 = 1920.0; -const DEFAULT_FALLBACK_DISPLAY_HEIGHT: f64 = 1080.0; - -#[cfg(windows)] -const WINDOWS_WEBVIEW2_BROWSER_ARGS: &str = "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required --disable-vulkan --use-angle=d3d11"; - -#[cfg(windows)] -fn windows_webview2_browser_args() -> String { - let mut args = WINDOWS_WEBVIEW2_BROWSER_ARGS.to_string(); - if cap_rendering::force_software_wgpu_adapter() - || std::env::args_os().any(|arg| arg.to_str() == Some("--disable-gpu")) - { - args.push_str(" --disable-gpu"); - } - args -} - -#[cfg(target_os = "macos")] -fn is_system_dark_mode() -> bool { - use cocoa::base::{id, nil}; - use cocoa::foundation::NSString; - use objc::{class, msg_send, sel, sel_impl}; - - unsafe { - let app: id = msg_send![class!(NSApplication), sharedApplication]; - let appearance: id = msg_send![app, effectiveAppearance]; - if appearance == nil { - return false; - } - let name: id = msg_send![appearance, name]; - if name == nil { - return false; - } - let dark_appearance = NSString::alloc(nil).init_str("NSAppearanceNameDarkAqua"); - let vibrant_dark = NSString::alloc(nil).init_str("NSAppearanceNameVibrantDark"); - let is_dark: bool = msg_send![name, isEqualToString: dark_appearance]; - let is_vibrant_dark: bool = msg_send![name, isEqualToString: vibrant_dark]; - is_dark || is_vibrant_dark - } -} - -#[cfg(target_os = "windows")] -fn is_system_dark_mode() -> bool { - use winreg::RegKey; - use winreg::enums::HKEY_CURRENT_USER; - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(key) = - hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") - && let Ok(value) = key.get_value::("AppsUseLightTheme") - { - return value == 0; - } - false -} - -#[cfg(target_os = "linux")] -fn is_system_dark_mode() -> bool { - let output = std::process::Command::new("gsettings") - .args(["get", "org.gnome.desktop.interface", "color-scheme"]) - .output(); - if let Ok(output) = output - && output.status.success() - { - return String::from_utf8_lossy(&output.stdout).contains("dark"); - } - false -} - -pub fn hide_overlay(window: &WebviewWindow) { - let _ = window.set_ignore_cursor_events(true); - let _ = window.hide(); -} - -pub fn show_overlay(window: &WebviewWindow) { - let _ = window.set_ignore_cursor_events(false); - let _ = window.show(); -} - -fn emit_app_event(app: &AppHandle, event: E) -where - E: Event + serde::Serialize + Clone, -{ - let event_name = std::any::type_name::(); - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| event.emit(app))) { - Ok(Ok(())) => {} - Ok(Err(error)) => warn!(event = event_name, %error, "Failed to emit app event"), - Err(panic) => { - let message = crate::panic_payload_message(&panic); - error!(event = event_name, panic = %message, "Suppressed panic while emitting app event"); - } - } -} - -fn hide_recording_windows(app: &AppHandle, restore_target_select_overlays: bool) { - let focus_manager = app.try_state::(); - - for (label, window) in app.webview_windows() { - if let Ok(id) = CapWindowId::from_str(&label) - && matches!( - id, - CapWindowId::TargetSelectOverlay { .. } | CapWindowId::Main | CapWindowId::Camera - ) - { - if matches!(id, CapWindowId::TargetSelectOverlay { .. }) { - if restore_target_select_overlays - && window.is_visible().unwrap_or(false) - && let Some(focus_manager) = focus_manager.as_ref() - { - focus_manager.remember_overlay_for_restore(label); - } - hide_overlay(&window); - } else { - let _ = window.hide(); - } - } - } -} - -/// Release the live camera preview feed after `hide_recording_windows` when a -/// foreground window (Settings, an editor) takes over. Hiding the camera window -/// alone leaves the capture session running, so the OS camera-in-use indicator -/// stays lit while the user is in the editor. `restore_main_window_inputs` -/// re-attaches the feed when the main window comes back. -fn release_camera_preview_if_idle(app: &AppHandle) { - let is_recording = app - .try_state::>() - .and_then(|state| { - state - .try_read() - .ok() - .map(|state| state.is_recording_active_or_pending()) - }) - .unwrap_or(true); - - if is_recording { - return; - } - - let app = app.clone(); - tokio::spawn(async move { - if let Some(state) = app.try_state::>() { - let app_state = &mut *state.write().await; - app_state.camera_preview.pause(); - let _ = app_state.camera_feed.ask(feeds::camera::RemoveInput).await; - app_state.camera_in_use = false; - } else { - warn!("App state unavailable while pausing camera preview"); - } - }); -} - -fn bump_camera_window_session(app: &AppHandle) -> u64 { - app.state::>().fetch_add(1, Ordering::AcqRel) + 1 -} - -fn camera_window_label_for_session(session_id: u64) -> String { - format!("camera-{session_id}") -} - -fn is_camera_window_label(label: &str) -> bool { - label == "camera" - || label - .strip_prefix("camera-") - .is_some_and(|suffix| suffix.parse::().is_ok()) -} - -fn camera_window_rank(label: &str) -> u64 { - if label == "camera" { - return 0; - } - - label - .strip_prefix("camera-") - .and_then(|suffix| suffix.parse::().ok()) - .unwrap_or(0) -} - -#[cfg(target_os = "macos")] -fn camera_window_labels(app: &AppHandle) -> Vec { - app.webview_windows() - .into_keys() - .filter(|label| is_camera_window_label(label)) - .collect() -} - -fn camera_webview_window_entries(app: &AppHandle) -> Vec<(String, WebviewWindow)> { - app.webview_windows() - .into_iter() - .filter(|(label, _)| is_camera_window_label(label)) - .collect() -} - -fn camera_webview_windows(app: &AppHandle) -> Vec { - camera_webview_window_entries(app) - .into_iter() - .map(|(_, window)| window) - .collect() -} - -fn current_camera_window(app: &AppHandle) -> Option { - #[cfg(target_os = "macos")] - { - camera_webview_window_entries(app) - .into_iter() - .filter(|(_, window)| is_window_handle_valid(window)) - .max_by_key(|(label, _)| camera_window_rank(label)) - .map(|(_, window)| window) - } - - #[cfg(not(target_os = "macos"))] - { - camera_webview_window_entries(app) - .into_iter() - .max_by_key(|(label, _)| camera_window_rank(label)) - .map(|(_, window)| window) - } -} - -fn destroy_camera_window_handle( - app: &AppHandle, - window: WebviewWindow, -) -> tokio::sync::oneshot::Receiver<()> { - let (destroy_tx, destroy_rx) = tokio::sync::oneshot::channel(); - let _ = window.as_ref().close(); - app.run_on_main_thread({ - let window = window.clone(); - move || { - let _ = window.destroy(); - let _ = destroy_tx.send(()); - } - }) - .ok(); - destroy_rx -} - -async fn init_native_camera_preview( - app_state: &mut App, - window: WebviewWindow, -) -> Result<(), String> { - let camera_feed = app_state.camera_feed.clone(); - let init_result = app_state - .camera_preview - .init_window(window, camera_feed.clone()) - .await; - - match init_result { - Ok(()) => { - #[allow(deprecated)] - let camera_ws_sender = app_state.camera_ws_sender.clone(); - #[allow(deprecated)] - if let Err(err) = camera_feed - .ask(feeds::camera::RemoveSender(camera_ws_sender)) - .await - { - warn!(error = %err, "Failed to remove legacy camera preview sender"); - } - Ok(()) - } - Err(err) => { - #[allow(deprecated)] - let camera_ws_sender = app_state.camera_ws_sender.clone(); - #[allow(deprecated)] - if let Err(add_err) = camera_feed - .ask(feeds::camera::AddSender(camera_ws_sender)) - .await - { - warn!(error = %add_err, "Failed to restore legacy camera preview sender"); - } - Err(err.to_string()) - } - } -} - -pub(crate) async fn ensure_camera_input_active(app_state: &mut App) { - if let Some(id) = app_state.selected_camera_id.clone() - && !app_state.camera_in_use - { - let settings = crate::recording_settings::RecordingSettingsStore::camera_settings_for( - &app_state.handle, - &id, - ); - match app_state - .camera_feed - .ask(feeds::camera::SetInput { id, settings }) - .await - { - Ok(ready_future) => { - if let Err(err) = ready_future.await { - error!("Camera failed to initialize: {err}"); - return; - } - } - Err(err) => { - error!("Failed to send SetInput to camera feed: {err}"); - return; - } - } - - app_state.camera_in_use = true; - app_state.camera_cleanup_done = false; - } -} - -pub(crate) async fn restore_main_window_inputs(app: &AppHandle) { - let Some(state) = app.try_state::>() else { - warn!("App state unavailable while restoring main window inputs"); - return; - }; - - let should_restore = state - .try_read() - .map(|state| !state.is_recording_active_or_pending()) - .unwrap_or(false); - - if !should_restore { - return; - } - - let settings = crate::recording_settings::RecordingSettingsStore::get(app) - .ok() - .flatten() - .unwrap_or_default(); - let stored_camera_id = settings.camera_id.clone(); - - if let Err(err) = crate::set_mic_input(state.clone(), settings.mic_name).await { - warn!("Failed to restore microphone input for main window: {err}"); - } - - let Some(operation_lock) = app.try_state::() else { - warn!("CameraWindowOperationLock unavailable while restoring main window inputs"); - return; - }; - let operation_guard = operation_lock.lock().await; - - let camera_to_restore = state - .try_read() - .map(|s| { - if !s.camera_cleanup_done && !s.camera_in_use { - s.selected_camera_id - .clone() - .or_else(|| stored_camera_id.clone()) - } else { - None - } - }) - .unwrap_or(None) - // A remembered camera that isn't connected must not run the init/retry - // loop below: it would flash the preview window and toast an error on - // every main-window reveal while the device is away. - .filter(crate::is_camera_available); - - if let Some(camera_id) = camera_to_restore { - emit_camera_preview_clear(app); - let settings = - crate::recording_settings::RecordingSettingsStore::camera_settings_for(app, &camera_id); - - let (camera_feed, camera_ws_sender, native_sender) = { - let app_state = &mut *state.write().await; - app_state.selected_camera_id = Some(camera_id.clone()); - app_state.camera_in_use = true; - app_state.camera_cleanup_done = false; - #[allow(deprecated)] - ( - app_state.camera_feed.clone(), - app_state.camera_ws_sender.clone(), - app_state.camera_preview.sender(), - ) - }; - - if let Some(sender) = native_sender { - #[allow(deprecated)] - let _ = camera_feed - .ask(feeds::camera::RemoveSender(camera_ws_sender)) - .await; - if let Err(err) = sender.attach(&camera_feed).await { - warn!(error = %err, "Failed to add native preview camera sender"); - } - } else { - #[allow(deprecated)] - let _ = camera_feed - .ask(feeds::camera::AddSender(camera_ws_sender)) - .await; - } - - let mut showed_camera_window = false; - let mut attempts = 0; - let init_result: Result<(), String> = loop { - attempts += 1; - let request = camera_feed - .ask(feeds::camera::SetInput { - id: camera_id.clone(), - settings, - }) - .await - .map_err(|e| e.to_string()); - - if !showed_camera_window { - showed_camera_window = true; - crate::show_camera_window_unlocked(app); - } - - match request { - Ok(future) => match future.await { - Ok(_) => { - emit_camera_preview_clear(app); - break Ok(()); - } - Err(e) => { - if attempts == 1 { - emit_camera_preview_error( - app, - camera_preview_error_message(&e.to_string()), - ); - } - if attempts >= 3 { - break Err(format!( - "Failed to restore camera after {attempts} attempts: {e}" - )); - } - warn!("Camera restore attempt {attempts} failed: {e}. Retrying..."); - tokio::time::sleep(Duration::from_millis(500)).await; - } - }, - Err(e) => { - if attempts >= 3 { - break Err(e); - } - warn!("Camera restore attempt {attempts} failed: {e}. Retrying..."); - tokio::time::sleep(Duration::from_millis(500)).await; - } - } - }; - - drop(operation_guard); - - match init_result { - Ok(()) => crate::restore_camera_window(app), - Err(error) => { - let message = camera_preview_error_message(&error); - warn!("Failed to restore camera input for main window: {error}"); - let _ = camera_feed.ask(feeds::camera::RemoveInput).await; - let emit_input_lost = { - let app_state = &mut *state.write().await; - app_state.selected_camera_id = None; - app_state.camera_in_use = false; - app_state - .disconnected_inputs - .insert(RecordingInputKind::Camera) - }; - crate::show_camera_window_unlocked(app); - if emit_input_lost { - let _ = RecordingEvent::InputLost { - input: RecordingInputKind::Camera, - } - .emit(app); - } - emit_camera_preview_error(app, message.clone()); - let _ = NewNotification { - title: "Camera unavailable".to_string(), - body: message, - is_error: true, - } - .emit(app); - } - } - } -} - -pub(crate) async fn cleanup_camera_window( - app: &AppHandle, - window: Option<&WebviewWindow>, - #[allow(unused_variables)] reset_panel: bool, - wait_for_removal: bool, -) -> bool { - use crate::CameraWindowCloseGate; - - #[cfg(target_os = "macos")] - if reset_panel { - let panel_manager = app.state::(); - panel_manager.force_reset(PanelWindowType::Camera).await; - } - - app.state::().set_allow_close(true); - - #[cfg(target_os = "macos")] - { - let panel_labels = window - .map(|window| vec![window.label().to_string()]) - .unwrap_or_else(|| camera_window_labels(app)); - let (panel_close_tx, panel_close_rx) = tokio::sync::oneshot::channel(); - let app_for_close = app.clone(); - app.run_on_main_thread(move || { - use tauri_nspanel::ManagerExt; - for label in panel_labels { - if let Ok(panel) = app_for_close.get_webview_panel(&label) { - panel.released_when_closed(false); - panel.close(); - } - } - let _ = panel_close_tx.send(()); - }) - .ok(); - let _ = tokio::time::timeout(std::time::Duration::from_millis(500), panel_close_rx).await; - } - - let windows = window - .cloned() - .map(|window| vec![window]) - .unwrap_or_else(|| camera_webview_windows(app)); - for window in windows { - let destroy_rx = destroy_camera_window_handle(app, window); - let _ = tokio::time::timeout(std::time::Duration::from_millis(500), destroy_rx).await; - } - - if wait_for_removal { - let start = std::time::Instant::now(); - let timeout = std::time::Duration::from_millis(2000); - while start.elapsed() < timeout && !camera_webview_windows(app).is_empty() { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - } - - let still_exists = !camera_webview_windows(app).is_empty(); - app.state::().set_allow_close(false); - - !still_exists -} - -struct CursorMonitorInfo { - x: f64, - y: f64, - width: f64, - height: f64, - // On Windows each monitor's "logical" rect is its physical rect divided by - // its own scale, so logical rects of mixed-DPI monitors overlap and tao's - // LogicalPosition conversion (which uses whatever monitor the window - // currently occupies) can land a window on the wrong monitor. Positioning - // must go through this monitor's own scale, as a physical position. - #[cfg(windows)] - scale: f64, -} - -impl CursorMonitorInfo { - fn get() -> Self { - Self::from_display(&Display::get_containing_cursor().unwrap_or_else(Display::primary)) - } - - fn from_display(display: &Display) -> Self { - let bounds = display.raw_handle().logical_bounds(); - - #[cfg(windows)] - let scale = bounds - .as_ref() - .map(|b| b.size().width()) - .filter(|width| *width > 0.0) - .and_then(|logical_width| { - display - .physical_size() - .map(|physical| physical.width() / logical_width) - }) - .filter(|scale| scale.is_finite() && *scale > 0.0) - .unwrap_or(1.0); - - let (x, y, width, height) = bounds - .map(|b| { - ( - b.position().x(), - b.position().y(), - b.size().width(), - b.size().height(), - ) - }) - .unwrap_or(( - 0.0, - 0.0, - DEFAULT_FALLBACK_DISPLAY_WIDTH, - DEFAULT_FALLBACK_DISPLAY_HEIGHT, - )); - - Self { - x, - y, - width, - height, - #[cfg(windows)] - scale, - } - } - - /// Converts a global-logical point on this monitor into a `Position` that - /// lands exactly there regardless of which monitor the window currently - /// occupies. Logical on macOS/Linux (a true global space there), physical - /// on Windows. - fn position(&self, x: f64, y: f64) -> tauri::Position { - #[cfg(windows)] - return tauri::Position::Physical(tauri::PhysicalPosition::new( - (x * self.scale).round() as i32, - (y * self.scale).round() as i32, - )); - - #[cfg(not(windows))] - tauri::Position::Logical(tauri::LogicalPosition::new(x, y)) - } - - fn center_position(&self, window_width: f64, window_height: f64) -> (f64, f64) { - let pos_x = self.x + (self.width - window_width) / 2.0; - let pos_y = self.y + (self.height - window_height) / 2.0; - (pos_x, pos_y) - } - - fn bottom_center_position( - &self, - window_width: f64, - window_height: f64, - offset_y: f64, - ) -> (f64, f64) { - let pos_x = self.x + (self.width - window_width) / 2.0; - let pos_y = self.y + self.height - window_height - offset_y; - (pos_x, pos_y) - } - - fn from_window(window: &tauri::WebviewWindow) -> Self { - let Ok(window_pos) = window.outer_position() else { - return Self::get(); - }; - - // outer_position is physical. On Windows, resolve the display in - // physical space (per-monitor logical rects overlap in mixed-DPI - // layouts). On macOS, convert to logical points, a true global space. - // On Linux scap reports logical bounds in unscaled physical units, so - // the raw position compares directly. - #[cfg(windows)] - { - let (pos_x, pos_y) = (window_pos.x as f64, window_pos.y as f64); - for display in Display::list() { - if let Some(bounds) = display.raw_handle().physical_bounds() { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - return Self::from_display(&display); - } - } - } - - Self::get() - } - - #[cfg(target_os = "macos")] - { - let scale = window.scale_factor().unwrap_or(1.0); - let pos = window_pos.to_logical::(scale); - - for display in Display::list() { - if display_contains_logical(&display, pos.x, pos.y) { - return Self::from_display(&display); - } - } - - Self::get() - } - - #[cfg(target_os = "linux")] - { - let (pos_x, pos_y) = (window_pos.x as f64, window_pos.y as f64); - - for display in Display::list() { - if display_contains_logical(&display, pos_x, pos_y) { - return Self::from_display(&display); - } - } - - Self::get() - } - } -} - -fn display_contains_logical(display: &Display, pos_x: f64, pos_y: f64) -> bool { - display - .raw_handle() - .logical_bounds() - .map(|bounds| { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height - }) - .unwrap_or(false) -} - -fn display_containing_logical(pos_x: f64, pos_y: f64) -> Option { - Display::list() - .into_iter() - .find(|display| display_contains_logical(display, pos_x, pos_y)) -} - -/// Resolves the display a persisted window position belongs to, preferring the -/// display it was saved on. On Windows the saved logical coordinates are only -/// meaningful relative to that display (mixed-DPI logical rects overlap), so -/// restores must convert through its scale rather than the window's current one. -fn display_for_saved_position( - pos_x: f64, - pos_y: f64, - display_id: Option<&DisplayId>, -) -> Option { - display_id - .and_then(Display::from_id) - .filter(|display| display_contains_logical(display, pos_x, pos_y)) - .or_else(|| display_containing_logical(pos_x, pos_y)) -} - -/// Converts a global-logical point into a `Position` that lands exactly there, -/// resolving the owning display by containment when the caller doesn't know it. -/// Falls back to a plain logical position when no display contains the point. -pub fn logical_point_position(pos_x: f64, pos_y: f64) -> tauri::Position { - #[cfg(windows)] - if let Some(display) = display_containing_logical(pos_x, pos_y) { - return CursorMonitorInfo::from_display(&display).position(pos_x, pos_y); - } - - tauri::Position::Logical(tauri::LogicalPosition::new(pos_x, pos_y)) -} - -fn center_camera_window(app: &AppHandle, window: &WebviewWindow) { - let camera_state = match app.try_state::>() { - Some(state) => state - .try_read() - .ok() - .and_then(|guard| guard.camera_preview.get_state().ok()) - .unwrap_or_default(), - None => crate::camera::CameraPreviewState::default(), - }; - - let toolbar_height = 56.0; - let size = camera_state.size as f64; - let is_full = camera_state.shape == crate::camera::CameraPreviewShape::Full; - let aspect_ratio = crate::camera::WIDE_CAMERA_ASPECT_RATIO as f64; - - let window_width = if is_full { size * aspect_ratio } else { size }; - let window_height = size + toolbar_height; - - let monitor_info = CursorMonitorInfo::get(); - let (pos_x, pos_y) = monitor_info.center_position(window_width, window_height); - - let _ = window.set_size(tauri::LogicalSize::new(window_width, window_height)); - if let Some(guard) = app.try_state::() { - guard.ignore_for(1000); - } - let _ = window.set_position(monitor_info.position(pos_x, pos_y)); - - if let Some(state) = app.try_state::>() - && let Ok(guard) = state.try_read() - { - guard - .camera_preview - .notify_window_resized(window_width as u32, window_height as u32); - } -} - -fn is_position_on_display(display_id: &DisplayId, pos_x: f64, pos_y: f64) -> bool { - Display::from_id(display_id) - .and_then(|display| display.raw_handle().logical_bounds()) - .map(|bounds| { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height - }) - .unwrap_or(false) -} - -fn display_name_for_position(pos_x: f64, pos_y: f64) -> Option { - Display::list().into_iter().find_map(|display| { - let bounds = display.raw_handle().logical_bounds()?; - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - display.name().filter(|name| !name.trim().is_empty()) - } else { - None - } - }) -} - -fn is_position_on_monitor_name(monitor_name: &str, pos_x: f64, pos_y: f64) -> bool { - Display::list().into_iter().any(|display| { - if display.name().as_deref() != Some(monitor_name) { - return false; - } - - display - .raw_handle() - .logical_bounds() - .map(|bounds| { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height - }) - .unwrap_or(false) - }) -} - -fn is_position_on_any_screen(pos_x: f64, pos_y: f64) -> bool { - for display in Display::list() { - if let Some(bounds) = display.raw_handle().logical_bounds() { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - return true; - } - } - } - false -} - -// Recovers a window that ended up entirely off every connected display (e.g. the -// monitor it was on got disconnected), which otherwise leaves it open but unreachable. -fn recenter_window_if_offscreen(window: &WebviewWindow) { - let Ok(position) = window.outer_position() else { - return; - }; - let Ok(size) = window.outer_size() else { - return; - }; - let scale = window.scale_factor().unwrap_or(1.0); - - let on_screen = Display::list() - .iter() - .any(|display| display.intersects(position, size, scale)); - if on_screen { - return; - } - - let monitor = CursorMonitorInfo::get(); - let (pos_x, pos_y) = - monitor.center_position(size.width as f64 / scale, size.height as f64 / scale); - let _ = window.set_position(monitor.position(pos_x, pos_y)); -} - -fn ensure_settings_window_bounds(window: &WebviewWindow) { - const MIN_W: f64 = 780.0; - const MIN_H: f64 = 560.0; - let _ = window.set_min_size(Some(LogicalSize::new(MIN_W, MIN_H))); - if let (Ok(physical), Ok(scale)) = (window.inner_size(), window.scale_factor()) { - let width = physical.width as f64 / scale; - let height = physical.height as f64 / scale; - if width < MIN_W || height < MIN_H { - let _ = window.set_size(LogicalSize::new(width.max(MIN_W), height.max(MIN_H))); - } - } -} - -#[derive(Clone, Deserialize, Type)] -pub enum CapWindowId { - Main, - Settings, - Editor { id: u32 }, - RecordingsOverlay, - WindowCaptureOccluder { screen_id: DisplayId }, - TargetSelectOverlay { display_id: DisplayId }, - CaptureArea, - Camera, - RecordingControls, - Upgrade, - ModeSelect, - Debug, - ScreenshotEditor { id: u32 }, - Onboarding, - Teleprompter, -} - -impl FromStr for CapWindowId { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(match s { - "main" => Self::Main, - "settings" => Self::Settings, - s if is_camera_window_label(s) => Self::Camera, - "capture-area" => Self::CaptureArea, - // legacy identifier - "in-progress-recording" => Self::RecordingControls, - "recordings-overlay" => Self::RecordingsOverlay, - "upgrade" => Self::Upgrade, - "mode-select" => Self::ModeSelect, - "debug" => Self::Debug, - "onboarding" => Self::Onboarding, - "teleprompter" => Self::Teleprompter, - s if s.starts_with("editor-") => Self::Editor { - id: s - .replace("editor-", "") - .parse::() - .map_err(|e| e.to_string())?, - }, - s if s.starts_with("screenshot-editor-") => Self::ScreenshotEditor { - id: s - .replace("screenshot-editor-", "") - .parse::() - .map_err(|e| e.to_string())?, - }, - s if s.starts_with("window-capture-occluder-") => Self::WindowCaptureOccluder { - screen_id: s - .replace("window-capture-occluder-", "") - .parse::() - .map_err(|e| e.to_string())?, - }, - s if s.starts_with("target-select-overlay-") => Self::TargetSelectOverlay { - display_id: s - .replace("target-select-overlay-", "") - .parse::() - .map_err(|e| e.to_string())?, - }, - _ => return Err(format!("unknown window label: {s}")), - }) - } -} - -impl std::fmt::Display for CapWindowId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Main => write!(f, "main"), - Self::Settings => write!(f, "settings"), - Self::Camera => write!(f, "camera"), - Self::WindowCaptureOccluder { screen_id } => { - write!(f, "window-capture-occluder-{screen_id}") - } - Self::CaptureArea => write!(f, "capture-area"), - Self::TargetSelectOverlay { display_id } => { - write!(f, "target-select-overlay-{display_id}") - } - Self::RecordingControls => write!(f, "in-progress-recording"), // legacy identifier - Self::RecordingsOverlay => write!(f, "recordings-overlay"), - Self::Upgrade => write!(f, "upgrade"), - Self::ModeSelect => write!(f, "mode-select"), - Self::Editor { id } => write!(f, "editor-{id}"), - Self::Debug => write!(f, "debug"), - Self::ScreenshotEditor { id } => write!(f, "screenshot-editor-{id}"), - Self::Onboarding => write!(f, "onboarding"), - Self::Teleprompter => write!(f, "teleprompter"), - } - } -} - -impl CapWindowId { - pub fn label(&self) -> String { - self.to_string() - } - - pub fn title(&self) -> String { - match self { - Self::Settings => "Cap Settings".to_string(), - Self::WindowCaptureOccluder { .. } => "Cap Window Capture Occluder".to_string(), - Self::CaptureArea => "Cap Capture Area".to_string(), - Self::RecordingControls => "Cap Recording Controls".to_string(), - Self::Editor { .. } => "Cap Editor".to_string(), - Self::ScreenshotEditor { .. } => "Cap Screenshot Editor".to_string(), - Self::ModeSelect => "Cap Mode Selection".to_string(), - Self::Onboarding => "Welcome to Cap".to_string(), - Self::Camera => "Cap Camera".to_string(), - Self::RecordingsOverlay => "Cap Recordings Overlay".to_string(), - Self::TargetSelectOverlay { .. } => "Cap Target Select".to_string(), - Self::Teleprompter => "Cap Teleprompter".to_string(), - _ => "Cap".to_string(), - } - } - - #[cfg(target_os = "macos")] - pub fn activates_dock(&self) -> bool { - matches!( - self, - Self::Main - | Self::Editor { .. } - | Self::ScreenshotEditor { .. } - | Self::Settings - | Self::Upgrade - | Self::ModeSelect - | Self::Onboarding - ) - } - - pub fn is_transparent(&self) -> bool { - if matches!(self, Self::Settings) { - return cfg!(target_os = "macos"); - } - - matches!( - self, - Self::Main - | Self::Onboarding - | Self::Camera - | Self::WindowCaptureOccluder { .. } - | Self::CaptureArea - | Self::RecordingControls - | Self::RecordingsOverlay - | Self::TargetSelectOverlay { .. } - ) - } - - pub fn get(&self, app: &AppHandle) -> Option { - if matches!(self, Self::Camera) { - return current_camera_window(app); - } - - let label = self.label(); - app.get_webview_window(&label) - } - - #[cfg(target_os = "macos")] - pub fn traffic_lights_position(&self) -> Option>> { - match self { - Self::Editor { .. } | Self::ScreenshotEditor { .. } => { - Some(Some(LogicalPosition::new(20.0, 32.0))) - } - Self::Camera - | Self::Main - | Self::Onboarding - | Self::WindowCaptureOccluder { .. } - | Self::CaptureArea - | Self::RecordingsOverlay - | Self::RecordingControls - | Self::TargetSelectOverlay { .. } => None, - Self::Settings => Some(Some(LogicalPosition::new(22.0, 22.0))), - Self::Teleprompter => Some(Some(LogicalPosition::new(14.0, 14.0))), - _ => Some(None), - } - } - - pub fn min_size(&self) -> Option<(f64, f64)> { - Some(match self { - Self::Main => (330.0, 395.0), - Self::Editor { .. } => (1275.0, 800.0), - Self::ScreenshotEditor { .. } => (800.0, 600.0), - Self::Settings => (780.0, 560.0), - Self::Camera => (200.0, 200.0), - Self::Upgrade => (950.0, 850.0), - Self::ModeSelect => (580.0, 340.0), - Self::Onboarding => (860.0, 690.0), - _ => return None, - }) - } -} - -#[derive(Debug, Clone, Type, Deserialize)] -pub enum ShowCapWindow { - Main { - init_target_mode: Option, - }, - Settings { - page: Option, - }, - Editor { - project_path: PathBuf, - }, - RecordingsOverlay, - WindowCaptureOccluder { - screen_id: DisplayId, - }, - TargetSelectOverlay { - display_id: DisplayId, - target_mode: Option, - }, - CaptureArea { - screen_id: DisplayId, - }, - Camera { - centered: bool, - }, - InProgressRecording { - countdown: Option, - #[serde(default)] - capture_target: Option, - }, - Upgrade, - ModeSelect, - ScreenshotEditor { - path: PathBuf, - }, - Onboarding, -} - -impl ShowCapWindow { - pub async fn show(&self, app: &AppHandle) -> tauri::Result { - if let Self::Editor { project_path } = &self { - let state = app.state::(); - let window_id = { - let mut s = state.ids.lock().unwrap(); - if !s.iter().any(|(path, _)| path == project_path) { - let id = state - .counter - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - s.push((project_path.clone(), id)); - id - } else { - s.iter().find(|(path, _)| path == project_path).unwrap().1 - } - }; - - let window_label = CapWindowId::Editor { id: window_id }.label(); - PendingEditorInstances::start_prewarm(app, window_label, project_path.clone()).await; - } - - if let Self::ScreenshotEditor { path } = &self { - let state = app.state::(); - { - let mut s = state.ids.lock().unwrap(); - if !s.iter().any(|(p, _)| p == path) { - let id = state - .counter - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - s.push((path.clone(), id)); - } - } - } - - let camera_window_label = if matches!(self, Self::Camera { .. }) { - Some(camera_window_label_for_session(bump_camera_window_session( - app, - ))) - } else { - None - }; - - if let Self::Camera { centered } = self { - #[cfg(target_os = "macos")] - { - let panel_manager = app.state::(); - let mut panel_state = panel_manager.get_state(PanelWindowType::Camera).await; - - if panel_state == PanelState::Destroying { - debug!("Camera window is being destroyed, waiting..."); - let wait_result = panel_manager - .wait_for_state( - PanelWindowType::Camera, - &[PanelState::None], - std::time::Duration::from_millis(500), - ) - .await; - - if !wait_result { - warn!("Camera destroy wait timed out, force resetting state"); - panel_manager.force_reset(PanelWindowType::Camera).await; - } - panel_state = panel_manager.get_state(PanelWindowType::Camera).await; - } - - if panel_state == PanelState::Creating { - debug!("Camera window is being created, waiting..."); - panel_manager - .wait_for_state( - PanelWindowType::Camera, - &[PanelState::Ready], - std::time::Duration::from_millis(500), - ) - .await; - } - } - - if let Some(window) = self.id(app).get(app) { - #[cfg(target_os = "macos")] - { - use crate::panel_manager::is_window_handle_valid; - - let handle_valid = is_window_handle_valid(&window); - - if !handle_valid { - warn!( - "Camera window exists but handle is invalid, destroying and recreating..." - ); - let cleanup_success = - cleanup_camera_window(app, Some(&window), true, true).await; - if !cleanup_success { - warn!( - "Camera window still in registry after cleanup attempts, will retry later" - ); - return Err(tauri::Error::WindowNotFound); - } - debug!("Camera window successfully removed from registry"); - } else { - let panel_manager = app.state::(); - let mut panel_state = - panel_manager.get_state(PanelWindowType::Camera).await; - - if panel_state == PanelState::Creating { - debug!( - "Camera window valid but state is Creating, waiting for completion" - ); - panel_manager - .wait_for_state( - PanelWindowType::Camera, - &[PanelState::Ready, PanelState::None], - std::time::Duration::from_millis(1000), - ) - .await; - panel_state = panel_manager.get_state(PanelWindowType::Camera).await; - } - - if panel_state != PanelState::Ready { - debug!( - "Camera window exists but panel state is {:?}, updating to Ready", - panel_state - ); - panel_manager.force_reset(PanelWindowType::Camera).await; - panel_manager.mark_ready(PanelWindowType::Camera, 0).await; - } - - let Some(state) = app.try_state::>() else { - warn!("App state unavailable while showing camera window"); - return Err(tauri::Error::WindowNotFound); - }; - let mut app_state = state.write().await; - - let enable_native_camera_preview = - GeneralSettingsStore::native_camera_preview_enabled(app); - - let shutdown_preview = if !enable_native_camera_preview { - app_state.camera_preview.begin_shutdown() - } else { - None - }; - - ensure_camera_input_active(&mut app_state).await; - - if enable_native_camera_preview - && let Err(err) = - init_native_camera_preview(&mut app_state, window.clone()).await - { - error!( - "Error reinitializing camera preview for existing window: {err}" - ); - } - - drop(app_state); - - if let Some(rx) = shutdown_preview { - let _ = tokio::time::timeout(Duration::from_millis(500), rx).await; - } - - let (show_tx, show_rx) = tokio::sync::oneshot::channel(); - app.run_on_main_thread({ - let window = window.clone(); - move || { - use crate::panel_manager::try_to_panel; - - // IMPORTANT: We intentionally use window.show() + set_focus() here - // instead of panel.order_front_regardless(). - // - // order_front_regardless() was found to cause a crash after ~4-5 - // camera toggle cycles due to macOS internal state accumulation. - // The crash manifested as a hard crash in the Metal/CAMetalLayer - // subsystem, not in our Rust code. - // - // Using standard Tauri window APIs avoids this macOS-specific issue - // while still properly showing and focusing the camera preview window. - let _ = window.show(); - let _ = window.set_focus(); - let _ = show_tx.send(true); - } - }) - .ok(); - - let show_result = show_rx.await.unwrap_or(false); - - if show_result { - if *centered { - center_camera_window(app, &window); - } - return Ok(window); - } else { - warn!("Camera panel show failed, will recreate window"); - let cleanup_success = - cleanup_camera_window(app, Some(&window), true, true).await; - if !cleanup_success { - warn!( - "Camera window still in registry after show failure, will retry later" - ); - return Err(tauri::Error::WindowNotFound); - } - debug!("Camera window successfully removed after show failure"); - } - } - } - - #[cfg(not(target_os = "macos"))] - { - let Some(state) = app.try_state::>() else { - warn!("App state unavailable while showing camera window"); - return Err(tauri::Error::WindowNotFound); - }; - let mut app_state = state.write().await; - - let enable_native_camera_preview = - GeneralSettingsStore::native_camera_preview_enabled(app); - - let shutdown_preview = if !enable_native_camera_preview { - app_state.camera_preview.begin_shutdown() - } else { - None - }; - - ensure_camera_input_active(&mut app_state).await; - - if enable_native_camera_preview - && let Err(err) = - init_native_camera_preview(&mut app_state, window.clone()).await - { - error!("Error reinitializing camera preview for existing window: {err}"); - } - - drop(app_state); - - if let Some(rx) = shutdown_preview { - let _ = tokio::time::timeout(Duration::from_millis(500), rx).await; - } - - if *centered { - center_camera_window(app, &window); - } - window.show().ok(); - window.set_focus().ok(); - return Ok(window); - } - } - } - - if matches!(self, Self::Settings { .. }) { - hide_recording_windows(app, true); - release_camera_preview_if_idle(app); - } - - #[cfg(target_os = "macos")] - if let Self::InProgressRecording { capture_target, .. } = self - && let Some(window) = self.id(app).get(app) - { - use crate::panel_manager::is_window_handle_valid; - - if is_window_handle_valid(&window) { - debug!("InProgressRecording: reusing existing window"); - let width = 320.0; - let height = 150.0; - let (pos_x, pos_y) = capture_target - .as_ref() - .and_then(fake_window::calculate_recording_controls_position_for_target) - .unwrap_or_else(|| { - CursorMonitorInfo::get().bottom_center_position(width, height, 120.0) - }); - let _ = window.set_position(tauri::LogicalPosition::new(pos_x, pos_y)); - - let label = window.label().to_string(); - app.run_on_main_thread({ - let app = app.clone(); - move || { - use tauri_nspanel::ManagerExt; - if let Ok(panel) = app.get_webview_panel(&label) { - panel.order_front_regardless(); - panel.show(); - } - } - }) - .ok(); - fake_window::spawn_fake_window_listener(app.clone(), window.clone()); - return Ok(window); - } else { - warn!("InProgressRecording window handle invalid, destroying and recreating..."); - let _ = window.destroy(); - - let window_id = self.id(app); - let max_wait = std::time::Duration::from_millis(500); - let poll_interval = std::time::Duration::from_millis(25); - let start = std::time::Instant::now(); - while start.elapsed() < max_wait { - if window_id.get(app).is_none() { - debug!( - "InProgressRecording window removed from registry after {:?}", - start.elapsed() - ); - break; - } - tokio::time::sleep(poll_interval).await; - } - - if window_id.get(app).is_some() { - error!("InProgressRecording window STILL in registry, cannot recreate"); - return Err(tauri::Error::WindowNotFound); - } - debug!("InProgressRecording window cleaned up, will recreate"); - } - } - - #[cfg(not(target_os = "macos"))] - if let Self::InProgressRecording { capture_target, .. } = self - && let Some(window) = self.id(app).get(app) - { - let width = 320.0; - let height = 150.0; - let (pos_x, pos_y) = capture_target - .as_ref() - .and_then(fake_window::calculate_recording_controls_position_for_target) - .unwrap_or_else(|| { - CursorMonitorInfo::get().bottom_center_position(width, height, 120.0) - }); - let _ = window.set_position(logical_point_position(pos_x, pos_y)); - window.show().ok(); - window.set_focus().ok(); - fake_window::spawn_fake_window_listener(app.clone(), window.clone()); - return Ok(window); - } - - if !matches!(self, Self::Camera { .. } | Self::InProgressRecording { .. }) - && let Some(window) = self.id(app).get(app) - { - if matches!(self, Self::Main { .. }) && crate::should_show_onboarding(app) { - return Box::pin(Self::Onboarding.show(app)).await; - } - - #[cfg(target_os = "macos")] - if matches!(self, Self::Main { .. }) && !app.state::().is_ready() - { - return Ok(window); - } - - let cursor_display_id = if let Self::Main { init_target_mode } = self { - if init_target_mode.is_some() { - Display::get_containing_cursor() - .map(|d| d.id().to_string()) - .or_else(|| Some(Display::primary().id().to_string())) - } else { - None - } - } else { - None - }; - - if let Self::Main { - init_target_mode: Some(target_mode), - } = self - { - window.hide().ok(); - emit_app_event( - app, - RequestSetTargetMode { - target_mode: Some(*target_mode), - display_id: cursor_display_id, - }, - ); - } else { - let should_restore_main_window_inputs = matches!(self, Self::Main { .. }); - - if let Self::Onboarding = self { - let _ = window.set_ignore_cursor_events(false); - } - - if matches!(self, Self::Main { .. } | Self::Settings { .. }) { - recenter_window_if_offscreen(&window); - } - - window.show().ok(); - window.unminimize().ok(); - window.set_focus().ok(); - - if let Self::Settings { .. } = self { - ensure_settings_window_bounds(&window); - } - - if let Self::Main { init_target_mode } = self { - emit_app_event( - app, - RequestSetTargetMode { - target_mode: *init_target_mode, - display_id: cursor_display_id, - }, - ); - } - - if should_restore_main_window_inputs { - let app = app.clone(); - tokio::spawn(async move { - restore_main_window_inputs(&app).await; - }); - } - } - - #[cfg(target_os = "macos")] - if self.id(app).activates_dock() { - crate::permissions::sync_macos_dock_visibility(app); - } - - return Ok(window); - } - - let _id = self.id(app); - let cursor_monitor = CursorMonitorInfo::get(); - - let window = match self { - Self::Main { init_target_mode } => { - if !permissions::do_permissions_check(false).necessary_granted() { - return Box::pin(Self::Onboarding.show(app)).await; - } - - let title = CapWindowId::Main.title(); - let should_protect = should_protect_window(app, &title); - - #[cfg(target_os = "macos")] - let panel_activation_guard = permissions::prepare_macos_panel_window(app); - - let window = self - .window_builder(app, "/") - .resizable(false) - .maximized(false) - .maximizable(false) - .minimizable(false) - .always_on_top(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .transparent(true) - .visible(false) - .initialization_script(format!( - " - window.__CAP__ = window.__CAP__ ?? {{}}; - window.__CAP__.initialTargetMode = {} - ", - serde_json::to_string(init_target_mode) - .expect("Failed to serialize initial target mode") - )) - .build()?; - lock_window_text_scale(&window); - - let saved_position = GeneralSettingsStore::get(app) - .ok() - .flatten() - .and_then(|s| s.main_window_position) - .filter(|pos| is_position_on_any_screen(pos.x, pos.y)); - - let main_position = if let Some(pos) = saved_position { - match display_for_saved_position(pos.x, pos.y, pos.display_id.as_ref()) { - Some(display) => { - CursorMonitorInfo::from_display(&display).position(pos.x, pos.y) - } - None => tauri::Position::Logical(tauri::LogicalPosition::new(pos.x, pos.y)), - } - } else { - let (pos_x, pos_y) = cursor_monitor.center_position(330.0, 395.0); - cursor_monitor.position(pos_x, pos_y) - }; - - #[cfg(target_os = "macos")] - { - app.run_on_main_thread({ - let window = window.clone(); - let app = app.clone(); - let panel_activation_guard = panel_activation_guard; - move || { - let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use crate::panel_manager::try_to_panel; - - let delegate = panel_delegate!(MainPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match try_to_panel(&window) { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert main window to panel: {}", e); - crate::permissions::sync_macos_dock_visibility(&app); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - panel.set_delegate(delegate); - - panel.set_level(MAIN_PANEL_LEVEL); - - let _ = window.set_position(main_position); - - crate::platform::apply_squircle_corners(&window, 16.0); - - crate::permissions::schedule_macos_dock_visibility_sync(&app); - } - }) - .ok(); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = window.set_position(main_position); - - #[cfg(windows)] - { - if let Err(e) = window.set_size(LogicalSize::new(330.0, 395.0)) { - warn!("Failed to set Main window size on Windows: {}", e); - } - if let Err(e) = window.set_position(main_position) { - warn!("Failed to position Main window on Windows: {}", e); - } - } - - window.show().ok(); - } - - window - } - Self::TargetSelectOverlay { - display_id, - target_mode, - } => { - let Some(display) = scap_targets::Display::from_id(display_id) else { - return Err(tauri::Error::WindowNotFound); - }; - let is_hovered_display = scap_targets::Display::get_containing_cursor() - .map(|d| d.id()) - == Some(display.id()); - - let title = CapWindowId::TargetSelectOverlay { - display_id: display_id.clone(), - } - .title(); - let should_protect = should_protect_window(app, &title); - - let target_mode_param = match target_mode { - Some(RecordingTargetMode::Display) => "&targetMode=display", - Some(RecordingTargetMode::Window) => "&targetMode=window", - Some(RecordingTargetMode::Area) => "&targetMode=area", - Some(RecordingTargetMode::Camera) => "&targetMode=camera", - None => "", - }; - - let camera_ws_port = { - let Some(state) = app.try_state::>() else { - warn!("App state unavailable during target select overlay creation"); - return Err(tauri::Error::WindowNotFound); - }; - let state = state.read().await; - state.camera_ws_port - }; - - #[cfg(target_os = "macos")] - let panel_activation_guard = permissions::prepare_macos_panel_window(app); - - let mut window_builder = self - .window_builder( - app, - format!("/target-select-overlay?displayId={display_id}&isHoveredDisplay={is_hovered_display}{target_mode_param}"), - ) - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .content_protected(should_protect) - .always_on_top(true) - .visible_on_all_workspaces(true) - .skip_taskbar(true) - .transparent(true) - .visible(false) - .initialization_script(format!( - "window.__CAP__ = window.__CAP__ ?? {{}}; window.__CAP__.cameraWsPort = {camera_ws_port};" - )); - - #[cfg(target_os = "macos")] - { - let position = display.raw_handle().logical_position(); - let size = display.logical_size().unwrap(); - - window_builder = window_builder - .inner_size(size.width(), size.height()) - .position(position.x(), position.y()); - } - - #[cfg(windows)] - { - window_builder = window_builder.inner_size(100.0, 100.0).position(0.0, 0.0); - } - - #[cfg(target_os = "linux")] - { - let position = display.raw_handle().physical_position().unwrap(); - let size = display.physical_size().unwrap(); - window_builder = window_builder - .inner_size(size.width(), size.height()) - .position(position.x(), position.y()); - } - - let window = window_builder.build()?; - lock_window_text_scale(&window); - - #[cfg(target_os = "linux")] - { - use tauri::{LogicalSize, PhysicalPosition}; - let position = display.raw_handle().physical_position().unwrap(); - let size = display.physical_size().unwrap(); - let _ = window.set_position(PhysicalPosition::new(position.x(), position.y())); - let _ = window.set_size(LogicalSize::new(size.width(), size.height())); - } - - #[cfg(windows)] - { - let Some(position) = display.raw_handle().physical_position() else { - warn!(display_id = %display_id, "Missing display position for target select overlay"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(logical_size) = display.logical_size() else { - warn!(display_id = %display_id, "Missing display logical size for target select overlay"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(physical_size) = display.physical_size() else { - warn!(display_id = %display_id, "Missing display physical size for target select overlay"); - return Err(tauri::Error::WindowNotFound); - }; - use tauri::{LogicalSize, PhysicalPosition, PhysicalSize}; - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - let _ = window.set_position(PhysicalPosition::new(position.x(), position.y())); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - match window.inner_size() { - Ok(actual_physical_size) - if physical_size.width() != actual_physical_size.width as f64 => - { - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - } - Ok(_) => {} - Err(err) => { - warn!(%err, "Failed to read target select overlay inner size"); - } - } - } - - app.state::() - .spawn(display_id, window.clone()); - - #[cfg(target_os = "macos")] - { - app.run_on_main_thread({ - let window = window.clone(); - let app = app.clone(); - let panel_activation_guard = panel_activation_guard; - move || { - let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; - - #[link(name = "CoreGraphics", kind = "framework")] - unsafe extern "C" { - fn CGWindowLevelForKey(key: i32) -> i32; - } - - #[allow(non_upper_case_globals)] - const kCGMaximumWindowLevelKey: i32 = 10; - - let delegate = panel_delegate!(TargetSelectOverlayPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match window.to_panel() { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert target select overlay to panel: {:?}", e); - crate::permissions::sync_macos_dock_visibility(&app); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - panel.set_delegate(delegate); - - #[allow(non_upper_case_globals)] - const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; - panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); - - let max_level = unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; - panel.set_level(max_level - 1); - - panel.order_front_regardless(); - panel.show(); - - crate::permissions::schedule_macos_dock_visibility_sync(&app); - } - }) - .ok(); - } - - #[cfg(not(target_os = "macos"))] - { - window.show().ok(); - } - - window - } - Self::Settings { page } => { - let mut builder = self - .window_builder( - app, - format!("/settings/{}", page.clone().unwrap_or_default()), - ) - .inner_size(782.0, 775.0) - .min_inner_size(780.0, 560.0) - .resizable(true) - .maximized(false) - .focused(true); - - #[cfg(target_os = "macos")] - { - builder = builder.transparent(true); - } - - let window = builder.build()?; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(782.0, 775.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - if let Err(e) = window.set_size(LogicalSize::new(782.0, 775.0)) { - warn!("Failed to set Settings window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position Settings window on Windows: {}", e); - } - } - - ensure_settings_window_bounds(&window); - - window - } - Self::Editor { .. } => { - let open_started = std::time::Instant::now(); - hide_recording_windows(app, false); - release_camera_preview_if_idle(app); - - let window = match self - .window_builder(app, "/editor") - .maximizable(true) - .inner_size(1275.0, 800.0) - .min_inner_size(1275.0, 800.0) - .focused(true) - .build() - { - Ok(window) => window, - Err(error) => { - // Don't leave the prewarmed instance (decoders, frame - // websocket) orphaned if the window failed to appear. - let window_label = self.id(app).label(); - PendingEditorInstances::get(app) - .cancel_prewarm(&window_label) - .await; - return Err(error); - } - }; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(1275.0, 800.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(1275.0, 800.0)) { - warn!("Failed to set Editor window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position Editor window on Windows: {}", e); - } - } - - // Show immediately: the native background color is already - // themed, so the window can appear before the webview loads and - // the editor skeleton takes over. When window transparency is - // enabled we keep the old behaviour (the frontend reveals the - // window after applying the HudWindow effects) to avoid an - // opaque-to-transparent pop. - let transparency_enabled = GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|s| s.window_transparency) - .unwrap_or(false); - if !transparency_enabled { - window.show().ok(); - window.set_focus().ok(); - } - - info!( - window_built_and_shown_ms = open_started.elapsed().as_millis() as u64, - shown_from_rust = !transparency_enabled, - "Editor open: window ready" - ); - - window - } - Self::ScreenshotEditor { path } => { - hide_recording_windows(app, false); - release_camera_preview_if_idle(app); - - let window_label = self.id(app).label(); - let pending = PendingScreenshotEditorInstances::get(app); - PendingScreenshotEditorInstances::start_prewarm( - app, - window_label.clone(), - path.clone(), - ) - .await; - - let window = match self - .window_builder(app, "/screenshot-editor") - .maximizable(true) - .inner_size(1240.0, 800.0) - .min_inner_size(800.0, 600.0) - .focused(true) - .build() - { - Ok(window) => window, - Err(error) => { - pending.cancel_prewarm(&window_label).await; - return Err(error); - } - }; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(1240.0, 800.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(1240.0, 800.0)) { - warn!( - "Failed to set ScreenshotEditor window size on Windows: {}", - e - ); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!( - "Failed to position ScreenshotEditor window on Windows: {}", - e - ); - } - } - - window.show().ok(); - window.set_focus().ok(); - - window - } - Self::Upgrade => { - if let Some(main) = CapWindowId::Main.get(app) { - let _ = main.hide(); - } - - let window = self - .window_builder(app, "/upgrade") - .inner_size(950.0, 850.0) - .min_inner_size(950.0, 850.0) - .resizable(false) - .focused(true) - .always_on_top(true) - .maximized(false) - .shadow(true) - .build()?; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(950.0, 850.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(950.0, 850.0)) { - warn!("Failed to set Upgrade window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position Upgrade window on Windows: {}", e); - } - } - - window.show().ok(); - window.set_focus().ok(); - - window - } - Self::ModeSelect => { - if let Some(main) = CapWindowId::Main.get(app) { - let _ = main.hide(); - } - - let window = self - .window_builder(app, "/mode-select") - .inner_size(580.0, 340.0) - .min_inner_size(580.0, 340.0) - .resizable(false) - .maximized(false) - .maximizable(false) - .focused(true) - .shadow(true) - .build()?; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(580.0, 340.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(580.0, 340.0)) { - warn!("Failed to set ModeSelect window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position ModeSelect window on Windows: {}", e); - } - } - - window.show().ok(); - window.set_focus().ok(); - - window - } - Self::Onboarding => { - if let Some(main) = CapWindowId::Main.get(app) { - let _ = main.hide(); - } - - let width = (cursor_monitor.width * 0.58).clamp(860.0, 1080.0); - let height = (width * 0.72).clamp(690.0, 780.0); - - let window = self - .window_builder(app, "/onboarding") - .inner_size(width, height) - .min_inner_size(860.0, 690.0) - .resizable(false) - .maximized(false) - .maximizable(false) - .transparent(true) - .focused(true) - .shadow(true) - .build()?; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(width, height); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - let _ = window.set_ignore_cursor_events(false); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(width, height)) { - warn!("Failed to set Onboarding window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position Onboarding window on Windows: {}", e); - } - } - - window.show().ok(); - window.set_focus().ok(); - - window - } - Self::Camera { centered } => { - const DEFAULT_WINDOW_SIZE: f64 = 230.0 * 2.0; - const CENTERED_WINDOW_SIZE: f64 = 400.0; - - #[cfg(target_os = "macos")] - let create_guard = { - let panel_manager = app.state::(); - panel_manager - .try_begin_create(PanelWindowType::Camera) - .await - }; - - #[cfg(target_os = "macos")] - let Some(mut create_guard) = create_guard else { - let panel_manager = app.state::(); - let state = panel_manager.get_state(PanelWindowType::Camera).await; - warn!("Camera window creation blocked, current state: {:?}", state); - if state == PanelState::Ready - && let Some(window) = CapWindowId::Camera.get(app) - { - if *centered { - center_camera_window(app, &window); - } - return Ok(window); - } - panel_manager - .wait_for_state( - PanelWindowType::Camera, - &[PanelState::Ready, PanelState::None], - std::time::Duration::from_millis(500), - ) - .await; - if let Some(window) = CapWindowId::Camera.get(app) { - if *centered { - center_camera_window(app, &window); - } - return Ok(window); - } - return Err(tauri::Error::WindowNotFound); - }; - - let enable_native_camera_preview = - GeneralSettingsStore::native_camera_preview_enabled(app); - - { - let Some(state) = app.try_state::>() else { - warn!("App state unavailable while creating camera window"); - return Err(tauri::Error::WindowNotFound); - }; - let mut state = state.write().await; - - let shutdown_preview = - if !enable_native_camera_preview && state.camera_preview.is_initialized() { - state.camera_preview.begin_shutdown() - } else { - None - }; - - if enable_native_camera_preview && state.camera_preview.is_initialized() { - warn!("Detected existing camera preview, will reuse it"); - } - - // Camera protection is applied per recording mode in `start_recording`; - // protecting at creation hides the live preview on virtual/mirrored displays. - let should_protect = false; - - #[cfg(target_os = "macos")] - let panel_activation_guard = permissions::prepare_macos_panel_window(app); - - let label = camera_window_label - .clone() - .unwrap_or_else(|| CapWindowId::Camera.label()); - let mut window_builder = self - .window_builder_with_label(app, "/camera", label) - .maximized(false) - .resizable(false) - .shadow(false) - .fullscreen(false) - .always_on_top(true) - .visible_on_all_workspaces(true) - .skip_taskbar(true) - .initialization_script(format!( - " - window.__CAP__ = window.__CAP__ ?? {{}}; - window.__CAP__.cameraWsPort = {}; - window.__CAP__.cameraOnlyMode = {}; - window.__CAP__.enableNativeCameraPreview = {}; - ", - state.camera_ws_port, centered, enable_native_camera_preview - )) - .content_protected(should_protect) - .transparent(true) - .visible(false); - - let window = match window_builder.build() { - Ok(w) => w, - Err(e) => { - let is_label_exists = e.to_string().contains("already exists"); - if is_label_exists { - warn!( - "Camera webview label already exists, cleaning up for next attempt" - ); - cleanup_camera_window(app, None, false, false).await; - } - - #[cfg(target_os = "macos")] - { - let panel_manager = app.state::(); - panel_manager.force_reset(PanelWindowType::Camera).await; - } - return Err(e); - } - }; - lock_window_text_scale(&window); - - #[cfg(target_os = "windows")] - log_window_content_protection( - &window, - should_protect, - &CapWindowId::Camera.title(), - ); - - let camera_monitor = CapWindowId::Main - .get(app) - .map(|w| CursorMonitorInfo::from_window(&w)) - .unwrap_or(cursor_monitor); - - let preferred_monitor_name = display_name_for_position( - camera_monitor.x + camera_monitor.width / 2.0, - camera_monitor.y + camera_monitor.height / 2.0, - ); - - let saved_position = - GeneralSettingsStore::get(app) - .ok() - .flatten() - .and_then(|settings| { - if let Some(monitor_name) = preferred_monitor_name.as_deref() { - settings - .camera_window_positions_by_monitor_name - .get(monitor_name) - .cloned() - .filter(|pos| { - is_position_on_monitor_name(monitor_name, pos.x, pos.y) - }) - .or_else(|| { - settings.camera_window_position.filter(|pos| { - is_position_on_monitor_name( - monitor_name, - pos.x, - pos.y, - ) - }) - }) - } else { - settings.camera_window_position.filter(|pos| { - if let Some(display_id) = &pos.display_id { - is_position_on_display(display_id, pos.x, pos.y) - } else { - is_position_on_any_screen(pos.x, pos.y) - } - }) - } - }); - - let camera_position = if let Some(pos) = saved_position { - match display_for_saved_position(pos.x, pos.y, pos.display_id.as_ref()) { - Some(display) => { - CursorMonitorInfo::from_display(&display).position(pos.x, pos.y) - } - None => { - tauri::Position::Logical(tauri::LogicalPosition::new(pos.x, pos.y)) - } - } - } else if *centered { - let aspect_ratio = crate::camera::WIDE_CAMERA_ASPECT_RATIO as f64; - let toolbar_height = 56.0; - let window_width = CENTERED_WINDOW_SIZE * aspect_ratio; - let window_height = CENTERED_WINDOW_SIZE + toolbar_height; - let (camera_pos_x, camera_pos_y) = - camera_monitor.center_position(window_width, window_height); - camera_monitor.position(camera_pos_x, camera_pos_y) - } else { - let camera_pos_x = - camera_monitor.x + camera_monitor.width - DEFAULT_WINDOW_SIZE - 100.0; - let camera_pos_y = - camera_monitor.y + camera_monitor.height - DEFAULT_WINDOW_SIZE - 100.0; - camera_monitor.position(camera_pos_x, camera_pos_y) - }; - - #[cfg(not(target_os = "macos"))] - { - if let Some(guard) = app.try_state::() { - guard.ignore_for(1000); - } - let _ = window.set_position(camera_position); - } - - ensure_camera_input_active(&mut state).await; - - #[cfg(target_os = "macos")] - { - let panel_manager = app.state::(); - let operation_id = create_guard.operation_id; - - let (panel_tx, panel_rx) = tokio::sync::oneshot::channel(); - app.run_on_main_thread({ - let window = window.clone(); - let app = app.clone(); - let panel_activation_guard = panel_activation_guard; - move || { - let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use crate::panel_manager::try_to_panel; - - #[link(name = "CoreGraphics", kind = "framework")] - unsafe extern "C" { - fn CGWindowLevelForKey(key: i32) -> i32; - } - - #[allow(non_upper_case_globals)] - const kCGMaximumWindowLevelKey: i32 = 10; - - let delegate = panel_delegate!(CameraPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match try_to_panel(&window) { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert camera to panel: {}", e); - crate::permissions::sync_macos_dock_visibility(&app); - let _ = panel_tx.send(false); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - panel.set_delegate(delegate); - - let max_level = - unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; - panel.set_level(max_level); - - if let Some(guard) = app.try_state::() { - guard.ignore_for(1000); - } - let _ = window.set_position(camera_position); - - panel.order_front_regardless(); - panel.show(); - crate::permissions::schedule_macos_dock_visibility_sync(&app); - let _ = panel_tx.send(true); - } - }) - .ok(); - - if panel_rx.await.unwrap_or(false) { - panel_manager - .mark_ready(PanelWindowType::Camera, operation_id) - .await; - create_guard.mark_completed(); - } else { - warn!("Camera panel creation failed"); - panel_manager.force_reset(PanelWindowType::Camera).await; - } - } - - if enable_native_camera_preview - && let Err(err) = - init_native_camera_preview(&mut state, window.clone()).await - { - error!( - "Error initializing camera preview, falling back to WebSocket preview: {err}" - ); - } - - #[cfg(not(target_os = "macos"))] - { - window.show().ok(); - } - - drop(state); - - if let Some(rx) = shutdown_preview { - let _ = tokio::time::timeout(Duration::from_millis(500), rx).await; - } - - window - } - } - Self::WindowCaptureOccluder { screen_id } => { - let Some(display) = Display::from_id(screen_id) else { - return Err(tauri::Error::WindowNotFound); - }; - - let title = CapWindowId::WindowCaptureOccluder { - screen_id: screen_id.clone(), - } - .title(); - let should_protect = should_protect_window(app, &title); - - let mut window_builder = self - .window_builder(app, "/window-capture-occluder") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .skip_taskbar(true) - .transparent(true); - - #[cfg(target_os = "macos")] - { - let position = display.raw_handle().logical_position(); - let Some(size) = display.logical_size() else { - warn!(screen_id = %screen_id, "Missing display logical size for window capture occluder"); - return Err(tauri::Error::WindowNotFound); - }; - - window_builder = window_builder - .inner_size(size.width(), size.height()) - .position(position.x(), position.y()); - } - - // On Windows a window's DPI scale isn't known until it's placed on a - // monitor, so sizing/positioning from display bounds at build time is - // unreliable across monitors with different DPIs. Build a placeholder - // and fix the geometry up after the window exists (below), mirroring - // the TargetSelectOverlay path. - #[cfg(windows)] - { - window_builder = window_builder.inner_size(100.0, 100.0).position(0.0, 0.0); - } - - #[cfg(target_os = "linux")] - { - let position = display.raw_handle().physical_position().unwrap(); - let Some(size) = display.physical_size() else { - warn!(screen_id = %screen_id, "Missing display size for window capture occluder"); - return Err(tauri::Error::WindowNotFound); - }; - window_builder = window_builder - .inner_size(size.width(), size.height()) - .position(position.x(), position.y()); - } - - let window = window_builder.build()?; - lock_window_text_scale(&window); - - #[cfg(target_os = "linux")] - { - use tauri::{LogicalSize, PhysicalPosition}; - let position = display.raw_handle().physical_position().unwrap(); - if let Some(size) = display.physical_size() { - let _ = - window.set_position(PhysicalPosition::new(position.x(), position.y())); - let _ = window.set_size(LogicalSize::new(size.width(), size.height())); - } - } - - // Fix up the occluder geometry now that the window exists and its real - // per-monitor DPI is known: position with physical coordinates (which - // are unambiguous across monitors), then set the logical size so the - // window covers the full display. Verify the resulting physical size - // matches the display and re-apply once if the initial placement raced - // the DPI change. - #[cfg(windows)] - { - let Some(position) = display.raw_handle().physical_position() else { - warn!(screen_id = %screen_id, "Missing display position for window capture occluder"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(logical_size) = display.logical_size() else { - warn!(screen_id = %screen_id, "Missing display logical size for window capture occluder"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(physical_size) = display.physical_size() else { - warn!(screen_id = %screen_id, "Missing display physical size for window capture occluder"); - return Err(tauri::Error::WindowNotFound); - }; - use tauri::{LogicalSize, PhysicalPosition}; - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - let _ = window.set_position(PhysicalPosition::new(position.x(), position.y())); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - match window.inner_size() { - Ok(actual_physical_size) - if physical_size.width() != actual_physical_size.width as f64 => - { - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - } - Ok(_) => {} - Err(err) => { - warn!(%err, "Failed to read window capture occluder inner size"); - } - } - } - - if let Err(err) = window.set_ignore_cursor_events(true) { - warn!(%err, "Failed to ignore cursor events for window capture occluder"); - } - - #[cfg(target_os = "macos")] - { - crate::platform::set_window_level(window.as_ref().window(), 900); - } - - window - } - Self::CaptureArea { screen_id } => { - let title = CapWindowId::CaptureArea.title(); - let should_protect = should_protect_window(app, &title); - - let mut window_builder = self - .window_builder(app, "/capture-area") - .maximized(false) - .fullscreen(false) - .shadow(false) - .resizable(false) - .always_on_top(true) - .content_protected(should_protect) - .skip_taskbar(true) - .closable(true) - .decorations(false) - .transparent(true); - - let Some(display) = Display::from_id(screen_id) else { - return Err(tauri::Error::WindowNotFound); - }; - - #[cfg(target_os = "macos")] - if let Some(bounds) = display.raw_handle().logical_bounds() { - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } - - // On Windows a window's DPI scale isn't known until it's placed on a - // monitor, so sizing/positioning from logical bounds at build time is - // unreliable across monitors with different DPIs — the overlay ends up - // sized for the wrong monitor and no longer covers the target display, - // which truncates area selections on HiDPI secondary monitors. Build a - // placeholder and fix the geometry up after the window exists (below), - // mirroring the TargetSelectOverlay path. - #[cfg(windows)] - { - window_builder = window_builder.inner_size(100.0, 100.0).position(0.0, 0.0); - } - - #[cfg(target_os = "linux")] - if let Some(bounds) = display.raw_handle().physical_bounds() { - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } - - let window = window_builder.build()?; - lock_window_text_scale(&window); - - // Fix up the overlay geometry now that the window exists and its real - // per-monitor DPI is known: position with physical coordinates (which are - // unambiguous across monitors), then set the logical size so the window - // covers the full display. Verify the resulting physical size matches the - // display and re-apply once if the initial placement raced the DPI change. - #[cfg(windows)] - { - let Some(position) = display.raw_handle().physical_position() else { - warn!(display_id = %screen_id, "Missing display position for capture area overlay"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(logical_size) = display.logical_size() else { - warn!(display_id = %screen_id, "Missing display logical size for capture area overlay"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(physical_size) = display.physical_size() else { - warn!(display_id = %screen_id, "Missing display physical size for capture area overlay"); - return Err(tauri::Error::WindowNotFound); - }; - use tauri::{LogicalSize, PhysicalPosition}; - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - let _ = window.set_position(PhysicalPosition::new(position.x(), position.y())); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - match window.inner_size() { - Ok(actual_physical_size) - if physical_size.width() != actual_physical_size.width as f64 => - { - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - } - Ok(_) => {} - Err(err) => { - warn!(%err, "Failed to read capture area overlay inner size"); - } - } - } - - #[cfg(target_os = "linux")] - if let Some(bounds) = display.raw_handle().physical_bounds() { - use tauri::{LogicalSize, PhysicalPosition}; - let _ = window.set_position(PhysicalPosition::new( - bounds.position().x(), - bounds.position().y(), - )); - let _ = window.set_size(LogicalSize::new( - bounds.size().width(), - bounds.size().height(), - )); - } - - #[cfg(target_os = "macos")] - crate::platform::set_window_level( - window.as_ref().window(), - objc2_app_kit::NSPopUpMenuWindowLevel, - ); - - // Hide the main window if the target monitor is the same - if let Some(main_window) = CapWindowId::Main.get(app) - && let (Ok(outer_pos), Ok(outer_size)) = - (main_window.outer_position(), main_window.outer_size()) - && let Ok(scale_factor) = main_window.scale_factor() - && display.intersects(outer_pos, outer_size, scale_factor) - { - let _ = main_window.minimize(); - }; - - window - } - Self::InProgressRecording { - countdown, - capture_target, - } => { - let width = 320.0; - let height = 150.0; - - let title = CapWindowId::RecordingControls.title(); - let should_protect = should_protect_window(app, &title); - - #[cfg(target_os = "macos")] - let panel_activation_guard = permissions::prepare_macos_panel_window(app); - - #[cfg(target_os = "macos")] - let window = { - self.window_builder(app, "/in-progress-recording") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .transparent(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .inner_size(width, height) - .skip_taskbar(true) - .visible(false) - .initialization_script(format!( - "window.COUNTDOWN = {};", - countdown.unwrap_or_default() - )) - .build()? - }; - - #[cfg(windows)] - let window = self - .window_builder(app, "/in-progress-recording") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .transparent(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .inner_size(width, height) - .skip_taskbar(false) - .initialization_script(format!( - "window.COUNTDOWN = {};", - countdown.unwrap_or_default() - )) - .build()?; - - #[cfg(target_os = "linux")] - let window = self - .window_builder(app, "/in-progress-recording") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .transparent(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .inner_size(width, height) - .skip_taskbar(false) - .initialization_script(format!( - "window.COUNTDOWN = {};", - countdown.unwrap_or_default() - )) - .build()?; - - lock_window_text_scale(&window); - - #[cfg(target_os = "windows")] - log_window_content_protection(&window, should_protect, &title); - - let (pos_x, pos_y) = capture_target - .as_ref() - .and_then(fake_window::calculate_recording_controls_position_for_target) - .unwrap_or_else(|| cursor_monitor.bottom_center_position(width, height, 120.0)); - let _ = window.set_position(logical_point_position(pos_x, pos_y)); - - debug!( - "InProgressRecording window: cursor_monitor=({}, {}, {}, {}), pos=({}, {})", - cursor_monitor.x, - cursor_monitor.y, - cursor_monitor.width, - cursor_monitor.height, - pos_x, - pos_y - ); - - debug!( - "InProgressRecording window created: label={}, inner_size={:?}, outer_position={:?}", - window.label(), - window.inner_size(), - window.outer_position() - ); - - #[cfg(target_os = "macos")] - { - app.run_on_main_thread({ - let window = window.clone(); - let app = app.clone(); - let panel_activation_guard = panel_activation_guard; - move || { - let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; - - #[link(name = "CoreGraphics", kind = "framework")] - unsafe extern "C" { - fn CGWindowLevelForKey(key: i32) -> i32; - } - - #[allow(non_upper_case_globals)] - const kCGMaximumWindowLevelKey: i32 = 10; - - let delegate = panel_delegate!(RecordingControlsPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match window.to_panel() { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert recording controls to panel: {:?}", e); - crate::permissions::sync_macos_dock_visibility(&app); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - panel.set_delegate(delegate); - - let max_level = unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; - panel.set_level(max_level); - - panel.order_front_regardless(); - panel.show(); - - crate::permissions::schedule_macos_dock_visibility_sync(&app); - } - }) - .ok(); - - fake_window::spawn_fake_window_listener(app.clone(), window.clone()); - } - - #[cfg(windows)] - { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - let show_result = window.show(); - debug!( - "InProgressRecording window.show() result: {:?}", - show_result - ); - window.set_focus().ok(); - fake_window::spawn_fake_window_listener(app.clone(), window.clone()); - } - - window - } - Self::RecordingsOverlay => { - let title = CapWindowId::RecordingsOverlay.title(); - let should_protect = should_protect_window(app, &title); - - let window = self - .window_builder(app, "/recordings-overlay") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .visible_on_all_workspaces(true) - .accept_first_mouse(true) - .content_protected(should_protect) - .inner_size(cursor_monitor.width, cursor_monitor.height) - .skip_taskbar(true) - .transparent(true) - .build()?; - lock_window_text_scale(&window); - - let _ = window - .set_position(cursor_monitor.position(cursor_monitor.x, cursor_monitor.y)); - - // The build-time inner_size above was interpreted with the DPI of - // whatever monitor the window materialized on; now that it sits on the - // cursor monitor, re-apply the logical size so it converts with that - // monitor's scale, then verify against the expected physical size. - #[cfg(windows)] - { - let _ = window.set_size(LogicalSize::new( - cursor_monitor.width, - cursor_monitor.height, - )); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - let expected_physical_width = - (cursor_monitor.width * cursor_monitor.scale).round(); - match window.inner_size() { - Ok(actual_physical_size) - if expected_physical_width != actual_physical_size.width as f64 => - { - let _ = window.set_size(LogicalSize::new( - cursor_monitor.width, - cursor_monitor.height, - )); - } - Ok(_) => {} - Err(err) => { - warn!(%err, "Failed to read recordings overlay inner size"); - } - } - } - - #[cfg(target_os = "macos")] - { - app.run_on_main_thread({ - let window = window.clone(); - move || { - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use crate::panel_manager::try_to_panel; - - let panel = match try_to_panel(&window) { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert recordings overlay to panel: {}", e); - return; - } - }; - - panel.set_level(cocoa::appkit::NSMainMenuWindowLevel); - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorTransient - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorIgnoresCycle, - ); - - #[allow(non_upper_case_globals)] - const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; - panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); - } - }) - .ok(); - } - - fake_window::spawn_fake_window_listener(app.clone(), window.clone()); - - window - } - }; - - // removing this for now as it causes windows to just stay hidden sometimes -_- - // window.hide().ok(); - - #[cfg(target_os = "macos")] - if let Some(position) = _id.traffic_lights_position() { - add_traffic_lights(&window, position); - } - - #[cfg(target_os = "macos")] - if _id.activates_dock() { - crate::permissions::sync_macos_dock_visibility(app); - } - - Ok(window) - } - - fn window_builder<'a>( - &'a self, - app: &'a AppHandle, - url: impl Into, - ) -> WebviewWindowBuilder<'a, Wry, AppHandle> { - let id = self.id(app); - self.window_builder_with_label(app, url, id.label()) - } - - fn window_builder_with_label<'a>( - &'a self, - app: &'a AppHandle, - url: impl Into, - label: impl Into, - ) -> WebviewWindowBuilder<'a, Wry, AppHandle> { - let id = self.id(app); - - let settings = GeneralSettingsStore::get(app).ok().flatten(); - let window_transparency_enabled = settings - .as_ref() - .map(|s| s.window_transparency) - .unwrap_or(false); - let theme = settings - .map(|s| match s.theme { - AppTheme::System => None, - AppTheme::Light => Some(tauri::Theme::Light), - AppTheme::Dark => Some(tauri::Theme::Dark), - }) - .unwrap_or(None); - - let mut builder = WebviewWindow::builder(app, label, WebviewUrl::App(url.into())) - .title(id.title()) - .visible(false) - .accept_first_mouse(true) - .shadow(true) - .theme(theme) - .devtools(cfg!(debug_assertions)); - - if !id.is_transparent() { - let is_dark = match theme { - Some(tauri::Theme::Dark) => true, - Some(tauri::Theme::Light) => false, - None | Some(_) => is_system_dark_mode(), - }; - - let bg_color = if is_dark { "#141414" } else { "#ffffff" }; - let init_script = format!( - r#"(function(){{var s=document.createElement('style');s.textContent='html,body{{background-color:{bg_color}}}';document.documentElement.appendChild(s);}})();"# - ); - builder = builder.initialization_script(&init_script); - - // Native backing color so the window is themed before the webview's - // first paint, allowing windows to be shown immediately without a - // white/black flash. Skipped when the user has window transparency - // enabled: an opaque native background would sit behind the - // translucent webview content and defeat the effect. - if !window_transparency_enabled { - let native_bg = if is_dark { - tauri::window::Color(0x14, 0x14, 0x14, 0xff) - } else { - tauri::window::Color(0xff, 0xff, 0xff, 0xff) - }; - builder = builder.background_color(native_bg); - } - } - - if let Some(min) = id.min_size() { - builder = builder - .inner_size(min.0, min.1) - .min_inner_size(min.0, min.1); - } - - #[cfg(target_os = "macos")] - { - if id.traffic_lights_position().is_some() { - builder = builder - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay); - } else { - builder = builder.decorations(false) - } - } - - #[cfg(windows)] - { - let browser_args = windows_webview2_browser_args(); - let browser_args_json = serde_json::to_string(&browser_args) - .expect("Failed to serialize Windows WebView2 browser arguments"); - builder = builder - .decorations(false) - .zoom_hotkeys_enabled(false) - .additional_browser_args(&browser_args) - .initialization_script(format!( - "window.__CAP__ = window.__CAP__ ?? {{}}; window.__CAP__.windowsWebview2BrowserArgs = {browser_args_json};" - )); - } - - // Linux has no native macOS-style traffic lights, so we drop the window - // manager decorations and draw our own chrome (matching the macOS layout). - #[cfg(target_os = "linux")] - { - builder = builder.decorations(false); - } - - builder - } - - pub fn id(&self, app: &AppHandle) -> CapWindowId { - match self { - ShowCapWindow::Main { .. } => CapWindowId::Main, - ShowCapWindow::Settings { .. } => CapWindowId::Settings, - ShowCapWindow::Editor { project_path } => { - let state = app.state::(); - let s = state.ids.lock().unwrap(); - let id = s.iter().find(|(path, _)| path == project_path).unwrap().1; - CapWindowId::Editor { id } - } - ShowCapWindow::RecordingsOverlay => CapWindowId::RecordingsOverlay, - ShowCapWindow::TargetSelectOverlay { display_id, .. } => { - CapWindowId::TargetSelectOverlay { - display_id: display_id.clone(), - } - } - ShowCapWindow::WindowCaptureOccluder { screen_id } => { - CapWindowId::WindowCaptureOccluder { - screen_id: screen_id.clone(), - } - } - ShowCapWindow::CaptureArea { .. } => CapWindowId::CaptureArea, - ShowCapWindow::Camera { .. } => CapWindowId::Camera, - ShowCapWindow::InProgressRecording { .. } => CapWindowId::RecordingControls, - ShowCapWindow::Upgrade => CapWindowId::Upgrade, - ShowCapWindow::ModeSelect => CapWindowId::ModeSelect, - ShowCapWindow::Onboarding => CapWindowId::Onboarding, - ShowCapWindow::ScreenshotEditor { path } => { - let state = app.state::(); - let s = state.ids.lock().unwrap(); - let id = s.iter().find(|(p, _)| p == path).unwrap().1; - CapWindowId::ScreenshotEditor { id } - } - } - } -} - -fn lock_window_text_scale(_window: &WebviewWindow) { - #[cfg(windows)] - { - let scale_factor = match _window.scale_factor() { - Ok(scale_factor) => scale_factor, - Err(e) => { - warn!("Failed to read window scale factor: {}", e); - return; - } - }; - - if let Err(e) = _window.with_webview(move |webview| unsafe { - use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller3; - use windows_core::Interface; - - let controller = webview.controller(); - - if let Err(e) = controller.SetZoomFactor(1.0) { - warn!("Failed to lock WebView zoom factor: {}", e); - } - - let Ok(controller3) = controller.cast::() else { - warn!("Failed to access WebView2 controller scale APIs"); - return; - }; - - if let Err(e) = controller3.SetShouldDetectMonitorScaleChanges(false) { - warn!("Failed to disable WebView scale detection: {}", e); - } - - if let Err(e) = controller3.SetRasterizationScale(scale_factor) { - warn!("Failed to lock WebView rasterization scale: {}", e); - } - }) { - warn!("Failed to access platform WebView: {}", e); - } - } -} - -/// `lock_window_text_scale` disables WebView2's own monitor-scale detection -/// (so the Windows text-size setting can't zoom the UI), which also stops it -/// following per-monitor DPI. The new scale factor must be forwarded here on -/// every `ScaleFactorChanged`, or a window dragged to a monitor with -/// different scaling keeps rasterizing and laying out at the old DPI. -pub fn update_window_rasterization_scale(_window: &WebviewWindow, _scale_factor: f64) { - #[cfg(windows)] - { - if let Err(e) = _window.with_webview(move |webview| unsafe { - use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller3; - use windows_core::Interface; - - let controller = webview.controller(); - - let Ok(controller3) = controller.cast::() else { - warn!("Failed to access WebView2 controller scale APIs"); - return; - }; - - if let Err(e) = controller3.SetRasterizationScale(_scale_factor) { - warn!("Failed to update WebView rasterization scale: {}", e); - } - }) { - warn!("Failed to access platform WebView: {}", e); - } - } -} - -#[cfg(target_os = "macos")] -fn add_traffic_lights(window: &WebviewWindow, controls_inset: Option>) { - use crate::platform::delegates; - - let target_window = window.clone(); - window - .run_on_main_thread(move || { - delegates::setup( - target_window.as_ref().window(), - controls_inset.unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET), - ); - - let c_win = target_window.clone(); - target_window.on_window_event(move |event| match event { - tauri::WindowEvent::ThemeChanged(..) | tauri::WindowEvent::Focused(..) => { - position_traffic_lights_impl(&c_win.as_ref().window(), controls_inset); - } - _ => {} - }); - }) - .ok(); -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window))] -pub fn set_theme(window: tauri::Window, theme: AppTheme) { - let _ = window.set_theme(match theme { - AppTheme::System => None, - AppTheme::Light => Some(tauri::Theme::Light), - AppTheme::Dark => Some(tauri::Theme::Dark), - }); - - #[cfg(target_os = "macos")] - match CapWindowId::from_str(window.label()) { - Ok(win) if win.traffic_lights_position().is_some() => position_traffic_lights(window, None), - Ok(_) | Err(_) => {} - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(_window))] -pub fn position_traffic_lights(_window: tauri::Window, _controls_inset: Option<(f64, f64)>) { - #[cfg(target_os = "macos")] - position_traffic_lights_impl( - &_window, - _controls_inset.map(LogicalPosition::from).or_else(|| { - // Attempt to get the default inset from the window's traffic lights position - CapWindowId::from_str(_window.label()) - .ok() - .and_then(|id| id.traffic_lights_position().flatten()) - }), - ); -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(_window))] -pub fn set_teleprompter_window_level(_window: tauri::Window, _always_on_top: bool) { - #[cfg(target_os = "macos")] - if _window.label() == CapWindowId::Teleprompter.to_string() { - let level = if _always_on_top { - TELEPROMPTER_PANEL_LEVEL - } else { - objc2_app_kit::NSNormalWindowLevel - }; - crate::platform::set_window_level(_window, level); - } - - #[cfg(not(target_os = "macos"))] - if _window.label() == CapWindowId::Teleprompter.to_string() - && let Err(error) = _window.set_always_on_top(_always_on_top) - { - warn!(?error, "Failed to update teleprompter window level"); - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(_window))] -pub fn set_teleprompter_window_opacity(_window: tauri::Window, _opacity: f64) { - #[cfg(target_os = "macos")] - if _window.label() == CapWindowId::Teleprompter.to_string() { - crate::platform::set_window_opacity(_window, _opacity); - } -} - -#[cfg(target_os = "macos")] -fn position_traffic_lights_impl( - window: &tauri::Window, - controls_inset: Option>, -) { - use crate::platform::delegates::{UnsafeWindowHandle, position_window_controls}; - let c_win = window.clone(); - window - .run_on_main_thread(move || { - let ns_window = match c_win.ns_window() { - Ok(handle) => handle, - Err(_) => return, - }; - position_window_controls( - UnsafeWindowHandle(ns_window), - &controls_inset.unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET), - ); - }) - .ok(); -} - -// Capture exclusion (WDA_EXCLUDEFROMCAPTURE / NSWindowSharingType::None) also hides -// the window from "capture-based" displays such as virtual/indirect/dummy-HDMI or -// mirrored monitors, making it invisible and unreachable. We therefore only protect -// Cap's own windows while a recording is actually active, which is the only time the -// exclusion is meaningful. -// -// On desktops that are themselves delivered through a capture-based stream (Shadow -// and other cloud PCs, RDP, VMs), even recording-gated exclusion hides the recording -// controls from the user and trips DRM detectors (Shadow error S:102), so exclusion -// is skipped entirely there — Cap's windows then appear in recordings, which is the -// lesser evil. Overridable via the CAP_WINDOW_CAPTURE_EXCLUSION env var. -#[cfg(target_os = "windows")] -pub fn capture_exclusion_hides_ui() -> bool { - static LAST_LOGGED: std::sync::Mutex> = std::sync::Mutex::new(None); - - let reason = crate::platform::win::capture_streamed_display_reason(); - - if let Ok(mut last) = LAST_LOGGED.lock() - && *last != reason - { - match &reason { - Some(reason) => warn!( - %reason, - "Skipping window capture exclusion: this desktop is viewed through a \ - capture-based stream, so excluded windows would be invisible to the user. \ - Cap's windows will appear in recordings." - ), - None => info!("Window capture exclusion re-enabled"), - } - *last = reason.clone(); - } - - reason.is_some() -} - -#[cfg(not(target_os = "windows"))] -pub fn capture_exclusion_hides_ui() -> bool { - false -} - -fn content_protection_enabled(app: &AppHandle) -> bool { - app.try_state::>() - .and_then(|state| { - state - .try_read() - .ok() - .map(|app| app.is_recording_active_or_pending()) - }) - .unwrap_or(false) -} - -fn window_capture_excluded(app: &AppHandle, window_title: &str) -> bool { - if window_title == CapWindowId::Teleprompter.title() { - return true; - } - - let matches = |list: &[WindowExclusion]| { - list.iter() - .any(|entry| entry.matches(None, None, Some(window_title))) - }; - - GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|settings| matches(&settings.excluded_windows)) - .unwrap_or_else(|| matches(&general_settings::default_excluded_windows())) -} - -fn should_protect_window(app: &AppHandle, window_title: &str) -> bool { - content_protection_enabled(app) - && !capture_exclusion_hides_ui() - && window_capture_excluded(app, window_title) -} - -pub fn apply_content_protection(app: &AppHandle, enabled: bool) { - let enabled = enabled && !capture_exclusion_hides_ui(); - - for (label, window) in app.webview_windows() { - let Ok(id) = CapWindowId::from_str(&label) else { - continue; - }; - - // The camera window's protection depends on the recording mode (studio excludes - // the preview, instant keeps it) and is driven from `start_recording`. Only ever - // clear it here so it stays visible outside of recordings. - if matches!(id, CapWindowId::Camera) { - if !enabled { - let _ = window.set_content_protected(false); - } - continue; - } - - let title = id.title(); - let should_protect = enabled && window_capture_excluded(app, &title); - let _ = window.set_content_protected(should_protect); - - #[cfg(target_os = "windows")] - log_window_content_protection(&window, should_protect, &title); - } -} - -#[cfg(target_os = "windows")] -fn cached_windows_version() -> Option<&'static scap_direct3d::WindowsVersion> { - static VERSION: std::sync::OnceLock> = - std::sync::OnceLock::new(); - VERSION - .get_or_init(scap_direct3d::WindowsVersion::detect) - .as_ref() -} - -#[cfg(target_os = "windows")] -fn display_affinity_name(value: u32) -> &'static str { - match value { - 0 => "WDA_NONE", - 1 => "WDA_MONITOR", - 17 => "WDA_EXCLUDEFROMCAPTURE", - _ => "UNKNOWN", - } -} - -#[cfg(target_os = "windows")] -fn log_window_content_protection(window: &WebviewWindow, enabled: bool, window_title: &str) { - use windows::Win32::UI::WindowsAndMessaging::{ - GetWindowDisplayAffinity, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, - }; - - let expected = if enabled { - WDA_EXCLUDEFROMCAPTURE - } else { - WDA_NONE - }; - - if let Some(version) = cached_windows_version() - && enabled - && version.build < 19041 - { - warn!( - window = window.label(), - title = window_title, - version = %version.display_name(), - expected = display_affinity_name(expected.0), - "Window capture exclusion is not fully supported on this Windows build" - ); - } - - let hwnd = match window.hwnd() { - Ok(hwnd) => windows::Win32::Foundation::HWND(hwnd.0), - Err(error) => { - warn!( - window = window.label(), - title = window_title, - error = %error, - "Failed to get HWND for content protection diagnostics" - ); - return; - } - }; - - let mut applied = 0u32; - match unsafe { GetWindowDisplayAffinity(hwnd, &mut applied) } { - Ok(()) => { - if applied == expected.0 { - debug!( - window = window.label(), - title = window_title, - expected = display_affinity_name(expected.0), - applied = display_affinity_name(applied), - "Window content protection verified" - ); - } else { - warn!( - window = window.label(), - title = window_title, - expected = display_affinity_name(expected.0), - expected_raw = expected.0, - applied = display_affinity_name(applied), - applied_raw = applied, - "Window content protection mismatch" - ); - } - } - Err(error) => { - warn!( - window = window.label(), - title = window_title, - expected = display_affinity_name(expected.0), - error = %error, - "Failed to query window display affinity" - ); - } - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), String> { - let enabled = content_protection_enabled(&app); - apply_content_protection(&app, enabled); - Ok(()) -} - -// Credits: tauri-plugin-window-state -trait MonitorExt { - fn intersects( - &self, - position: PhysicalPosition, - size: PhysicalSize, - scale: f64, - ) -> bool; -} - -impl MonitorExt for Display { - fn intersects( - &self, - position: PhysicalPosition, - size: PhysicalSize, - _scale: f64, - ) -> bool { - #[cfg(target_os = "macos")] - { - let Some(bounds) = self.raw_handle().logical_bounds() else { - return false; - }; - - let left = (bounds.position().x() * _scale) as i32; - let right = left + (bounds.size().width() * _scale) as i32; - let top = (bounds.position().y() * _scale) as i32; - let bottom = top + (bounds.size().height() * _scale) as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) - } - - #[cfg(windows)] - { - let Some(bounds) = self.raw_handle().physical_bounds() else { - return false; - }; - - let left = bounds.position().x() as i32; - let right = left + bounds.size().width() as i32; - let top = bounds.position().y() as i32; - let bottom = top + bounds.size().height() as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) - } - - #[cfg(target_os = "linux")] - { - let Some(bounds) = self.raw_handle().physical_bounds() else { - return false; - }; - - let left = bounds.position().x() as i32; - let right = left + bounds.size().width() as i32; - let top = bounds.position().y() as i32; - let bottom = top + bounds.size().height() as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) - } - } -} - -#[specta::specta] -#[tauri::command(async)] -#[instrument(skip(_window))] -pub async fn apply_macos_liquid_glass_background( - _window: tauri::Window, - _enabled: bool, - _radius: f64, -) -> Result { - #[cfg(target_os = "macos")] - { - let window = _window.clone(); - let (tx, rx) = tokio::sync::oneshot::channel(); - - _window - .run_on_main_thread(move || { - let result = if window.label() == CapWindowId::Main.label() { - crate::platform::apply_main_window_liquid_glass_background( - &window, _enabled, _radius, - ) - } else { - crate::platform::apply_liquid_glass_background(&window, _enabled, _radius) - }; - let _ = tx.send(result); - }) - .map_err(|error| error.to_string())?; - - return rx - .await - .map_err(|_| "macOS Liquid Glass task was cancelled".to_string())?; - } - - #[cfg(not(target_os = "macos"))] - { - Ok(false) - } -} - -#[specta::specta] -#[tauri::command(async)] -#[instrument(skip(_window))] -pub fn set_window_transparent(_window: tauri::Window, _value: bool) { - #[cfg(target_os = "macos")] - { - let ns_win = _window - .ns_window() - .expect("Failed to get native window handle") - as *const objc2_app_kit::NSWindow; - - unsafe { - (*ns_win).setOpaque(!_value); - } - } -} - -#[derive(Default, Clone)] -pub struct EditorWindowIds { - pub ids: Arc>>, - pub counter: Arc, -} - -impl EditorWindowIds { - pub fn get(app: &AppHandle) -> Self { - app.state::().deref().clone() - } -} - -#[derive(Default, Clone)] -pub struct ScreenshotEditorWindowIds { - pub ids: Arc>>, - pub counter: Arc, -} - -impl ScreenshotEditorWindowIds { - pub fn get(app: &AppHandle) -> Self { - app.state::().deref().clone() - } -} - -#[derive(Default, Clone)] -pub struct EditorRecordingTarget(pub Arc>>); - -impl EditorRecordingTarget { - pub fn get(app: &AppHandle) -> Self { - app.state::().deref().clone() - } - - pub fn set(app: &AppHandle, path: Option) { - *Self::get(app).0.lock().unwrap() = path; - } - - pub fn current(app: &AppHandle) -> Option { - Self::get(app).0.lock().unwrap().clone() - } - - pub fn take(app: &AppHandle) -> Option { - Self::get(app).0.lock().unwrap().take() - } -} - -pub fn editor_window_for_path(app: &AppHandle, path: &std::path::Path) -> Option { - let ids = EditorWindowIds::get(app); - let id = { - let guard = ids.ids.lock().unwrap(); - guard.iter().find(|(p, _)| p == path).map(|(_, id)| *id)? - }; - CapWindowId::Editor { id }.get(app) -} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json deleted file mode 100644 index ff84112d8e1..00000000000 --- a/apps/desktop/src-tauri/tauri.conf.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "Cap - Development", - "identifier": "so.cap.desktop.dev", - "mainBinaryName": "Cap - Development", - "build": { - "beforeDevCommand": "pnpm localdev", - "devUrl": "http://localhost:3002", - "beforeBuildCommand": "pnpm turbo build --filter @cap/desktop", - "frontendDist": "../.output/public" - }, - "app": { - "macOSPrivateApi": true, - "security": { - "csp": "default-src 'self' ws: ipc: http://ipc.localhost; connect-src 'self' ws: wss: ipc: http://ipc.localhost https://t.cap.so; img-src 'self' asset: http://asset.localhost blob: data:; media-src 'self' asset: http://asset.localhost; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'", - "assetProtocol": { - "enable": true, - "scope": [ - "$APPDATA/**", - "**/*.jpg", - "**/*.webp", - "**/*.mp3", - "**/*.wav", - "**/*.m4a", - "**/*.ogg", - "**/*.flac", - "**/*.aac", - "$RESOURCE/**", - "**/*.riv", - "$HOME/**" - ] - } - } - }, - "plugins": { - "updater": { "active": false, "pubkey": "" }, - "deep-link": { - "desktop": { - "schemes": ["cap-desktop"] - } - } - }, - "bundle": { - "active": true, - "createUpdaterArtifacts": true, - "targets": "all", - "externalBin": [ - "binaries/cap-muxer", - "binaries/cap-exporter", - "binaries/cap-cli" - ], - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/macos/icon.icns", - "icons/icon.ico" - ], - "resources": { - "assets/backgrounds/macOS/*": "assets/backgrounds/macOS/", - "assets/backgrounds/blue/*": "assets/backgrounds/blue/", - "assets/backgrounds/cities/*": "assets/backgrounds/cities/", - "assets/backgrounds/dark/*": "assets/backgrounds/dark/", - "assets/backgrounds/orange/*": "assets/backgrounds/orange/", - "assets/backgrounds/purple/*": "assets/backgrounds/purple/", - "../src/assets/rive/*.riv": "assets/rive/", - "../src/assets/music/*.mp3": "assets/music/" - }, - "macOS": { - "dmg": { - "background": "assets/dmg-background.png", - "appPosition": { - "x": 180, - "y": 140 - }, - "applicationFolderPosition": { - "x": 480, - "y": 140 - } - }, - "frameworks": ["../../../target/native-deps/Spacedrive.framework"] - }, - "windows": { - "nsis": { - "headerImage": "assets/nsis-header.bmp", - "sidebarImage": "assets/nsis-sidebar.bmp", - "installerIcon": "icons/icon.ico" - }, - "wix": { - "upgradeCode": "79f4309d-ca23-54df-b6f9-826a1d783676", - "bannerPath": "assets/wix-banner.bmp", - "dialogImagePath": "assets/wix-dialog.bmp" - } - }, - "linux": { - "deb": { - "depends": [ - "libwebkit2gtk-4.1-0", - "libgtk-3-0", - "libayatana-appindicator3-1", - "libva2", - "libva-drm2", - "libpipewire-0.3-0", - "libasound2t64 | libasound2" - ], - "files": { - "/usr/share/icons/hicolor/scalable/status/so.cap.desktop-tray-default-symbolic.svg": "icons/linux/so.cap.desktop-tray-default-symbolic.svg", - "/usr/share/icons/hicolor/scalable/status/so.cap.desktop-tray-instant-symbolic.svg": "icons/linux/so.cap.desktop-tray-instant-symbolic.svg", - "/usr/share/icons/hicolor/scalable/status/so.cap.desktop-tray-screenshot-symbolic.svg": "icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg", - "/usr/share/icons/hicolor/scalable/status/so.cap.desktop-tray-studio-symbolic.svg": "icons/linux/so.cap.desktop-tray-studio-symbolic.svg", - "/usr/share/icons/hicolor/scalable/status/so.cap.desktop-tray-stop-symbolic.svg": "icons/linux/so.cap.desktop-tray-stop-symbolic.svg" - } - } - }, - "fileAssociations": [ - { - "ext": ["cap"], - "name": "Cap Project", - "mimeType": "application/x-cap-project", - "role": "Editor" - } - ] - } -} diff --git a/apps/desktop/src-tauri/tauri.prod.conf.json b/apps/desktop/src-tauri/tauri.prod.conf.json deleted file mode 100644 index 3297c552ab5..00000000000 --- a/apps/desktop/src-tauri/tauri.prod.conf.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "Cap", - "mainBinaryName": "Cap", - "identifier": "so.cap.desktop", - "build": { - "beforeBundleCommand": "node scripts/prodBeforeBundle.js" - }, - "plugins": { - "updater": { - "active": true, - "endpoints": [ - "https://cdn.crabnebula.app/update/cap/cap/{{target}}/{{current_version}}" - ], - "dialog": true, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUyOTAzOTdFNzJFQkRFOTMKUldTVDN1dHlmam1RNHFXb1VYTXlrQk1iMFFkcjN0YitqZlA5WnZNY0ZtQ1dvM1dxK211M3VIYUQK" - } - }, - "bundle": { - "macOS": { - "entitlements": "Entitlements.plist" - }, - "windows": { - "wix": { - "upgradeCode": "a765d9de-0ecc-55d0-b8a0-61e9d3276664" - } - } - } -} diff --git a/crates/desktop-runtime-macros/Cargo.toml b/crates/desktop-runtime-macros/Cargo.toml new file mode 100644 index 00000000000..e46cbcadf91 --- /dev/null +++ b/crates/desktop-runtime-macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cap-desktop-runtime-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } + +[lints] +workspace = true diff --git a/crates/desktop-runtime-macros/src/lib.rs b/crates/desktop-runtime-macros/src/lib.rs new file mode 100644 index 00000000000..21470931faf --- /dev/null +++ b/crates/desktop-runtime-macros/src/lib.rs @@ -0,0 +1,236 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + DeriveInput, FnArg, GenericArgument, ItemFn, Pat, PathArguments, ReturnType, Type, + parse_macro_input, +}; + +#[proc_macro_attribute] +pub fn command(_attribute: TokenStream, item: TokenStream) -> TokenStream { + let function = parse_macro_input!(item as ItemFn); + expand_command(function).into() +} + +#[proc_macro_derive(Event)] +pub fn derive_event(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let name = input.ident; + let event_name = pascal_to_kebab(&name.to_string()); + + quote! { + impl ::cap_desktop_runtime::Event for #name { + const NAME: &'static str = #event_name; + } + } + .into() +} + +fn expand_command(function: ItemFn) -> proc_macro2::TokenStream { + let function_name = &function.sig.ident; + let command_name = function_name.to_string(); + let handler_name = format_ident!("__cap_desktop_command_{function_name}"); + let mut setup = Vec::new(); + let mut arguments = Vec::new(); + + for input in &function.sig.inputs { + let FnArg::Typed(argument) = input else { + return syn::Error::new_spanned(input, "desktop commands cannot have a receiver") + .to_compile_error(); + }; + let Pat::Ident(pattern) = argument.pat.as_ref() else { + return syn::Error::new_spanned( + &argument.pat, + "desktop command arguments must be identifiers", + ) + .to_compile_error(); + }; + let variable = &pattern.ident; + let type_name = last_type_name(&argument.ty); + + match type_name.as_deref() { + Some("AppHandle") => { + setup.push(quote! { let #variable = context.app().clone(); }); + arguments.push(quote! { #variable }); + } + Some("Window") | Some("WebviewWindow") => { + setup.push(quote! { let #variable = context.window(); }); + arguments.push(quote! { #variable }); + } + Some("State") | Some("MutableState") => { + let Some(inner) = first_type_argument(&argument.ty) else { + return syn::Error::new_spanned( + &argument.ty, + "state arguments must include their managed type", + ) + .to_compile_error(); + }; + if type_name.as_deref() == Some("MutableState") { + setup.push(quote! { + let #variable = context + .app() + .state::<::std::sync::Arc<::tokio::sync::RwLock<#inner>>>(); + }); + } else { + setup.push(quote! { let #variable = context.app().state::<#inner>(); }); + } + arguments.push(quote! { #variable }); + } + Some("Channel") => { + let Some(inner) = first_type_argument(&argument.ty) else { + return syn::Error::new_spanned( + &argument.ty, + "channel arguments must include their message type", + ) + .to_compile_error(); + }; + let key = snake_to_camel(&variable.to_string()); + setup.push(quote! { + let #variable = ::cap_desktop_runtime::Channel::<#inner>::from_value( + context.channel_sender(), + ::cap_desktop_runtime::take_argument(&mut arguments, #key)?, + )?; + }); + arguments.push(quote! { #variable }); + } + Some( + "WindowEditorInstance" + | "OptionalWindowEditorInstance" + | "WindowScreenshotEditorInstance", + ) => { + let ty = &argument.ty; + setup.push(quote! { + let #variable = <#ty as ::cap_desktop_runtime::CommandArg>::from_command(&context)?; + }); + arguments.push(quote! { #variable }); + } + _ => { + let key = snake_to_camel(&variable.to_string()); + if let Type::Reference(reference) = argument.ty.as_ref() { + let inner = reference.elem.as_ref(); + if last_type_name(inner).as_deref() == Some("str") { + setup.push(quote! { + let #variable: ::std::string::String = ::cap_desktop_runtime::deserialize_argument( + ::cap_desktop_runtime::take_argument(&mut arguments, #key)?, + #key, + )?; + }); + arguments.push(quote! { #variable.as_str() }); + } else { + setup.push(quote! { + let #variable: #inner = ::cap_desktop_runtime::deserialize_argument( + ::cap_desktop_runtime::take_argument(&mut arguments, #key)?, + #key, + )?; + }); + arguments.push(quote! { &#variable }); + } + } else { + let ty = &argument.ty; + setup.push(quote! { + let #variable: #ty = ::cap_desktop_runtime::deserialize_argument( + ::cap_desktop_runtime::take_argument(&mut arguments, #key)?, + #key, + )?; + }); + arguments.push(quote! { #variable }); + } + } + } + } + + let call = quote! { #function_name(#(#arguments),*) }; + let call = if function.sig.asyncness.is_some() { + quote! { #call.await } + } else { + call + }; + let serialize = match &function.sig.output { + ReturnType::Type(_, ty) if is_result_type(ty) => quote! { + let value = #call.map_err(|error| format!("{error:?}"))?; + ::cap_desktop_runtime::serialize_command_result(value) + }, + _ => quote! { + let value = #call; + ::cap_desktop_runtime::serialize_command_result(value) + }, + }; + + quote! { + #function + + fn #handler_name( + context: ::cap_desktop_runtime::CommandContext, + arguments: ::serde_json::Value, + ) -> ::cap_desktop_runtime::CommandFuture { + Box::pin(async move { + let mut arguments = ::cap_desktop_runtime::argument_object(arguments)?; + #(#setup)* + #serialize + }) + } + + ::cap_desktop_runtime::inventory::submit! { + ::cap_desktop_runtime::CommandRegistration { + name: #command_name, + handler: #handler_name, + } + } + } +} + +fn last_type_name(ty: &Type) -> Option { + match ty { + Type::Path(path) => path + .path + .segments + .last() + .map(|segment| segment.ident.to_string()), + Type::Reference(reference) => last_type_name(&reference.elem), + _ => None, + } +} + +fn first_type_argument(ty: &Type) -> Option { + let Type::Path(path) = ty else { + return None; + }; + let segment = path.path.segments.last()?; + let PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return None; + }; + arguments.args.iter().find_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty.clone()), + _ => None, + }) +} + +fn is_result_type(ty: &Type) -> bool { + last_type_name(ty).as_deref() == Some("Result") +} + +fn snake_to_camel(value: &str) -> String { + let mut output = String::new(); + let mut uppercase = false; + for character in value.trim_start_matches('_').chars() { + if character == '_' { + uppercase = true; + } else if uppercase { + output.extend(character.to_uppercase()); + uppercase = false; + } else { + output.push(character); + } + } + output +} + +fn pascal_to_kebab(value: &str) -> String { + let mut output = String::new(); + for (index, character) in value.chars().enumerate() { + if character.is_uppercase() && index > 0 { + output.push('-'); + } + output.extend(character.to_lowercase()); + } + output +} diff --git a/crates/desktop-runtime/Cargo.toml b/crates/desktop-runtime/Cargo.toml new file mode 100644 index 00000000000..9011f433a43 --- /dev/null +++ b/crates/desktop-runtime/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cap-desktop-runtime" +version = "0.1.0" +edition = "2024" + +[dependencies] +cap-desktop-runtime-macros = { path = "../desktop-runtime-macros" } +dirs = "6" +futures = { workspace = true } +inventory = "0.3.19" +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["io-util", "net"] } +tracing = { workspace = true } +url = "2" + +[lints] +workspace = true diff --git a/crates/desktop-runtime/src/lib.rs b/crates/desktop-runtime/src/lib.rs new file mode 100644 index 00000000000..fa508afe5a0 --- /dev/null +++ b/crates/desktop-runtime/src/lib.rs @@ -0,0 +1,1334 @@ +mod protocol; +mod transport; + +use std::{ + any::{Any, TypeId}, + collections::{HashMap, hash_map::Entry}, + future::Future, + marker::PhantomData, + ops::Deref, + path::PathBuf, + pin::Pin, + sync::{ + Arc, Mutex as StdMutex, OnceLock, RwLock, + atomic::{AtomicU64, Ordering}, + }, +}; + +extern crate self as cap_desktop_runtime; + +pub use cap_desktop_runtime_macros::{Event, command}; +pub use inventory; +pub use protocol::{ + BackendMessage, ChannelMessage, CommandResponse, DesktopOperation, PROTOCOL_VERSION, + ShellMessage, WindowEvent, WindowOptions, WindowState, +}; +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::{Map, Value}; +use tokio::sync::mpsc; +pub use transport::{BackendOptions, run_backend}; +pub use url::Url; + +pub type Error = String; +pub type Result = std::result::Result; +pub type CommandFuture = Pin> + Send + 'static>>; +pub type EventId = u64; + +pub trait Runtime: Send + Sync + 'static {} + +#[derive(Clone, Copy, Debug)] +pub struct ElectronRuntime; + +impl Runtime for ElectronRuntime {} + +pub type Wry = ElectronRuntime; + +pub trait Manager { + fn app_handle(&self) -> &AppHandle; + + fn state(&self) -> State<'_, T> { + self.app_handle().state() + } + + fn try_state(&self) -> Option> { + self.app_handle().try_state() + } +} +pub trait Emitter {} +pub trait Listener {} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub struct LogicalPosition { + pub x: T, + pub y: T, +} + +impl LogicalPosition { + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } +} + +impl From> for Position { + fn from(value: LogicalPosition) -> Self { + Self::Logical(LogicalPosition::new(value.x as f64, value.y as f64)) + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub struct PhysicalPosition { + pub x: T, + pub y: T, +} + +impl PhysicalPosition { + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } +} + +impl PhysicalPosition { + pub fn to_logical>(self, scale_factor: f64) -> LogicalPosition { + LogicalPosition::new( + U::from(self.x as f64 / scale_factor), + U::from(self.y as f64 / scale_factor), + ) + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub struct LogicalSize { + pub width: T, + pub height: T, +} + +impl LogicalSize { + pub const fn new(width: T, height: T) -> Self { + Self { width, height } + } +} + +impl From> for Size { + fn from(value: LogicalSize) -> Self { + Self::Logical(LogicalSize::new(value.width as f64, value.height as f64)) + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub struct PhysicalSize { + pub width: T, + pub height: T, +} + +impl PhysicalSize { + pub const fn new(width: T, height: T) -> Self { + Self { width, height } + } +} + +impl PhysicalSize { + pub fn to_logical>(self, scale_factor: f64) -> LogicalSize { + LogicalSize::new( + U::from(self.width as f64 / scale_factor), + U::from(self.height as f64 / scale_factor), + ) + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub enum Position { + Logical(LogicalPosition), + Physical(PhysicalPosition), +} + +impl From> for Position { + fn from(value: LogicalPosition) -> Self { + Self::Logical(value) + } +} + +impl From> for Position { + fn from(value: PhysicalPosition) -> Self { + Self::Physical(value) + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub enum Size { + Logical(LogicalSize), + Physical(PhysicalSize), +} + +impl From> for Size { + fn from(value: LogicalSize) -> Self { + Self::Logical(value) + } +} + +impl From> for Size { + fn from(value: PhysicalSize) -> Self { + Self::Physical(value) + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Monitor { + pub name: Option, + pub position: PhysicalPosition, + pub size: PhysicalSize, + pub work_area: MonitorWorkArea, + pub scale_factor: f64, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct MonitorWorkArea { + pub position: PhysicalPosition, + pub size: PhysicalSize, +} + +impl Monitor { + pub fn position(&self) -> &PhysicalPosition { + &self.position + } + + pub fn size(&self) -> &PhysicalSize { + &self.size + } + + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Theme { + Light, + Dark, +} + +#[derive(Clone, Copy, Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ActivationPolicy { + Regular, + Accessory, + Prohibited, +} + +pub mod path { + #[derive(Clone, Copy, Debug)] + pub enum BaseDirectory { + Audio, + Cache, + Config, + Data, + LocalData, + Desktop, + Document, + Download, + Home, + Picture, + Public, + Resource, + Temp, + Template, + Video, + AppConfig, + AppData, + AppLocalData, + AppCache, + AppLog, + } +} + +pub mod ipc { + pub use crate::{Channel, CommandArg}; + pub type InvokeError = String; +} + +pub struct CommandRegistration { + pub name: &'static str, + pub handler: fn(CommandContext, Value) -> CommandFuture, +} + +inventory::collect!(CommandRegistration); + +#[derive(Clone)] +pub struct CommandContext { + app: AppHandle, + window_label: String, + channel_sender: mpsc::UnboundedSender, +} + +impl CommandContext { + pub fn new( + app: AppHandle, + window_label: impl Into, + channel_sender: mpsc::UnboundedSender, + ) -> Self { + Self { + app, + window_label: window_label.into(), + channel_sender, + } + } + + pub fn app(&self) -> &AppHandle { + &self.app + } + + pub fn window(&self) -> Window { + Window::new(self.app.clone(), self.window_label.clone()) + } + + pub fn channel_sender(&self) -> mpsc::UnboundedSender { + self.channel_sender.clone() + } +} + +pub trait CommandArg: Sized { + fn from_command(context: &CommandContext) -> Result; +} + +pub fn argument_object(value: Value) -> Result> { + match value { + Value::Null => Ok(Map::new()), + Value::Object(arguments) => Ok(arguments), + _ => Err("desktop command arguments must be an object".to_string()), + } +} + +pub fn take_argument(arguments: &mut Map, name: &str) -> Result { + arguments + .remove(name) + .ok_or_else(|| format!("missing desktop command argument '{name}'")) +} + +pub fn deserialize_argument(value: Value, name: &str) -> Result { + serde_json::from_value(value) + .map_err(|error| format!("invalid desktop command argument '{name}': {error}")) +} + +pub fn serialize_command_result(value: T) -> Result { + serde_json::to_value(value) + .map_err(|error| format!("failed to serialize command result: {error}")) +} + +pub async fn dispatch_command( + context: CommandContext, + command: &str, + arguments: Value, +) -> Result { + let registration = inventory::iter:: + .into_iter() + .find(|registration| registration.name == command) + .ok_or_else(|| format!("unknown desktop command '{command}'"))?; + (registration.handler)(context, arguments).await +} + +type ManagedState = HashMap>; +type EventHandler = Arc; +type WindowEventHandler = Arc; +type GlobalWindowEventHandler = Arc; + +pub struct AppHandle { + inner: Arc, + runtime: PhantomData, +} + +struct AppHandleInner { + outbound: mpsc::UnboundedSender, + state: RwLock, + windows: RwLock>, + listeners: RwLock>>, + window_listeners: RwLock>>, + global_window_listeners: RwLock>, + next_event_id: AtomicU64, + paths: AppPaths, + stores: RwLock>>, + native_requests: StdMutex>>>, + next_native_request_id: AtomicU64, + cursor_position: RwLock>, +} + +impl Clone for AppHandle { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + runtime: PhantomData, + } + } +} + +impl std::fmt::Debug for AppHandle { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.debug_struct("AppHandle").finish_non_exhaustive() + } +} + +impl Manager for AppHandle { + fn app_handle(&self) -> &AppHandle { + self + } +} +impl Emitter for AppHandle {} +impl Listener for AppHandle {} + +impl AppHandle { + pub fn new(outbound: mpsc::UnboundedSender, paths: AppPaths) -> Self { + Self { + inner: Arc::new(AppHandleInner { + outbound, + state: RwLock::new(HashMap::new()), + windows: RwLock::new(HashMap::new()), + listeners: RwLock::new(HashMap::new()), + window_listeners: RwLock::new(HashMap::new()), + global_window_listeners: RwLock::new(HashMap::new()), + next_event_id: AtomicU64::new(1), + paths, + stores: RwLock::new(HashMap::new()), + native_requests: StdMutex::new(HashMap::new()), + next_native_request_id: AtomicU64::new(1), + cursor_position: RwLock::new(PhysicalPosition::new(0.0, 0.0)), + }), + runtime: PhantomData, + } + } + + pub fn manage(&self, value: T) -> bool { + let mut state = self + .inner + .state + .write() + .expect("desktop state lock poisoned"); + match state.entry(TypeId::of::()) { + Entry::Vacant(entry) => { + entry.insert(Arc::new(value)); + true + } + Entry::Occupied(_) => false, + } + } + + pub fn state(&self) -> State<'_, T> { + self.try_state::().unwrap_or_else(|| { + panic!( + "desktop state '{}' is not managed", + std::any::type_name::() + ) + }) + } + + pub fn try_state(&self) -> Option> { + let value = self + .inner + .state + .read() + .expect("desktop state lock poisoned") + .get(&TypeId::of::())? + .clone() + .downcast::() + .ok()?; + Some(State { + value, + lifetime: PhantomData, + }) + } + + pub fn emit(&self, event: &str, payload: S) -> Result<()> { + let payload = serde_json::to_value(payload) + .map_err(|error| format!("failed to serialize event '{event}': {error}"))?; + self.emit_value(event, payload) + } + + pub fn emit_value(&self, event: &str, payload: Value) -> Result<()> { + self.receive_event(event, payload.clone()); + self.send(BackendMessage::Event { + event: event.to_string(), + payload, + target: None, + }) + } + + pub fn receive_event(&self, event: &str, payload: Value) { + if let Some(handlers) = self + .inner + .listeners + .read() + .expect("desktop event lock poisoned") + .get(event) + { + for handler in handlers.values() { + handler(payload.clone()); + } + } + } + + pub fn receive_window_event(&self, label: String, event: WindowEvent) { + if matches!(event, WindowEvent::Destroyed) { + self.remove_window(&label); + } + if let Some(handlers) = self + .inner + .window_listeners + .read() + .expect("desktop window event lock poisoned") + .get(&label) + { + for handler in handlers.values() { + handler(event.clone()); + } + } + for handler in self + .inner + .global_window_listeners + .read() + .expect("desktop global window event lock poisoned") + .values() + { + handler(label.clone(), event.clone()); + } + } + + fn listen_value( + &self, + event: impl Into, + handler: impl Fn(Value) + Send + Sync + 'static, + ) -> EventId { + let id = self.inner.next_event_id.fetch_add(1, Ordering::Relaxed); + self.inner + .listeners + .write() + .expect("desktop event lock poisoned") + .entry(event.into()) + .or_default() + .insert(id, Arc::new(handler)); + id + } + + pub fn listen( + &self, + event: impl Into, + handler: impl Fn(RuntimeEvent) + Send + Sync + 'static, + ) -> EventId { + self.listen_value(event, move |payload| { + handler(RuntimeEvent { + payload: serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string()), + }); + }) + } + + pub fn listen_any( + &self, + event: impl Into, + handler: impl Fn(RuntimeEvent) + Send + Sync + 'static, + ) -> EventId { + self.listen(event, handler) + } + + pub fn unlisten(&self, id: EventId) { + for handlers in self + .inner + .listeners + .write() + .expect("desktop event lock poisoned") + .values_mut() + { + handlers.remove(&id); + } + } + + fn listen_window( + &self, + label: String, + handler: impl Fn(WindowEvent) + Send + Sync + 'static, + ) -> EventId { + let id = self.inner.next_event_id.fetch_add(1, Ordering::Relaxed); + self.inner + .window_listeners + .write() + .expect("desktop window event lock poisoned") + .entry(label) + .or_default() + .insert(id, Arc::new(handler)); + id + } + + pub fn on_window_event( + &self, + handler: impl Fn(Window, WindowEvent) + Send + Sync + 'static, + ) -> EventId { + let id = self.inner.next_event_id.fetch_add(1, Ordering::Relaxed); + let app = self.clone(); + self.inner + .global_window_listeners + .write() + .expect("desktop global window event lock poisoned") + .insert( + id, + Arc::new(move |label, event| { + handler(Window::new(app.clone(), label), event); + }), + ); + id + } + + pub fn get_webview_window(&self, label: &str) -> Option> { + self.inner + .windows + .read() + .expect("desktop window lock poisoned") + .contains_key(label) + .then(|| Window::new(self.clone(), label)) + } + + pub fn webview_windows(&self) -> HashMap> { + self.inner + .windows + .read() + .expect("desktop window lock poisoned") + .keys() + .map(|label| (label.clone(), Window::new(self.clone(), label))) + .collect() + } + + pub fn update_window_state(&self, label: String, state: WindowState) { + self.inner + .windows + .write() + .expect("desktop window lock poisoned") + .insert(label, state); + } + + pub fn remove_window(&self, label: &str) { + self.inner + .windows + .write() + .expect("desktop window lock poisoned") + .remove(label); + } + + pub fn path(&self) -> &AppPaths { + &self.inner.paths + } + + pub fn store(&self, name: &str) -> Result> { + if let Some(store) = self + .inner + .stores + .read() + .expect("desktop stores lock poisoned") + .get(name) + { + return Ok(store.clone()); + } + let path = self.inner.paths.app_data_dir.join(format!("{name}.json")); + let store = Arc::new(Store::load(path)?); + self.inner + .stores + .write() + .expect("desktop stores lock poisoned") + .insert(name.to_string(), store.clone()); + Ok(store) + } + + pub fn run_on_main_thread(&self, task: impl FnOnce() + Send + 'static) -> Result<()> { + task(); + Ok(()) + } + + pub fn exit(&self, code: i32) { + let _ = self.send(BackendMessage::NativeOperation { + operation: "app.exit".to_string(), + payload: serde_json::json!({ "exitCode": code }), + }); + } + + pub fn set_dock_visibility(&self, visible: bool) -> Result<()> { + self.native_operation( + "app.dockVisibility", + serde_json::json!({ "visible": visible }), + ) + } + + pub fn set_activation_policy(&self, policy: ActivationPolicy) -> Result<()> { + self.native_operation( + "app.activationPolicy", + serde_json::json!({ "policy": policy }), + ) + } + + pub fn native_operation(&self, operation: &str, payload: S) -> Result<()> { + let payload = serde_json::to_value(payload) + .map_err(|error| format!("failed to serialize native operation: {error}"))?; + self.send(BackendMessage::NativeOperation { + operation: operation.to_string(), + payload, + }) + } + + pub async fn native_request( + &self, + operation: &str, + payload: S, + ) -> Result { + let id = self + .inner + .next_native_request_id + .fetch_add(1, Ordering::Relaxed); + let payload = serde_json::to_value(payload) + .map_err(|error| format!("failed to serialize native request: {error}"))?; + let (sender, receiver) = tokio::sync::oneshot::channel(); + self.inner + .native_requests + .lock() + .expect("desktop native request lock poisoned") + .insert(id, sender); + if let Err(error) = self.send(BackendMessage::NativeRequest { + id, + operation: operation.to_string(), + payload, + }) { + self.inner + .native_requests + .lock() + .expect("desktop native request lock poisoned") + .remove(&id); + return Err(error); + } + let value = receiver + .await + .map_err(|_| "Electron native request was cancelled".to_string())??; + serde_json::from_value(value) + .map_err(|error| format!("invalid Electron native response: {error}")) + } + + pub fn receive_native_result(&self, id: u64, result: Result) { + if let Some(sender) = self + .inner + .native_requests + .lock() + .expect("desktop native request lock poisoned") + .remove(&id) + { + let _ = sender.send(result); + } + } + + pub fn receive_cursor_position(&self, x: f64, y: f64) { + *self + .inner + .cursor_position + .write() + .expect("desktop cursor position lock poisoned") = PhysicalPosition::new(x, y); + } + + pub fn send(&self, message: BackendMessage) -> Result<()> { + self.inner + .outbound + .send(message) + .map_err(|_| "Electron desktop connection is closed".to_string()) + } +} + +pub struct State<'a, T> { + value: Arc, + lifetime: PhantomData<&'a T>, +} + +pub type MutableState<'a, T> = State<'a, T>; + +impl Clone for State<'_, T> { + fn clone(&self) -> Self { + Self { + value: self.value.clone(), + lifetime: PhantomData, + } + } +} + +impl Deref for State<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl State<'_, T> { + pub fn inner(&self) -> &T { + &self.value + } +} + +impl std::fmt::Debug for State<'_, T> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(formatter) + } +} + +pub struct Window { + app: AppHandle, + label: String, +} + +pub type WebviewWindow = Window; + +impl Clone for Window { + fn clone(&self) -> Self { + Self { + app: self.app.clone(), + label: self.label.clone(), + } + } +} + +impl std::fmt::Debug for Window { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Window") + .field("label", &self.label) + .finish() + } +} + +impl Manager for Window { + fn app_handle(&self) -> &AppHandle { + &self.app + } +} +impl Emitter for Window {} +impl Listener for Window {} + +impl Window { + pub fn new(app: AppHandle, label: impl Into) -> Self { + Self { + app, + label: label.into(), + } + } + + pub fn label(&self) -> &str { + &self.label + } + + pub fn app_handle(&self) -> &AppHandle { + &self.app + } + + pub fn run_on_main_thread(&self, task: impl FnOnce() + Send + 'static) -> Result<()> { + self.app.run_on_main_thread(task) + } + + pub fn state(&self) -> State<'_, T> { + self.app.state() + } + + pub fn try_state(&self) -> Option> { + self.app.try_state() + } + + pub fn manage(&self, value: T) -> bool { + self.app.manage(value) + } + + pub fn show(&self) -> Result<()> { + self.operation(DesktopOperation::Show) + } + + pub fn hide(&self) -> Result<()> { + self.operation(DesktopOperation::Hide) + } + + pub fn close(&self) -> Result<()> { + self.operation(DesktopOperation::Close) + } + + pub fn destroy(&self) -> Result<()> { + self.operation(DesktopOperation::Destroy) + } + + pub fn set_focus(&self) -> Result<()> { + self.operation(DesktopOperation::Focus) + } + + pub fn is_visible(&self) -> Result { + Ok(self.state_snapshot()?.visible) + } + + pub fn is_focused(&self) -> Result { + Ok(self.state_snapshot()?.focused) + } + + pub fn is_fullscreen(&self) -> Result { + Ok(self.state_snapshot()?.fullscreen) + } + + pub fn native_window_id(&self) -> Result> { + Ok(self.state_snapshot()?.native_window_id) + } + + pub fn set_content_protected(&self, enabled: bool) -> Result<()> { + self.operation(DesktopOperation::SetContentProtected { enabled }) + } + + pub fn minimize(&self) -> Result<()> { + self.operation(DesktopOperation::Minimize) + } + + pub fn unminimize(&self) -> Result<()> { + self.operation(DesktopOperation::Unminimize) + } + + pub fn maximize(&self) -> Result<()> { + self.operation(DesktopOperation::Maximize) + } + + pub fn unmaximize(&self) -> Result<()> { + self.operation(DesktopOperation::Unmaximize) + } + + pub fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + self.operation(DesktopOperation::SetFullscreen { fullscreen }) + } + + pub fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { + self.operation(DesktopOperation::SetAlwaysOnTop { always_on_top }) + } + + pub fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { + self.operation(DesktopOperation::SetIgnoreCursorEvents { ignore }) + } + + pub fn set_position(&self, position: impl Into) -> Result<()> { + let (x, y, physical) = match position.into() { + Position::Logical(position) => (position.x, position.y, false), + Position::Physical(position) => (position.x as f64, position.y as f64, true), + }; + self.operation(DesktopOperation::SetPosition { x, y, physical }) + } + + pub fn set_size(&self, size: impl Into) -> Result<()> { + let (width, height, physical) = match size.into() { + Size::Logical(size) => (size.width, size.height, false), + Size::Physical(size) => (size.width as f64, size.height as f64, true), + }; + self.operation(DesktopOperation::SetSize { + width, + height, + physical, + }) + } + + pub fn set_min_size(&self, size: Option) -> Result<()> { + let (width, height, physical) = match size { + Some(Size::Logical(size)) => (Some(size.width), Some(size.height), false), + Some(Size::Physical(size)) => (Some(size.width as f64), Some(size.height as f64), true), + None => (None, None, false), + }; + self.operation(DesktopOperation::SetMinSize { + width, + height, + physical, + }) + } + + pub fn set_title(&self, title: &str) -> Result<()> { + self.operation(DesktopOperation::SetTitle { + title: title.to_string(), + }) + } + + pub fn set_resizable(&self, resizable: bool) -> Result<()> { + self.operation(DesktopOperation::SetResizable { resizable }) + } + + pub fn set_opacity(&self, opacity: f64) -> Result<()> { + self.operation(DesktopOperation::SetOpacity { opacity }) + } + + pub fn outer_position(&self) -> Result> { + let state = self.state_snapshot()?; + Ok(PhysicalPosition::new(state.x as i32, state.y as i32)) + } + + pub fn outer_size(&self) -> Result> { + let state = self.state_snapshot()?; + Ok(PhysicalSize::new(state.width as u32, state.height as u32)) + } + + pub fn inner_size(&self) -> Result> { + self.outer_size() + } + + pub fn scale_factor(&self) -> Result { + Ok(self.state_snapshot()?.scale_factor) + } + + pub fn current_monitor(&self) -> Result> { + let state = self.state_snapshot()?; + Ok(state.monitor.map(|monitor| Monitor { + name: monitor.name, + position: PhysicalPosition::new(monitor.x, monitor.y), + size: PhysicalSize::new(monitor.width, monitor.height), + work_area: MonitorWorkArea { + position: PhysicalPosition::new(monitor.work_x, monitor.work_y), + size: PhysicalSize::new(monitor.work_width, monitor.work_height), + }, + scale_factor: monitor.scale_factor.max(1.0), + })) + } + + pub fn cursor_position(&self) -> Result> { + Ok(*self + .app + .inner + .cursor_position + .read() + .expect("desktop cursor position lock poisoned")) + } + + pub fn emit(&self, event: &str, payload: S) -> Result<()> { + let payload = serde_json::to_value(payload) + .map_err(|error| format!("failed to serialize event '{event}': {error}"))?; + self.app.send(BackendMessage::Event { + event: event.to_string(), + payload, + target: Some(self.label.clone()), + }) + } + + pub fn on_window_event( + &self, + handler: impl Fn(WindowEvent) + Send + Sync + 'static, + ) -> EventId { + self.app.listen_window(self.label.clone(), handler) + } + + pub fn operation(&self, operation: DesktopOperation) -> Result<()> { + self.app.send(BackendMessage::DesktopOperation { + label: self.label.clone(), + operation, + }) + } + + fn state_snapshot(&self) -> Result { + self.app + .inner + .windows + .read() + .expect("desktop window lock poisoned") + .get(&self.label) + .cloned() + .ok_or_else(|| format!("window '{}' does not exist", self.label)) + } +} + +#[derive(Clone)] +pub struct Channel { + id: u64, + next_index: Arc, + sender: Option>, + local_sender: Option Result<()> + Send + Sync>>, + message: PhantomData, +} + +impl Channel { + pub fn new(sender: impl Fn(T) -> Result<()> + Send + Sync + 'static) -> Self { + Self { + id: 0, + next_index: Arc::new(AtomicU64::new(0)), + sender: None, + local_sender: Some(Arc::new(sender)), + message: PhantomData, + } + } + + pub fn from_value(sender: mpsc::UnboundedSender, value: Value) -> Result { + let marker = value + .as_str() + .and_then(|value| value.strip_prefix("__CHANNEL__:")) + .ok_or_else(|| "invalid desktop channel marker".to_string())?; + let id = marker + .parse() + .map_err(|error| format!("invalid desktop channel id: {error}"))?; + Ok(Self { + id, + next_index: Arc::new(AtomicU64::new(0)), + sender: Some(sender), + local_sender: None, + message: PhantomData, + }) + } +} + +impl Channel { + pub fn send(&self, value: T) -> Result<()> { + if let Some(sender) = &self.local_sender { + return sender(value); + } + let message = serde_json::to_value(value) + .map_err(|error| format!("failed to serialize channel message: {error}"))?; + self.sender + .as_ref() + .ok_or_else(|| "desktop channel sender is unavailable".to_string())? + .send(ChannelMessage { + channel_id: self.id, + index: self.next_index.fetch_add(1, Ordering::Relaxed), + message: Some(message), + end: false, + }) + .map_err(|_| "Electron desktop channel is closed".to_string()) + } +} + +impl Drop for Channel { + fn drop(&mut self) { + let Some(sender) = &self.sender else { + return; + }; + let _ = sender.send(ChannelMessage { + channel_id: self.id, + index: self.next_index.load(Ordering::Relaxed), + message: None, + end: true, + }); + } +} + +pub trait Event: Sized + 'static { + const NAME: &'static str; + + fn emit(&self, app: &AppHandle) -> Result<()> + where + Self: Serialize, + { + app.emit(Self::NAME, self) + } + + fn listen_any( + app: &AppHandle, + handler: impl Fn(EventPayload) + Send + Sync + 'static, + ) -> EventId + where + Self: DeserializeOwned, + { + app.listen_value(Self::NAME, move |payload| { + match serde_json::from_value(payload) { + Ok(payload) => handler(EventPayload { payload }), + Err(error) => { + tracing::warn!(event = Self::NAME, %error, "Invalid desktop event payload") + } + } + }) + } +} + +pub struct EventPayload { + pub payload: T, +} + +pub struct RuntimeEvent { + payload: String, +} + +impl RuntimeEvent { + pub fn payload(&self) -> &str { + &self.payload + } +} + +pub struct Store { + path: PathBuf, + values: RwLock>, +} + +impl Store { + fn load(path: PathBuf) -> Result { + let values = match std::fs::read(&path) { + Ok(bytes) => serde_json::from_slice(&bytes) + .map_err(|error| format!("failed to read desktop store: {error}"))?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Map::new(), + Err(error) => return Err(format!("failed to read desktop store: {error}")), + }; + Ok(Self { + path, + values: RwLock::new(values), + }) + } + + pub fn get(&self, key: &str) -> Option { + self.values + .read() + .expect("desktop store lock poisoned") + .get(key) + .cloned() + } + + pub fn set(&self, key: impl Into, value: T) { + match serde_json::to_value(value) { + Ok(value) => { + self.values + .write() + .expect("desktop store lock poisoned") + .insert(key.into(), value); + } + Err(error) => tracing::error!(%error, "Failed to serialize desktop store value"), + } + } + + pub fn delete(&self, key: &str) -> bool { + self.values + .write() + .expect("desktop store lock poisoned") + .remove(key) + .is_some() + } + + pub fn save(&self) -> Result<()> { + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("failed to create desktop store directory: {error}"))?; + } + let bytes = + serde_json::to_vec_pretty(&*self.values.read().expect("desktop store lock poisoned")) + .map_err(|error| format!("failed to serialize desktop store: {error}"))?; + std::fs::write(&self.path, bytes) + .map_err(|error| format!("failed to write desktop store: {error}")) + } +} + +#[derive(Clone)] +pub struct AppPaths { + app_data_dir: PathBuf, + resource_dir: PathBuf, +} + +impl AppPaths { + pub fn discover(identifier: &str, resource_dir: PathBuf) -> Result { + let app_data_dir = std::env::var_os("CAP_ELECTRON_APP_DATA_DIR") + .map(PathBuf::from) + .map(Ok) + .unwrap_or_else(|| { + dirs::data_local_dir() + .ok_or_else(|| "failed to resolve local application data directory".to_string()) + .map(|directory| directory.join(identifier)) + })?; + Ok(Self { + app_data_dir, + resource_dir, + }) + } + + pub fn app_data_dir(&self) -> Result { + Ok(self.app_data_dir.clone()) + } + + pub fn app_local_data_dir(&self) -> Result { + Ok(self.app_data_dir.clone()) + } + + pub fn resource_dir(&self) -> Result { + Ok(self.resource_dir.clone()) + } + + pub fn resolve( + &self, + path: impl AsRef, + base: path::BaseDirectory, + ) -> Result { + let base = match base { + path::BaseDirectory::Resource => Some(self.resource_dir.clone()), + path::BaseDirectory::AppData + | path::BaseDirectory::AppLocalData + | path::BaseDirectory::AppConfig + | path::BaseDirectory::AppCache + | path::BaseDirectory::AppLog => Some(self.app_data_dir.clone()), + path::BaseDirectory::Home => dirs::home_dir(), + path::BaseDirectory::Desktop => dirs::desktop_dir(), + path::BaseDirectory::Document => dirs::document_dir(), + path::BaseDirectory::Download => dirs::download_dir(), + path::BaseDirectory::Picture => dirs::picture_dir(), + path::BaseDirectory::Video => dirs::video_dir(), + path::BaseDirectory::Audio => dirs::audio_dir(), + path::BaseDirectory::Cache => dirs::cache_dir(), + path::BaseDirectory::Config => dirs::config_dir(), + path::BaseDirectory::Data | path::BaseDirectory::LocalData => dirs::data_local_dir(), + path::BaseDirectory::Public + | path::BaseDirectory::Temp + | path::BaseDirectory::Template => Some(std::env::temp_dir()), + } + .ok_or_else(|| "failed to resolve desktop base directory".to_string())?; + Ok(base.join(path)) + } +} + +pub mod async_runtime { + use super::*; + + static HANDLE: OnceLock = OnceLock::new(); + + pub fn set(handle: tokio::runtime::Handle) { + let _ = HANDLE.set(handle); + } + + pub fn spawn(future: F) -> tokio::task::JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + HANDLE + .get() + .cloned() + .unwrap_or_else(tokio::runtime::Handle::current) + .spawn(future) + } + + pub fn spawn_blocking(task: F) -> tokio::task::JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + HANDLE + .get() + .cloned() + .unwrap_or_else(tokio::runtime::Handle::current) + .spawn_blocking(task) + } + + pub fn block_on(future: F) -> F::Output { + HANDLE + .get() + .cloned() + .unwrap_or_else(tokio::runtime::Handle::current) + .block_on(future) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Clone, Debug, Deserialize, Event, PartialEq, Serialize)] + struct RecordingStarted { + id: String, + } + + #[test] + fn event_names_match_the_existing_frontend_contract() { + assert_eq!(RecordingStarted::NAME, "recording-started"); + } + + #[test] + fn command_argument_names_are_deserialized_from_json() { + let mut arguments = + argument_object(serde_json::json!({ "projectPath": "/tmp/a.cap" })).unwrap(); + let project_path: PathBuf = deserialize_argument( + take_argument(&mut arguments, "projectPath").unwrap(), + "projectPath", + ) + .unwrap(); + assert_eq!(project_path, PathBuf::from("/tmp/a.cap")); + } +} diff --git a/crates/desktop-runtime/src/protocol.rs b/crates/desktop-runtime/src/protocol.rs new file mode 100644 index 00000000000..0ecfb18d737 --- /dev/null +++ b/crates/desktop-runtime/src/protocol.rs @@ -0,0 +1,244 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +pub const PROTOCOL_VERSION: u32 = 1; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde( + tag = "type", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum ShellMessage { + Hello { + token: String, + protocol_version: u32, + }, + Invoke { + id: u64, + window_label: String, + command: String, + arguments: Value, + }, + WindowState { + label: String, + state: WindowState, + }, + WindowEvent { + label: String, + event: WindowEvent, + }, + Event { + event: String, + #[serde(default)] + payload: Value, + }, + CursorPosition { + x: f64, + y: f64, + }, + NativeResult { + id: u64, + result: std::result::Result, + }, + Shutdown, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde( + tag = "type", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum BackendMessage { + Ready { + protocol_version: u32, + commands: Vec, + }, + InvokeResult { + id: u64, + response: CommandResponse, + }, + Event { + event: String, + payload: Value, + target: Option, + }, + Channel(ChannelMessage), + CreateWindow { + options: WindowOptions, + }, + DesktopOperation { + label: String, + operation: DesktopOperation, + }, + NativeOperation { + operation: String, + payload: Value, + }, + NativeRequest { + id: u64, + operation: String, + payload: Value, + }, + ShutdownComplete, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "status", rename_all = "camelCase")] +pub enum CommandResponse { + Ok { value: Value }, + Error { error: String }, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelMessage { + pub channel_id: u64, + pub index: u64, + pub message: Option, + pub end: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowOptions { + pub label: String, + pub route: String, + pub title: String, + pub x: Option, + pub y: Option, + pub width: f64, + pub height: f64, + pub min_width: Option, + pub min_height: Option, + pub transparent: bool, + pub decorations: bool, + pub resizable: bool, + pub always_on_top: bool, + pub visible_on_all_workspaces: bool, + pub skip_taskbar: bool, + pub content_protected: bool, + pub focus: bool, + pub visible: bool, + pub initialization: Value, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowState { + #[serde(default)] + pub native_window_id: Option, + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, + pub visible: bool, + pub focused: bool, + pub minimized: bool, + pub maximized: bool, + pub fullscreen: bool, + pub always_on_top: bool, + pub scale_factor: f64, + #[serde(default)] + pub monitor: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowMonitorState { + pub name: Option, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub work_x: i32, + pub work_y: i32, + pub work_width: u32, + pub work_height: u32, + pub scale_factor: f64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde( + tag = "type", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum WindowEvent { + CloseRequested, + Destroyed, + Focused { focused: bool }, + Moved { x: f64, y: f64 }, + Resized { width: f64, height: f64 }, + ScaleFactorChanged { scale_factor: f64 }, + ThemeChanged { theme: String }, + DragDrop { paths: Vec }, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde( + tag = "type", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum DesktopOperation { + Show, + Hide, + Close, + Destroy, + Focus, + Minimize, + Unminimize, + Maximize, + Unmaximize, + SetFullscreen { + fullscreen: bool, + }, + SetAlwaysOnTop { + always_on_top: bool, + }, + SetContentProtected { + enabled: bool, + }, + SetPosition { + x: f64, + y: f64, + physical: bool, + }, + SetSize { + width: f64, + height: f64, + physical: bool, + }, + SetMinSize { + width: Option, + height: Option, + physical: bool, + }, + SetTitle { + title: String, + }, + SetResizable { + resizable: bool, + }, + SetIgnoreCursorEvents { + ignore: bool, + }, + SetOpacity { + opacity: f64, + }, + SetTheme { + theme: String, + }, + SetTrafficLightPosition { + x: Option, + y: Option, + }, + RequestUserAttention { + critical: bool, + }, + SetProgress { + progress: Option, + }, +} diff --git a/crates/desktop-runtime/src/transport.rs b/crates/desktop-runtime/src/transport.rs new file mode 100644 index 00000000000..30365a06bbc --- /dev/null +++ b/crates/desktop-runtime/src/transport.rs @@ -0,0 +1,212 @@ +use std::{future::Future, path::PathBuf}; + +use serde::{Serialize, de::DeserializeOwned}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + net::TcpStream, + sync::mpsc, +}; + +use crate::{ + AppHandle, AppPaths, BackendMessage, ChannelMessage, CommandContext, CommandResponse, + PROTOCOL_VERSION, Result, ShellMessage, dispatch_command, +}; + +const MAX_FRAME_SIZE: usize = 128 * 1024 * 1024; + +pub struct BackendOptions { + pub identifier: String, + pub resource_dir: PathBuf, +} + +impl BackendOptions { + pub fn new(identifier: impl Into, resource_dir: impl Into) -> Self { + Self { + identifier: identifier.into(), + resource_dir: resource_dir.into(), + } + } +} + +pub async fn run_backend( + options: BackendOptions, + setup: Setup, + shutdown: Shutdown, +) -> Result<()> +where + Setup: FnOnce(&AppHandle) -> Result<()>, + Shutdown: FnOnce(AppHandle) -> ShutdownFuture, + ShutdownFuture: Future>, +{ + let address = std::env::var("CAP_ELECTRON_IPC_ADDR") + .map_err(|_| "CAP_ELECTRON_IPC_ADDR is not set".to_string())?; + let expected_token = std::env::var("CAP_ELECTRON_IPC_TOKEN") + .map_err(|_| "CAP_ELECTRON_IPC_TOKEN is not set".to_string())?; + let stream = TcpStream::connect(&address) + .await + .map_err(|error| format!("failed to connect to Electron at {address}: {error}"))?; + let (mut reader, mut writer) = stream.into_split(); + + let hello = read_frame::<_, ShellMessage>(&mut reader).await?; + match hello { + ShellMessage::Hello { + token, + protocol_version, + } if token == expected_token && protocol_version == PROTOCOL_VERSION => {} + ShellMessage::Hello { .. } => { + return Err("Electron desktop authentication failed".to_string()); + } + _ => return Err("Electron desktop did not begin with a hello message".to_string()), + } + + let (outbound_tx, mut outbound_rx) = mpsc::unbounded_channel(); + let paths = AppPaths::discover(&options.identifier, options.resource_dir)?; + let app = AppHandle::new(outbound_tx.clone(), paths); + setup(&app)?; + + let (channel_tx, mut channel_rx) = mpsc::unbounded_channel::(); + let channel_outbound = outbound_tx.clone(); + tokio::spawn(async move { + while let Some(message) = channel_rx.recv().await { + if channel_outbound + .send(BackendMessage::Channel(message)) + .is_err() + { + break; + } + } + }); + + let writer_task = tokio::spawn(async move { + while let Some(message) = outbound_rx.recv().await { + let shutdown_complete = matches!(message, BackendMessage::ShutdownComplete); + write_frame(&mut writer, &message).await?; + if shutdown_complete { + break; + } + } + Ok::<_, String>(()) + }); + + let mut commands = inventory::iter:: + .into_iter() + .map(|registration| registration.name.to_string()) + .collect::>(); + commands.sort_unstable(); + app.send(BackendMessage::Ready { + protocol_version: PROTOCOL_VERSION, + commands, + })?; + + loop { + match read_frame::<_, ShellMessage>(&mut reader).await? { + ShellMessage::Hello { .. } => { + return Err("Electron desktop sent a duplicate hello message".to_string()); + } + ShellMessage::Invoke { + id, + window_label, + command, + arguments, + } => { + let context = CommandContext::new(app.clone(), window_label, channel_tx.clone()); + let invoke_outbound = outbound_tx.clone(); + tokio::spawn(async move { + let response = match dispatch_command(context, &command, arguments).await { + Ok(value) => CommandResponse::Ok { value }, + Err(error) => CommandResponse::Error { error }, + }; + let _ = invoke_outbound.send(BackendMessage::InvokeResult { id, response }); + }); + } + ShellMessage::WindowState { label, state } => app.update_window_state(label, state), + ShellMessage::WindowEvent { label, event } => app.receive_window_event(label, event), + ShellMessage::Event { event, payload } => app.receive_event(&event, payload), + ShellMessage::CursorPosition { x, y } => app.receive_cursor_position(x, y), + ShellMessage::NativeResult { id, result } => app.receive_native_result(id, result), + ShellMessage::Shutdown => break, + } + } + + shutdown(app.clone()).await?; + app.send(BackendMessage::ShutdownComplete)?; + drop(app); + drop(outbound_tx); + writer_task + .await + .map_err(|error| format!("Electron desktop writer task failed: {error}"))??; + Ok(()) +} + +async fn read_frame(reader: &mut R) -> Result +where + R: AsyncRead + Unpin, + T: DeserializeOwned, +{ + let length = reader + .read_u32() + .await + .map_err(|error| format!("failed to read Electron desktop frame: {error}"))? + as usize; + if length > MAX_FRAME_SIZE { + return Err(format!( + "Electron desktop frame exceeded {MAX_FRAME_SIZE} bytes" + )); + } + let mut bytes = vec![0; length]; + reader + .read_exact(&mut bytes) + .await + .map_err(|error| format!("failed to read Electron desktop payload: {error}"))?; + serde_json::from_slice(&bytes) + .map_err(|error| format!("invalid Electron desktop message: {error}")) +} + +async fn write_frame(writer: &mut W, message: &T) -> Result<()> +where + W: AsyncWrite + Unpin, + T: Serialize, +{ + let bytes = serde_json::to_vec(message) + .map_err(|error| format!("failed to serialize Electron desktop message: {error}"))?; + if bytes.len() > MAX_FRAME_SIZE { + return Err(format!( + "Electron desktop frame exceeded {MAX_FRAME_SIZE} bytes" + )); + } + writer + .write_u32(bytes.len() as u32) + .await + .map_err(|error| format!("failed to write Electron desktop frame: {error}"))?; + writer + .write_all(&bytes) + .await + .map_err(|error| format!("failed to write Electron desktop payload: {error}"))?; + writer + .flush() + .await + .map_err(|error| format!("failed to flush Electron desktop payload: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::duplex; + + #[tokio::test] + async fn framed_messages_round_trip() { + let (mut sender, mut receiver) = duplex(4096); + let message = BackendMessage::Event { + event: "recording-started".to_string(), + payload: serde_json::json!({ "id": "recording-1" }), + target: None, + }; + let expected = serde_json::to_value(&message).unwrap(); + let send = tokio::spawn(async move { write_frame(&mut sender, &message).await }); + let received = read_frame::<_, BackendMessage>(&mut receiver) + .await + .unwrap(); + send.await.unwrap().unwrap(); + assert_eq!(serde_json::to_value(received).unwrap(), expected); + } +} diff --git a/crates/scap-targets/src/platform/macos.rs b/crates/scap-targets/src/platform/macos.rs index df961872eb2..98720d8fd1f 100644 --- a/crates/scap-targets/src/platform/macos.rs +++ b/crates/scap-targets/src/platform/macos.rs @@ -510,7 +510,7 @@ impl WindowImpl { pub fn app_icon(&self) -> Option> { use cocoa::base::{id, nil}; - use cocoa::foundation::{NSArray, NSAutoreleasePool, NSString}; + use cocoa::foundation::{NSArray, NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString}; use objc::{class, msg_send, sel, sel_impl}; let owner_name = self.owner_name()?; @@ -549,13 +549,25 @@ impl WindowImpl { return None; } - let tiff_data: id = msg_send![icon, TIFFRepresentation]; - if tiff_data.is_null() { + // The target card renders this at 64 points. Asking AppKit for a + // proposed 64-point image selects the 128px Retina representation + // instead of serializing the full 1024px icon and every TIFF rep. + let mut proposed_rect = + NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(64.0, 64.0)); + let cg_image: *mut c_void = msg_send![ + icon, + CGImageForProposedRect: &mut proposed_rect + context: nil + hints: nil + ]; + if cg_image.is_null() { return None; } let bitmap_rep_class = class!(NSBitmapImageRep); - let bitmap_rep: id = msg_send![bitmap_rep_class, imageRepWithData: tiff_data]; + let bitmap_rep: id = msg_send![bitmap_rep_class, alloc]; + let bitmap_rep: id = msg_send![bitmap_rep, initWithCGImage: cg_image]; + let bitmap_rep: id = msg_send![bitmap_rep, autorelease]; if bitmap_rep.is_null() { return None; } diff --git a/crates/workspace-hack/Cargo.toml b/crates/workspace-hack/Cargo.toml index 32e6b341b5e..56405dc2307 100644 --- a/crates/workspace-hack/Cargo.toml +++ b/crates/workspace-hack/Cargo.toml @@ -66,7 +66,6 @@ simd-adler32 = { version = "0.3" } smallvec = { version = "1", default-features = false, features = ["const_new"] } specta = { version = "2.0.0-rc.20", features = ["derive", "function", "serde_json", "uuid"] } stable_deref_trait = { version = "1" } -tauri-utils = { version = "2", default-features = false, features = ["build", "compression", "resources"] } thiserror = { version = "2" } time = { version = "0.3", features = ["formatting", "macros", "parsing", "serde"] } tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "net", "process", "rt-multi-thread", "sync", "time", "tracing"] } @@ -111,7 +110,6 @@ smallvec = { version = "1", default-features = false, features = ["const_new"] } specta-macros = { version = "2.0.0-rc.17", features = ["DO_NOT_USE_function"] } stable_deref_trait = { version = "1" } syn = { version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -tauri-utils = { version = "2", default-features = false, features = ["build", "compression", "resources"] } thiserror = { version = "2" } uuid = { version = "1", features = ["serde", "v4", "v7"] } From fa925ef2409eb3afc1d686b552a08b40fbf53b2d Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:36:47 +0100 Subject: [PATCH 02/31] feat(desktop): add the Electron shell and renderer bridge --- apps/desktop/.gitignore | 4 +- apps/desktop/app.config.ts | 16 +- apps/desktop/electron-builder.yml | 109 + apps/desktop/electron/backend.cjs | 178 ++ apps/desktop/electron/main.cjs | 211 ++ apps/desktop/electron/native.cjs | 419 ++++ apps/desktop/electron/preload.cjs | 74 + apps/desktop/electron/windows.cjs | 615 ++++++ apps/desktop/package.json | 40 +- apps/desktop/scripts/build-electron.mjs | 34 + apps/desktop/scripts/prepare.js | 2 +- apps/desktop/src/app.tsx | 4 +- .../src/components/CapErrorBoundary.tsx | 4 +- apps/desktop/src/components/Cropper.tsx | 4 +- apps/desktop/src/components/Tooltip.tsx | 2 +- .../desktop/src/components/selection-hint.tsx | 2 +- .../src/components/titlebar/Titlebar.tsx | 2 +- .../controls/CaptionControlsMacOS.tsx | 2 +- .../controls/CaptionControlsWindows11.tsx | 2 +- apps/desktop/src/electron/app.ts | 8 + apps/desktop/src/electron/bridge.ts | 44 + apps/desktop/src/electron/clipboard.ts | 6 + apps/desktop/src/electron/core.ts | 50 + apps/desktop/src/electron/deep-link.ts | 7 + apps/desktop/src/electron/dialog.ts | 27 + apps/desktop/src/electron/dpi.ts | 30 + apps/desktop/src/electron/event.ts | 37 + apps/desktop/src/electron/fs.ts | 28 + apps/desktop/src/electron/http.ts | 1 + apps/desktop/src/electron/menu.ts | 103 + apps/desktop/src/electron/notification.ts | 19 + apps/desktop/src/electron/opener.ts | 4 + apps/desktop/src/electron/os.ts | 14 + apps/desktop/src/electron/path.ts | 37 + apps/desktop/src/electron/process.ts | 3 + apps/desktop/src/electron/shell.ts | 14 + apps/desktop/src/electron/store.ts | 38 + apps/desktop/src/electron/updater.ts | 14 + apps/desktop/src/electron/webviewWindow.ts | 65 + apps/desktop/src/electron/window.ts | 466 +++++ apps/desktop/src/entry-client.tsx | 2 +- apps/desktop/src/routes/(window-chrome).tsx | 6 +- .../(window-chrome)/new-main/CameraSelect.tsx | 2 +- .../new-main/ChangeLogButton.tsx | 4 +- .../new-main/MicrophoneSelect.tsx | 2 +- .../(window-chrome)/new-main/Recents.tsx | 2 +- .../(window-chrome)/new-main/TargetCard.tsx | 8 +- .../routes/(window-chrome)/new-main/index.tsx | 14 +- .../new-main/useRequestPermission.ts | 2 +- .../src/routes/(window-chrome)/onboarding.tsx | 10 +- .../src/routes/(window-chrome)/settings.tsx | 10 +- .../(window-chrome)/settings/automations.tsx | 4 +- .../routes/(window-chrome)/settings/cli.tsx | 4 +- .../(window-chrome)/settings/experimental.tsx | 4 +- .../(window-chrome)/settings/feedback.tsx | 6 +- .../(window-chrome)/settings/general.tsx | 8 +- .../(window-chrome)/settings/hotkeys.tsx | 2 +- .../(window-chrome)/settings/recordings.tsx | 8 +- .../(window-chrome)/settings/screenshots.tsx | 6 +- .../src/routes/(window-chrome)/update.tsx | 4 +- .../src/routes/(window-chrome)/upgrade.tsx | 2 +- apps/desktop/src/routes/camera.tsx | 6 +- apps/desktop/src/routes/capture-area.tsx | 8 +- apps/desktop/src/routes/debug.tsx | 6 +- .../src/routes/editor/AudioLibrary.tsx | 2 +- .../desktop/src/routes/editor/CaptionsTab.tsx | 4 +- .../src/routes/editor/ClipsSidebar.tsx | 14 +- .../src/routes/editor/ConfigSidebar.tsx | 8 +- apps/desktop/src/routes/editor/Editor.tsx | 10 +- .../src/routes/editor/EditorErrorScreen.tsx | 4 +- apps/desktop/src/routes/editor/ExportPage.tsx | 10 +- apps/desktop/src/routes/editor/Header.tsx | 8 +- .../src/routes/editor/ImportProgress.tsx | 2 +- .../src/routes/editor/PerformanceOverlay.tsx | 2 +- apps/desktop/src/routes/editor/Player.tsx | 4 +- .../desktop/src/routes/editor/ShareButton.tsx | 2 +- .../src/routes/editor/Timeline/ZoomTrack.tsx | 2 +- .../src/routes/editor/Timeline/index.tsx | 6 +- .../src/routes/editor/TranscriptPage.tsx | 4 +- apps/desktop/src/routes/editor/captions.ts | 4 +- .../src/routes/editor/editor-skeleton.tsx | 2 +- .../editor/existing-recording-picker.ts | 4 +- apps/desktop/src/routes/editor/index.tsx | 4 +- apps/desktop/src/routes/editor/utils.ts | 2 +- .../src/routes/in-progress-recording.tsx | 10 +- apps/desktop/src/routes/mode-select.tsx | 6 +- .../desktop/src/routes/recordings-overlay.tsx | 2 +- .../src/routes/screenshot-editor/Editor.tsx | 6 +- .../src/routes/screenshot-editor/Header.tsx | 10 +- .../screenshot-editor/OcrSelectionOverlay.tsx | 2 +- .../src/routes/screenshot-editor/context.tsx | 2 +- .../src/routes/screenshot-editor/index.tsx | 4 +- .../popovers/BackgroundSettingsPopover.tsx | 6 +- .../screenshot-editor-skeleton.tsx | 2 +- .../screenshot-editor/useScreenshotExport.ts | 4 +- .../src/routes/target-select-overlay.tsx | 10 +- apps/desktop/src/routes/teleprompter.tsx | 6 +- .../src/routes/window-capture-occluder.tsx | 2 +- apps/desktop/src/store.ts | 2 +- apps/desktop/src/styles/theme.css | 18 + apps/desktop/src/utils/analytics.ts | 2 +- apps/desktop/src/utils/auth.ts | 10 +- apps/desktop/src/utils/camera-window.ts | 2 +- apps/desktop/src/utils/export.ts | 2 +- apps/desktop/src/utils/importMedia.ts | 6 +- .../src/utils/macos-window-material.ts | 8 +- apps/desktop/src/utils/queries.ts | 2 +- apps/desktop/src/utils/recording.ts | 6 +- apps/desktop/src/utils/tauri.ts | 6 +- apps/desktop/src/utils/tauriSpectaHack.ts | 4 +- .../src/utils/teleprompter-window-options.ts | 6 +- apps/desktop/src/utils/teleprompter.test.ts | 4 +- apps/desktop/src/utils/teleprompter.ts | 4 +- apps/desktop/src/utils/titlebar-state.ts | 6 +- apps/desktop/src/utils/updater.ts | 4 +- apps/desktop/src/utils/web-api.ts | 2 +- apps/desktop/tsconfig.json | 2 +- package.json | 3 +- pnpm-lock.yaml | 1750 +++++++++++++---- pnpm-workspace.yaml | 1 - scripts/build-desktop-binaries.mjs | 23 +- scripts/build-desktop-binaries.sh | 7 +- scripts/check-tauri-plugin-versions.js | 337 ---- scripts/setup.js | 25 - scripts/verify-recording-pipeline.mjs | 2 +- turbo.json | 6 +- 126 files changed, 4260 insertions(+), 1029 deletions(-) create mode 100644 apps/desktop/electron-builder.yml create mode 100644 apps/desktop/electron/backend.cjs create mode 100644 apps/desktop/electron/main.cjs create mode 100644 apps/desktop/electron/native.cjs create mode 100644 apps/desktop/electron/preload.cjs create mode 100644 apps/desktop/electron/windows.cjs create mode 100644 apps/desktop/scripts/build-electron.mjs create mode 100644 apps/desktop/src/electron/app.ts create mode 100644 apps/desktop/src/electron/bridge.ts create mode 100644 apps/desktop/src/electron/clipboard.ts create mode 100644 apps/desktop/src/electron/core.ts create mode 100644 apps/desktop/src/electron/deep-link.ts create mode 100644 apps/desktop/src/electron/dialog.ts create mode 100644 apps/desktop/src/electron/dpi.ts create mode 100644 apps/desktop/src/electron/event.ts create mode 100644 apps/desktop/src/electron/fs.ts create mode 100644 apps/desktop/src/electron/http.ts create mode 100644 apps/desktop/src/electron/menu.ts create mode 100644 apps/desktop/src/electron/notification.ts create mode 100644 apps/desktop/src/electron/opener.ts create mode 100644 apps/desktop/src/electron/os.ts create mode 100644 apps/desktop/src/electron/path.ts create mode 100644 apps/desktop/src/electron/process.ts create mode 100644 apps/desktop/src/electron/shell.ts create mode 100644 apps/desktop/src/electron/store.ts create mode 100644 apps/desktop/src/electron/updater.ts create mode 100644 apps/desktop/src/electron/webviewWindow.ts create mode 100644 apps/desktop/src/electron/window.ts delete mode 100755 scripts/check-tauri-plugin-versions.js diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore index 76add878f8d..f5d6d05d16b 100644 --- a/apps/desktop/.gitignore +++ b/apps/desktop/.gitignore @@ -1,2 +1,4 @@ node_modules -dist \ No newline at end of file +dist +.electron-dist +dist-electron diff --git a/apps/desktop/app.config.ts b/apps/desktop/app.config.ts index f7caac6cabc..b48bbc4747e 100644 --- a/apps/desktop/app.config.ts +++ b/apps/desktop/app.config.ts @@ -34,22 +34,18 @@ export default defineConfig({ server: { preset: "static" }, // https://vitejs.dev/config vite: () => ({ - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // 1. tauri expects a fixed port, fail if that port is not available server: { port: 3001, strictPort: true, watch: { - ignored: ["**/src-tauri/**"], + ignored: ["**/electron/**", "**/src-backend/**"], }, headers: { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }, }, - // 3. to make use of `TAURI_DEBUG` and other env variables - // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand - envPrefix: ["VITE_", "TAURI_"], + envPrefix: ["VITE_", "CAP_"], assetsInclude: ["**/*.riv"], plugins: [ ...(enableSolidDevtools ? [devtools({ autoname: true })] : []), @@ -66,17 +62,9 @@ export default defineConfig({ }, optimizeDeps: { include: [ - "@tauri-apps/plugin-os", "@tanstack/solid-query", - "@tauri-apps/api/webviewWindow", - "@tauri-apps/plugin-dialog", - "@tauri-apps/plugin-store", "@openpanel/web", "uuid", - "@tauri-apps/plugin-clipboard-manager", - "@tauri-apps/api/window", - "@tauri-apps/api/core", - "@tauri-apps/api/event", "cva", ], }, diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml new file mode 100644 index 00000000000..22ae072808d --- /dev/null +++ b/apps/desktop/electron-builder.yml @@ -0,0 +1,109 @@ +appId: so.cap.desktop +productName: Cap +asar: true +generateUpdatesFilesForAllChannels: true +extraMetadata: + main: .electron-dist/main.cjs +directories: + output: dist-electron +files: + - .electron-dist/** + - .output/public/** + - package.json + - '!node_modules/**/*' +extraResources: + - from: src-backend/assets + to: assets + - from: src-backend/icons + to: tray-icons + filter: + - tray-*.png + - from: ../../target/native-deps/onnxruntime + to: onnxruntime +mac: + icon: src-backend/icons/macos/icon.icns + category: public.app-category.productivity + hardenedRuntime: true + notarize: true + entitlements: src-backend/Entitlements.plist + entitlementsInherit: src-backend/Entitlements.plist + target: + - dmg + - zip + extraFiles: + - from: src-backend/binaries + to: MacOS + filter: + - '!**/.gitignore' + - from: ../../target/native-deps/Spacedrive.framework + to: Frameworks/Spacedrive.framework + extendInfo: + NSCameraUsageDescription: Cap uses your camera to record you in your Caps + NSMicrophoneUsageDescription: Cap uses your microphone to record you in your Caps + NSScreenCaptureUsageDescription: Cap captures your screen as part of recording your Caps + NSCameraUseContinuityCameraDeviceType: true + CFBundleURLTypes: + - CFBundleURLName: Cap Desktop + CFBundleURLSchemes: + - cap-desktop + CFBundleDocumentTypes: + - CFBundleTypeName: Cap Project + CFBundleTypeExtensions: + - cap + CFBundleTypeRole: Editor + LSHandlerRank: Default + LSTypeIsPackage: true +dmg: + background: src-backend/assets/dmg-background.png + contents: + - x: 180 + y: 140 + - x: 480 + y: 140 + type: link + path: /Applications +win: + icon: src-backend/icons/icon.ico + extraResources: + - from: src-backend/binaries + to: bin + filter: + - '!**/.gitignore' + target: + - nsis +nsis: + installerIcon: src-backend/icons/icon.ico + installerHeader: src-backend/assets/nsis-header.bmp + installerSidebar: src-backend/assets/nsis-sidebar.bmp +linux: + icon: src-backend/icons + category: Utility + extraResources: + - from: src-backend/binaries + to: bin + filter: + - '!**/.gitignore' + - from: ../../target/native-deps/cap-deb-libs + to: lib/cap + target: + - deb +deb: + depends: + - libgtk-3-0 + - libayatana-appindicator3-1 + - libva2 + - libva-drm2 + - libpipewire-0.3-0 + - libasound2t64 | libasound2 +protocols: + - name: Cap Desktop + schemes: + - cap-desktop +fileAssociations: + - ext: cap + name: Cap Project + role: Editor +publish: + provider: github + owner: CapSoftware + repo: Cap diff --git a/apps/desktop/electron/backend.cjs b/apps/desktop/electron/backend.cjs new file mode 100644 index 00000000000..fd893571e9c --- /dev/null +++ b/apps/desktop/electron/backend.cjs @@ -0,0 +1,178 @@ +const crypto = require("node:crypto"); +const net = require("node:net"); + +const PROTOCOL_VERSION = 1; +const MAX_FRAME_SIZE = 128 * 1024 * 1024; + +class RustBackend { + constructor({ binaryPath, resourceDir, spawn }) { + this.binaryPath = binaryPath; + this.resourceDir = resourceDir; + this.spawn = spawn; + this.buffer = Buffer.alloc(0); + this.nextInvokeId = 1; + this.pending = new Map(); + this.listeners = new Set(); + this.startupMessages = []; + this.ready = new Promise((resolve, reject) => { + this.resolveReady = resolve; + this.rejectReady = reject; + }); + } + + async start() { + const token = crypto.randomBytes(32).toString("hex"); + this.server = net.createServer((socket) => this.accept(socket, token)); + await new Promise((resolve, reject) => { + this.server.once("error", reject); + this.server.listen(0, "127.0.0.1", resolve); + }); + const address = this.server.address(); + if (!address || typeof address === "string") throw new Error("Invalid backend address"); + this.child = this.spawn(this.binaryPath, [], { + cwd: this.resourceDir, + env: { + ...process.env, + CAP_ELECTRON_IPC_ADDR: `127.0.0.1:${address.port}`, + CAP_ELECTRON_IPC_TOKEN: token, + CAP_ELECTRON_RESOURCE_DIR: this.resourceDir, + CAP_ELECTRON_APP_DATA_DIR: require("electron").app.getPath("userData"), + }, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + this.child.stdout.on("data", (data) => process.stdout.write(data)); + this.child.stderr.on("data", (data) => process.stderr.write(data)); + this.child.once("error", (error) => this.fail(error)); + this.child.once("exit", (code, signal) => { + if (!this.stopping) this.fail(new Error(`Rust backend exited (${code ?? signal})`)); + }); + return this.ready; + } + + accept(socket, token) { + if (this.socket) { + socket.destroy(); + return; + } + this.socket = socket; + this.server.close(); + socket.setNoDelay(true); + socket.on("data", (data) => this.read(data)); + socket.once("error", (error) => this.fail(error)); + socket.once("close", () => { + if (!this.stopping) this.fail(new Error("Rust backend connection closed")); + }); + this.send({ type: "hello", token, protocolVersion: PROTOCOL_VERSION }); + } + + read(data) { + this.buffer = Buffer.concat([this.buffer, data]); + while (this.buffer.length >= 4) { + const length = this.buffer.readUInt32BE(0); + if (length > MAX_FRAME_SIZE) { + this.fail(new Error(`Rust backend frame exceeded ${MAX_FRAME_SIZE} bytes`)); + return; + } + if (this.buffer.length < length + 4) return; + const payload = this.buffer.subarray(4, length + 4); + this.buffer = this.buffer.subarray(length + 4); + try { + this.receive(JSON.parse(payload.toString("utf8"))); + } catch (error) { + this.fail(error); + return; + } + } + } + + receive(message) { + if (!this.commands && message.type !== "ready") this.startupMessages.push(message); + if (message.type === "ready") { + if (message.protocolVersion !== PROTOCOL_VERSION) { + this.fail(new Error("Rust backend protocol version mismatch")); + return; + } + this.commands = new Set(message.commands); + this.resolveReady(); + } + if (message.type === "invokeResult") { + const pending = this.pending.get(message.id); + if (!pending) return; + this.pending.delete(message.id); + if (message.response.status === "ok") pending.resolve(message.response.value); + else pending.reject(new Error(message.response.error)); + } + for (const listener of this.listeners) listener(message); + } + + invoke(windowLabel, command, arguments_) { + return this.ready.then( + () => + new Promise((resolve, reject) => { + const id = this.nextInvokeId++; + this.pending.set(id, { resolve, reject }); + const sent = this.send({ + type: "invoke", + id, + windowLabel, + command, + arguments: arguments_ ?? {}, + }); + if (!sent) { + this.pending.delete(id); + reject(this.failure ?? new Error("Rust backend is disconnected")); + } + }), + ); + } + + send(message) { + if (!this.socket || this.socket.destroyed || this.failure) return false; + const payload = Buffer.from(JSON.stringify(message)); + if (payload.length > MAX_FRAME_SIZE) { + throw new Error(`Rust backend frame exceeded ${MAX_FRAME_SIZE} bytes`); + } + const header = Buffer.allocUnsafe(4); + header.writeUInt32BE(payload.length); + this.socket.write(Buffer.concat([header, payload])); + return true; + } + + onMessage(listener) { + this.listeners.add(listener); + for (const message of this.startupMessages) listener(message); + return () => this.listeners.delete(listener); + } + + async stop() { + this.stopping = true; + if (this.socket && !this.socket.destroyed) this.send({ type: "shutdown" }); + if (this.child && this.child.exitCode === null) { + await Promise.race([ + new Promise((resolve) => this.child.once("exit", resolve)), + new Promise((resolve) => setTimeout(resolve, 3000)), + ]); + } + if (this.child && this.child.exitCode === null) this.child.kill(); + this.socket?.destroy(); + this.server?.close(); + } + + fail(error) { + if (this.failure || this.stopping) return; + this.failure = error instanceof Error ? error : new Error(String(error)); + this.rejectReady(error); + for (const pending of this.pending.values()) pending.reject(this.failure); + this.pending.clear(); + for (const listener of this.listeners) { + try { + listener({ type: "backendError", error: String(this.failure) }); + } catch (listenerError) { + console.error(listenerError); + } + } + } +} + +module.exports = { RustBackend }; diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs new file mode 100644 index 00000000000..d7e6a870304 --- /dev/null +++ b/apps/desktop/electron/main.cjs @@ -0,0 +1,211 @@ +const { app, ipcMain, net: electronNet, protocol, session } = require("electron"); +const { spawn } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const { pathToFileURL } = require("node:url"); + +const { RustBackend } = require("./backend.cjs"); +const { NativeBridge } = require("./native.cjs"); +const { WindowManager } = require("./windows.cjs"); + +protocol.registerSchemesAsPrivileged([ + { scheme: "cap", privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: false } }, + { scheme: "cap-asset", privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true } }, +]); + +const development = !app.isPackaged; +let backend; +let quitting = false; +const pendingOpenUrls = []; + +app.setName(development ? "Cap - Development" : "Cap"); +app.setAppUserModelId(development ? "so.cap.desktop.dev" : "so.cap.desktop"); +app.setPath( + "userData", + process.env.CAP_ELECTRON_USER_DATA_DIR + ? path.resolve(process.env.CAP_ELECTRON_USER_DATA_DIR) + : path.join(app.getPath("appData"), development ? "Cap Development" : "so.cap.desktop"), +); +if (!development) app.setAsDefaultProtocolClient("cap-desktop"); + +const hasSingleInstanceLock = app.requestSingleInstanceLock(); +if (!hasSingleInstanceLock) app.quit(); + +if (hasSingleInstanceLock) { + app.whenReady().then(start).catch((error) => { + console.error(error); + app.exit(1); + }); +} + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); + +app.on("activate", () => { + const mainWindow = globalThis.capWindowManager?.get("main"); + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + backend?.send({ type: "event", event: "electron://reopen", payload: null }); +}); + +app.on("second-instance", (_event, commandLine, workingDirectory) => { + queueOpenUrls(commandLine, workingDirectory); + const mainWindow = globalThis.capWindowManager?.get("main"); + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); + } +}); + +app.on("open-url", (event, url) => { + event.preventDefault(); + queueOpenUrls([url]); +}); + +app.on("open-file", (event, filePath) => { + event.preventDefault(); + queueOpenUrls([filePath]); +}); + +app.on("before-quit", (event) => { + if (quitting || !backend) return; + event.preventDefault(); + quitting = true; + globalThis.capWindowManager?.beginQuit(); + backend.stop().finally(() => app.quit()); +}); + +async function start() { + const publicDir = path.join(__dirname, "..", ".output", "public"); + registerProtocols(publicDir); + configureSecurity(); + + const backendFilename = `cap-desktop-${rustTargetTriple()}${process.platform === "win32" ? ".exe" : ""}`; + const binaryPath = development + ? path.resolve(__dirname, "../../../target/debug/cap-desktop") + : process.platform === "darwin" + ? path.join(path.dirname(process.execPath), backendFilename) + : path.join(process.resourcesPath, "bin", backendFilename); + if (!fs.existsSync(binaryPath)) { + throw new Error(`Rust backend is missing at ${binaryPath}. Run pnpm build:backend first.`); + } + backend = new RustBackend({ binaryPath, resourceDir: process.resourcesPath, spawn }); + await backend.start(); + + const windowManager = new WindowManager({ + backend, + devUrl: development ? process.env.CAP_DESKTOP_DEV_URL ?? "http://localhost:3002" : null, + publicDir, + preloadPath: path.join(__dirname, "preload.cjs"), + }); + globalThis.capWindowManager = windowManager; + const nativeBridge = new NativeBridge(windowManager); + backend.onMessage((message) => { + if (message.type === "backendError") { + console.error(message.error); + if (!quitting) app.quit(); + return; + } + if (message.type === "nativeOperation") { + void nativeBridge.invoke("main", message.operation, message.payload).catch(console.error); + } + if (message.type === "nativeRequest") { + void nativeBridge.invoke("main", message.operation, message.payload).then( + (value) => backend.send({ type: "nativeResult", id: message.id, result: { Ok: value ?? null } }), + (error) => backend.send({ type: "nativeResult", id: message.id, result: { Err: String(error) } }), + ); + } + }); + + ipcMain.handle("cap:invoke", (_event, request) => + backend.invoke(request.windowLabel, request.command, request.arguments), + ); + ipcMain.handle("cap:native", (_event, request) => + nativeBridge.invoke(request.windowLabel, request.operation, request.payload), + ); + ipcMain.on("cap:emit", (_event, request) => { + backend.send({ type: "event", event: request.event, payload: request.payload ?? null }); + }); + ipcMain.on("cap:window-drop", (_event, request) => { + windowManager.sendWindowEvent(request.windowLabel, { type: "dragDrop", paths: request.paths }); + }); + globalThis.__capMenuSelection = (id) => { + for (const window of windowManager.windows.values()) { + window.webContents.send("cap:event", { type: "event", event: `menu:${id}`, payload: null }); + } + }; + queueOpenUrls(process.argv, process.cwd()); +} + +function queueOpenUrls(values, workingDirectory = process.cwd()) { + for (const value of values) { + if (typeof value !== "string" || value.startsWith("--")) continue; + if (value.startsWith("cap-desktop://") || value.startsWith("file://")) { + pendingOpenUrls.push(value); + continue; + } + const candidate = path.isAbsolute(value) ? value : path.resolve(workingDirectory, value); + if (path.extname(candidate).toLowerCase() === ".cap" && fs.existsSync(candidate)) { + pendingOpenUrls.push(pathToFileURL(candidate).toString()); + } + } + if (backend?.socket && pendingOpenUrls.length > 0) { + backend.send({ type: "event", event: "electron://open-urls", payload: pendingOpenUrls.splice(0) }); + } +} + +function rustTargetTriple() { + const triple = { + "darwin-arm64": "aarch64-apple-darwin", + "darwin-x64": "x86_64-apple-darwin", + "win32-arm64": "aarch64-pc-windows-msvc", + "win32-x64": "x86_64-pc-windows-msvc", + "linux-arm64": "aarch64-unknown-linux-gnu", + "linux-x64": "x86_64-unknown-linux-gnu", + }[`${process.platform}-${process.arch}`]; + if (!triple) throw new Error(`Unsupported Electron target ${process.platform}-${process.arch}`); + return triple; +} + +function registerProtocols(publicDir) { + protocol.handle("cap", (request) => { + const url = new URL(request.url); + let relativePath = decodeURIComponent(url.pathname).replace(/^\/+/, ""); + if (!relativePath || !path.extname(relativePath)) relativePath = "index.html"; + const filePath = path.resolve(publicDir, relativePath); + if (!filePath.startsWith(`${path.resolve(publicDir)}${path.sep}`) && filePath !== path.join(publicDir, "index.html")) { + return new Response("Not found", { status: 404 }); + } + return electronNet.fetch(pathToFileURL(filePath).toString()); + }); + protocol.handle("cap-asset", (request) => { + const url = new URL(request.url); + try { + const encoded = url.pathname.replace(/^\/+/, ""); + const filePath = Buffer.from(encoded, "base64url").toString("utf8"); + if (!path.isAbsolute(filePath)) return new Response("Invalid asset path", { status: 400 }); + return electronNet.fetch(pathToFileURL(filePath).toString()); + } catch { + return new Response("Invalid asset URL", { status: 400 }); + } + }); +} + +function configureSecurity() { + session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => { + callback(["media", "notifications", "clipboard-sanitized-write"].includes(permission)); + }); + app.on("web-contents-created", (_event, contents) => { + contents.setWindowOpenHandler(({ url }) => { + require("electron").shell.openExternal(url); + return { action: "deny" }; + }); + contents.on("will-navigate", (event, url) => { + if (!url.startsWith("cap://") && !url.startsWith("http://localhost:3002")) event.preventDefault(); + }); + }); +} diff --git a/apps/desktop/electron/native.cjs b/apps/desktop/electron/native.cjs new file mode 100644 index 00000000000..8aa32fc4191 --- /dev/null +++ b/apps/desktop/electron/native.cjs @@ -0,0 +1,419 @@ +const { app, clipboard, dialog, globalShortcut, Menu, nativeImage, nativeTheme, Notification, screen, shell, Tray } = require("electron"); +const fs = require("node:fs/promises"); +const os = require("node:os"); +const path = require("node:path"); +const { autoUpdater } = require("electron-updater"); + +class NativeBridge { + constructor(windowManager) { + this.windowManager = windowManager; + this.stores = new Map(); + this.hotkeyAccelerators = new Set(); + this.escapeRegistered = false; + this.tray = null; + this.trayMode = "studio"; + this.trayRecording = false; + this.updateChannel = "stable"; + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.on("download-progress", (progress) => { + this.windowManager.backend.send({ + type: "event", + event: "update-download-progress", + payload: { downloaded: Math.min(progress.transferred, 0xffffffff), total: Math.min(progress.total, 0xffffffff) }, + }); + }); + autoUpdater.on("update-downloaded", (info) => { + this.windowManager.backend.send({ + type: "event", + event: "update-ready", + payload: { version: info.version, installed: true }, + }); + }); + } + + async invoke(windowLabel, operation, payload = {}) { + const window = this.windowManager.get(windowLabel); + switch (operation) { + case "app.exit": this.windowManager.beginQuit(); app.exit(payload.exitCode ?? 0); return; + case "app.dockVisibility": if (app.dock) payload.visible ? app.dock.show() : app.dock.hide(); return; + case "app.activationPolicy": if (process.platform === "darwin") app.setActivationPolicy(payload.policy); return; + case "app.version": return app.getVersion(); + case "app.name": return app.getName(); + case "app.relaunch": app.relaunch(); app.exit(0); return; + case "clipboard.readText": return clipboard.readText(); + case "clipboard.writeText": clipboard.writeText(payload.text); return; + case "dialog.open": return this.openDialog(window, payload); + case "dialog.save": return this.saveDialog(window, payload); + case "dialog.message": return this.messageDialog(window, payload); + case "dialog.ask": return this.questionDialog(window, payload, false); + case "dialog.confirm": return this.questionDialog(window, payload, true); + case "fs.exists": return this.exists(resolveBaseDirectory(payload.path, payload.baseDir)); + case "fs.readFile": return Array.from(await fs.readFile(resolveBaseDirectory(payload.path, payload.baseDir))); + case "fs.readTextFile": return fs.readFile(resolveBaseDirectory(payload.path, payload.baseDir), "utf8"); + case "fs.writeFile": await writeFile(payload); return; + case "fs.writeTextFile": await writeTextFile(payload); return; + case "fs.remove": await fs.rm(resolveBaseDirectory(payload.path, payload.baseDir), { recursive: payload.recursive ?? false, force: true }); return; + case "fs.mkdir": await fs.mkdir(resolveBaseDirectory(payload.path, payload.baseDir), { recursive: payload.recursive ?? false }); return; + case "fs.readDir": return readDir(resolveBaseDirectory(payload.path, payload.baseDir)); + case "fs.stat": return stat(resolveBaseDirectory(payload.path, payload.baseDir)); + case "hotkeys.configure": return this.configureHotkeys(payload.hotkeys ?? []); + case "hotkeys.escapeEnabled": return this.setEscapeEnabled(payload.enabled); + case "menu.popup": return this.popupMenu(window, payload.items, payload.position); + case "notification.permission": return Notification.isSupported() ? "granted" : "denied"; + case "notification.requestPermission": return Notification.isSupported() ? "granted" : "denied"; + case "notification.show": new Notification(payload).show(); return; + case "opener.openPath": return shell.openPath(payload.path); + case "opener.revealItemInDir": shell.showItemInFolder(payload.path); return; + case "os.arch": return normalizeArch(process.arch); + case "os.hostname": return os.hostname(); + case "os.locale": return app.getLocale(); + case "os.platform": return process.platform; + case "os.type": return normalizeType(process.platform); + case "os.version": return os.release(); + case "path.appDataDir": return withSeparator(app.getPath("userData")); + case "path.appLocalDataDir": return withSeparator(app.getPath("userData")); + case "path.audioDir": return withSeparator(app.getPath("music")); + case "path.cacheDir": return withSeparator(app.getPath("sessionData")); + case "path.configDir": return withSeparator(app.getPath("userData")); + case "path.dataDir": return withSeparator(app.getPath("appData")); + case "path.desktopDir": return withSeparator(app.getPath("desktop")); + case "path.documentDir": return withSeparator(app.getPath("documents")); + case "path.downloadDir": return withSeparator(app.getPath("downloads")); + case "path.homeDir": return withSeparator(app.getPath("home")); + case "path.pictureDir": return withSeparator(app.getPath("pictures")); + case "path.resourceDir": return withSeparator(process.resourcesPath); + case "path.tempDir": return withSeparator(app.getPath("temp")); + case "path.videoDir": return withSeparator(app.getPath("videos")); + case "path.join": return path.join(...payload.paths); + case "path.resolve": return path.resolve(...payload.paths); + case "shell.open": await shell.openExternal(payload.path); return; + case "shell.openPath": return shell.openPath(payload.path); + case "store.load": return this.loadStore(payload.path); + case "store.save": return this.saveStore(payload.path, payload.value); + case "theme.get": return nativeTheme.shouldUseDarkColors ? "dark" : "light"; + case "tray.configure": return this.configureTray(payload); + case "tray.setMode": this.trayMode = payload.mode ?? "studio"; return this.updateTrayIcon(); + case "tray.setRecording": this.trayRecording = payload.recording === true; return this.updateTrayIcon(); + case "updater.check": return this.checkForUpdates(payload.channel); + case "updater.configure": return this.configureUpdater(payload.channel); + case "updater.downloadAndInstall": return this.downloadAndInstall(payload.channel); + case "updater.setChannel": this.updateChannel = payload.channel ?? "stable"; return this.configureUpdater(this.updateChannel); + case "window.action": return this.windowAction(windowLabel, payload); + case "window.all": return [...this.windowManager.windows.keys()]; + case "window.create": this.windowManager.create(payload); return payload.label; + case "window.currentMonitor": return this.windowManager.normalizeDisplay(window ? screen.getDisplayMatching(window.getBounds()) : screen.getPrimaryDisplay()); + case "window.primaryMonitor": return this.windowManager.normalizeDisplay(screen.getPrimaryDisplay()); + case "window.availableMonitors": return screen.getAllDisplays().map((display) => this.windowManager.normalizeDisplay(display)); + case "window.monitorFromPoint": return this.windowManager.normalizeDisplay(this.windowManager.displayFromPhysicalPoint(payload)); + case "window.teardownVisualEffects": return; + default: throw new Error(`Unknown Electron native operation '${operation}'`); + } + } + + async openDialog(window, options) { + const result = await dialog.showOpenDialog(window, { + title: options.title, + defaultPath: options.defaultPath, + buttonLabel: options.buttonLabel, + filters: normalizeFilters(options.filters), + properties: [ + options.directory ? "openDirectory" : "openFile", + ...(options.multiple ? ["multiSelections"] : []), + ...(options.recursive ? ["showHiddenFiles"] : []), + ], + }); + if (result.canceled) return null; + return options.multiple ? result.filePaths : result.filePaths[0]; + } + + async saveDialog(window, options) { + const result = await dialog.showSaveDialog(window, { + title: options.title, + defaultPath: options.defaultPath, + buttonLabel: options.buttonLabel, + filters: normalizeFilters(options.filters), + }); + return result.canceled ? null : result.filePath; + } + + async messageDialog(window, options) { + const normalized = typeof options === "string" ? { message: options } : options; + const buttons = Array.isArray(normalized.buttons) + ? normalized.buttons + : normalized.buttons + ? Object.values(normalized.buttons).filter(Boolean) + : [normalized.okLabel ?? "OK"]; + const result = await dialog.showMessageBox(window, { + type: normalized.kind ?? "info", + title: normalized.title, + message: normalized.message, + detail: normalized.detail, + buttons, + }); + return buttons[result.response]; + } + + async questionDialog(window, options, isConfirm) { + const normalized = typeof options === "string" ? { message: options } : options; + const result = await dialog.showMessageBox(window, { + type: normalized.kind ?? "question", + title: normalized.title, + message: normalized.message, + detail: normalized.detail, + buttons: [normalized.okLabel ?? (isConfirm ? "OK" : "Yes"), normalized.cancelLabel ?? (isConfirm ? "Cancel" : "No")], + defaultId: 0, + cancelId: 1, + }); + return result.response === 0; + } + + async exists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + } + + popupMenu(window, items, position) { + const menu = Menu.buildFromTemplate(items.map(normalizeMenuItem)); + menu.popup({ + window, + x: position ? Math.round(position.x) : undefined, + y: position ? Math.round(position.y) : undefined, + }); + } + + async loadStore(name) { + const storePath = this.storePath(name); + if (this.stores.has(storePath)) return this.stores.get(storePath); + let value = {}; + try { + value = JSON.parse(await fs.readFile(storePath, "utf8")); + } catch {} + this.stores.set(storePath, value); + return value; + } + + async saveStore(name, value) { + const storePath = this.storePath(name); + this.stores.set(storePath, value); + await fs.mkdir(path.dirname(storePath), { recursive: true }); + await fs.writeFile(storePath, JSON.stringify(value, null, 2)); + } + + storePath(name) { + return path.join(app.getPath("userData"), "stores", path.basename(name)); + } + + windowAction(label, payload) { + const window = this.windowManager.get(payload.label ?? label); + if (!window || window.isDestroyed()) throw new Error(`Window '${payload.label ?? label}' does not exist`); + if (payload.action === "state") return this.windowManager.windowState(window); + if (payload.action === "cursorPosition") return this.windowManager.cursorPosition?.() ?? { x: 0, y: 0 }; + this.windowManager.operation(window, { type: payload.action, ...payload.value }); + } + + configureHotkeys(hotkeys) { + for (const accelerator of this.hotkeyAccelerators) globalShortcut.unregister(accelerator); + this.hotkeyAccelerators.clear(); + for (const hotkey of hotkeys) { + if (!hotkey.accelerator || this.hotkeyAccelerators.has(hotkey.accelerator)) continue; + if (globalShortcut.register(hotkey.accelerator, () => { + this.windowManager.backend.send({ + type: "event", + event: "hotkey://trigger", + payload: hotkey.action, + }); + })) this.hotkeyAccelerators.add(hotkey.accelerator); + } + const settingsAccelerator = "CommandOrControl+,"; + if (!this.hotkeyAccelerators.has(settingsAccelerator) && globalShortcut.register(settingsAccelerator, () => { + this.windowManager.backend.send({ type: "event", event: "hotkey://settings", payload: null }); + })) this.hotkeyAccelerators.add(settingsAccelerator); + } + + setEscapeEnabled(enabled) { + if (enabled && !this.escapeRegistered) { + this.escapeRegistered = globalShortcut.register("Escape", () => { + this.windowManager.backend.send({ type: "event", event: "hotkey://escape", payload: null }); + }); + } else if (!enabled && this.escapeRegistered) { + globalShortcut.unregister("Escape"); + this.escapeRegistered = false; + } + } + + configureTray(payload) { + this.trayMode = payload.mode ?? this.trayMode; + if (!this.tray) { + this.tray = new Tray(this.trayIconPath()); + this.tray.setToolTip("Cap"); + this.tray.on("click", () => { + if (this.trayRecording) this.emitTrayClick("stop_recording"); + else this.tray.popUpContextMenu(); + }); + } + this.tray.setContextMenu(Menu.buildFromTemplate((payload.items ?? []).map((item) => this.trayMenuItem(item)))); + this.updateTrayIcon(); + } + + trayMenuItem(item) { + if (item.type === "separator") return { type: "separator" }; + return { + id: item.id, + label: item.text, + type: item.checked === undefined ? "normal" : "checkbox", + checked: item.checked, + enabled: item.enabled ?? true, + submenu: item.items?.map((child) => this.trayMenuItem(child)), + click: () => item.id && this.emitTrayClick(item.id), + }; + } + + emitTrayClick(id) { + this.windowManager.backend.send({ type: "event", event: "tray://click", payload: id }); + } + + updateTrayIcon() { + if (!this.tray) return; + const image = nativeImage.createFromPath(this.trayIconPath()); + if (process.platform === "darwin") image.setTemplateImage(true); + this.tray.setImage(image); + } + + trayIconPath() { + const filename = this.trayRecording + ? "tray-stop-icon.png" + : this.trayMode === "instant" + ? "tray-default-icon-instant.png" + : this.trayMode === "screenshot" + ? "tray-default-icon-screenshot.png" + : "tray-default-icon.png"; + const iconsDir = app.isPackaged + ? path.join(process.resourcesPath, "tray-icons") + : path.join(__dirname, "..", "src-backend", "icons"); + return path.join(iconsDir, filename); + } + + configureUpdater(channel = "stable") { + this.updateChannel = channel; + this.setUpdaterFeed(channel); + if (this.updateTimer) clearInterval(this.updateTimer); + if (!app.isPackaged || channel !== "nightly") return; + this.updateTimer = setInterval(() => void this.checkForUpdates(channel).catch(console.error), 2 * 60 * 60 * 1000); + } + + setUpdaterFeed(channel) { + if (!app.isPackaged) return; + autoUpdater.channel = channel === "nightly" ? "nightly" : "latest"; + const baseUrl = process.env.CAP_ELECTRON_UPDATE_URL; + if (baseUrl) autoUpdater.setFeedURL({ provider: "generic", url: `${baseUrl}/${channel}` }); + else autoUpdater.setFeedURL({ provider: "github", owner: "CapSoftware", repo: "Cap" }); + } + + async checkForUpdates(channel = this.updateChannel) { + if (!app.isPackaged) return null; + this.setUpdaterFeed(channel); + const result = await autoUpdater.checkForUpdates(); + if (!result?.updateInfo || result.updateInfo.version === app.getVersion()) return null; + return { + version: result.updateInfo.version, + notes: typeof result.updateInfo.releaseNotes === "string" ? result.updateInfo.releaseNotes : null, + channel, + }; + } + + async downloadAndInstall(channel = this.updateChannel) { + const update = await this.checkForUpdates(channel); + if (!update) throw new Error("No update available"); + await autoUpdater.downloadUpdate(); + return { version: update.version, installed: true }; + } +} + +function normalizeMenuItem(item) { + if (item.type === "separator") return { type: "separator" }; + return { + id: item.id, + label: item.text, + type: item.checked === undefined ? "normal" : "checkbox", + checked: item.checked, + enabled: item.enabled ?? true, + accelerator: item.accelerator, + role: item.role, + submenu: item.items?.map(normalizeMenuItem), + click: () => item.id && globalThis.__capMenuSelection?.(item.id), + }; +} + +function normalizeFilters(filters) { + return filters?.map((filter) => ({ name: filter.name, extensions: filter.extensions })); +} + +function normalizeArch(arch) { + return ({ x64: "x86_64", arm64: "aarch64", ia32: "x86" })[arch] ?? arch; +} + +function normalizeType(platform) { + return ({ darwin: "macos", win32: "windows", linux: "linux" })[platform] ?? platform; +} + +function resolveBaseDirectory(filePath, baseDir) { + if (!baseDir || path.isAbsolute(filePath)) return filePath; + const names = { + 1: "audioDir", 2: "cacheDir", 3: "configDir", 4: "dataDir", 6: "localDataDir", + 7: "desktop", 8: "documents", 9: "downloads", 11: "home", 15: "pictures", + 18: "public", 20: "resource", 21: "temp", 22: "templates", 24: "videos", + }; + const name = names[baseDir]; + if (name === "resource") return path.join(process.resourcesPath, filePath); + if (name === "temp") return path.join(app.getPath("temp"), filePath); + if (name === "home") return path.join(app.getPath("home"), filePath); + if (name === "localDataDir" || name === "configDir") return path.join(app.getPath("userData"), filePath); + if (!name) throw new Error(`Unsupported base directory '${baseDir}'`); + return path.join(app.getPath(name), filePath); +} + +async function writeFile(payload) { + const filePath = resolveBaseDirectory(payload.path, payload.baseDir); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, Buffer.from(payload.data)); +} + +async function writeTextFile(payload) { + const filePath = resolveBaseDirectory(payload.path, payload.baseDir); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, payload.contents, "utf8"); +} + +async function readDir(filePath) { + const entries = await fs.readdir(filePath, { withFileTypes: true }); + return entries.map((entry) => ({ + name: entry.name, + isDirectory: entry.isDirectory(), + isFile: entry.isFile(), + isSymlink: entry.isSymbolicLink(), + })); +} + +async function stat(filePath) { + const value = await fs.stat(filePath); + return { + size: value.size, + isFile: value.isFile(), + isDirectory: value.isDirectory(), + isSymlink: value.isSymbolicLink(), + mtime: value.mtime, + atime: value.atime, + birthtime: value.birthtime, + }; +} + +function withSeparator(value) { + return value.endsWith(path.sep) ? value : `${value}${path.sep}`; +} + +module.exports = { NativeBridge }; diff --git a/apps/desktop/electron/preload.cjs b/apps/desktop/electron/preload.cjs new file mode 100644 index 00000000000..87db185fb16 --- /dev/null +++ b/apps/desktop/electron/preload.cjs @@ -0,0 +1,74 @@ +const { contextBridge, ipcRenderer, webUtils } = require("electron"); + +const labelArgument = process.argv.find((argument) => + argument.startsWith("--cap-window-label="), +); +const initArgument = process.argv.find((argument) => + argument.startsWith("--cap-window-init="), +); +const windowLabel = + labelArgument?.slice("--cap-window-label=".length) ?? "main"; +const encodedInitialization = initArgument?.slice("--cap-window-init=".length); +const normalizedInitialization = encodedInitialization + ?.replaceAll("-", "+") + .replaceAll("_", "/") + .padEnd(Math.ceil(encodedInitialization.length / 4) * 4, "="); +const initialization = initArgument + ? JSON.parse(Buffer.from(normalizedInitialization, "base64").toString("utf8")) + : null; + +const eventListeners = new Set(); +const channelListeners = new Set(); + +ipcRenderer.on("cap:event", (_event, message) => { + for (const listener of eventListeners) listener(message); +}); +ipcRenderer.on("cap:channel", (_event, message) => { + for (const listener of channelListeners) listener(message); +}); + +window.addEventListener("dragover", (event) => event.preventDefault()); +window.addEventListener("drop", (event) => { + event.preventDefault(); + const paths = [...(event.dataTransfer?.files ?? [])] + .map((file) => webUtils.getPathForFile(file)) + .filter(Boolean); + if (paths.length > 0) + ipcRenderer.send("cap:window-drop", { windowLabel, paths }); +}); + +contextBridge.exposeInMainWorld("capElectron", { + windowLabel, + initialization, + os: { + arch: + { x64: "x86_64", arm64: "aarch64", ia32: "x86" }[process.arch] ?? + process.arch, + platform: process.platform, + type: + { darwin: "macos", win32: "windows", linux: "linux" }[process.platform] ?? + process.platform, + version: process.getSystemVersion?.() ?? "", + }, + invoke: (command, arguments_) => + ipcRenderer.invoke("cap:invoke", { + windowLabel, + command, + arguments: arguments_, + }), + native: (operation, payload) => + ipcRenderer.invoke("cap:native", { windowLabel, operation, payload }), + emit: (event, payload) => + ipcRenderer.send("cap:emit", { windowLabel, event, payload }), + onEvent: (listener) => { + eventListeners.add(listener); + return () => eventListeners.delete(listener); + }, + onChannel: (listener) => { + channelListeners.add(listener); + return () => channelListeners.delete(listener); + }, +}); +contextBridge.exposeInMainWorld("__CAP__", initialization ?? {}); +contextBridge.exposeInMainWorld("COUNTDOWN", initialization?.countdown ?? 0); +contextBridge.exposeInMainWorld("FLAGS", { captions: false }); diff --git a/apps/desktop/electron/windows.cjs b/apps/desktop/electron/windows.cjs new file mode 100644 index 00000000000..7fe8cabede1 --- /dev/null +++ b/apps/desktop/electron/windows.cjs @@ -0,0 +1,615 @@ +const { BrowserWindow, nativeTheme, screen } = require("electron"); + +class WindowManager { + constructor({ backend, devUrl, publicDir, preloadPath }) { + this.backend = backend; + this.devUrl = devUrl; + this.publicDir = publicDir; + this.preloadPath = preloadPath; + this.windows = new Map(); + this.windowMetadata = new WeakMap(); + this.allowedToClose = new WeakSet(); + this.allowAllClose = false; + backend.onMessage((message) => this.receive(message)); + this.nativeThemeListener = () => { + const theme = nativeTheme.shouldUseDarkColors ? "dark" : "light"; + for (const [label, window] of this.windows) { + if (window.isDestroyed()) continue; + window.webContents.send("cap:event", { + type: "event", + event: "tauri://theme-changed", + payload: theme, + target: label, + }); + } + }; + nativeTheme.on("updated", this.nativeThemeListener); + this.lastCursorPosition = null; + this.cursorTimer = setInterval(() => { + const position = this.dipPointToPhysical(screen.getCursorScreenPoint()); + if ( + this.lastCursorPosition?.x === position.x && + this.lastCursorPosition?.y === position.y + ) + return; + this.lastCursorPosition = position; + this.backend.send({ + type: "cursorPosition", + x: position.x, + y: position.y, + }); + }, 50); + } + + create(options) { + const existing = this.windows.get(options.label); + if (existing && !existing.isDestroyed()) { + if (options.visible) existing.show(); + if (options.focus) existing.focus(); + return existing; + } + const bounds = this.initialBounds(options); + const chrome = nativeWindowChrome(options.label); + const transparent = shouldUseTransparentSurface(options); + const window = new BrowserWindow({ + title: options.title ?? "Cap", + x: finite(bounds.x), + y: finite(bounds.y), + width: Math.round(bounds.width ?? 800), + height: Math.round(bounds.height ?? 600), + minWidth: finite(options.minWidth), + minHeight: finite(options.minHeight), + show: false, + transparent, + backgroundColor: transparent + ? "#00000000" + : nativeTheme.shouldUseDarkColors + ? "#141414" + : "#ffffff", + frame: chrome.frame, + titleBarStyle: chrome.titleBarStyle, + trafficLightPosition: chrome.trafficLightPosition, + hasShadow: !isDisplaySurface(options.label), + resizable: options.resizable ?? true, + alwaysOnTop: options.alwaysOnTop ?? false, + skipTaskbar: options.skipTaskbar ?? false, + webPreferences: { + preload: this.preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + additionalArguments: [ + `--cap-window-label=${options.label}`, + `--cap-window-init=${Buffer.from(JSON.stringify(options.initialization ?? null)).toString("base64url")}`, + ], + }, + }); + this.windowMetadata.set(window, { + decorated: chrome.frame, + loaded: false, + pendingShow: options.visible === true, + pendingFocus: options.focus === true, + }); + window.setContentProtection(options.contentProtected ?? false); + if (options.visibleOnAllWorkspaces) { + window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + } + this.windows.set(options.label, window); + this.bind(options.label, window); + this.load(window, options.route ?? "/"); + window.webContents.once("did-finish-load", () => { + if (window.isDestroyed()) return; + const metadata = this.windowMetadata.get(window); + metadata.loaded = true; + if (!metadata.pendingShow) return; + window.show(); + if (metadata.pendingFocus) window.focus(); + }); + return window; + } + + initialBounds(options) { + const requestedDisplay = displayIdForOptions(options); + if (requestedDisplay !== null && isDisplaySurface(options.label)) { + const display = screen + .getAllDisplays() + .find(({ id }) => String(id) === requestedDisplay); + // macOS refuses to position a normal BrowserWindow above the menu-bar + // inset. Using full display bounds would therefore shift the window down + // while retaining its full height, leaving the selection surface offset + // and extending below the display. + if (display) + return { + ...(process.platform === "darwin" + ? display.workArea + : display.bounds), + }; + } + return { + x: options.x, + y: options.y, + width: options.width, + height: options.height, + }; + } + + load(window, route) { + const normalized = route.startsWith("/") ? route : `/${route}`; + if (this.devUrl) window.loadURL(`${this.devUrl}${normalized}`); + else window.loadURL(`cap://app${normalized}`); + } + + bind(label, window) { + let lastScaleFactor = null; + const update = () => { + const state = this.sendState(label, window); + if ( + state && + lastScaleFactor !== null && + state.scaleFactor !== lastScaleFactor + ) { + this.sendWindowEvent(label, { + type: "scaleFactorChanged", + scaleFactor: state.scaleFactor, + }); + } + if (state) lastScaleFactor = state.scaleFactor; + }; + window.on("ready-to-show", update); + window.on("show", update); + window.on("hide", update); + window.on("focus", () => { + update(); + this.sendWindowEvent(label, { type: "focused", focused: true }); + }); + window.on("blur", () => { + update(); + this.sendWindowEvent(label, { type: "focused", focused: false }); + }); + window.on("move", () => { + update(); + const { x, y } = this.physicalBounds(window); + this.sendWindowEvent(label, { type: "moved", x, y }); + }); + window.on("resize", () => { + update(); + const { width, height } = this.physicalBounds(window); + this.sendWindowEvent(label, { type: "resized", width, height }); + }); + window.on("close", (event) => { + if (this.allowAllClose || this.allowedToClose.has(window)) { + this.allowedToClose.delete(window); + return; + } + event.preventDefault(); + this.sendWindowEvent(label, { type: "closeRequested" }); + }); + window.on("closed", () => { + this.windows.delete(label); + this.sendWindowEvent(label, { type: "destroyed" }); + }); + window.webContents.on("did-finish-load", update); + } + + sendState(label, window) { + if (window.isDestroyed()) return null; + const state = this.windowState(window); + this.backend.send({ + type: "windowState", + label, + state, + }); + return state; + } + + sendWindowEvent(label, event) { + this.backend.send({ type: "windowEvent", label, event }); + const window = this.windows.get(label); + if (!window || window.isDestroyed()) return; + const rendererEvent = rendererWindowEvent(event, window); + if (rendererEvent) { + window.webContents.send("cap:event", { + type: "event", + event: rendererEvent.name, + payload: rendererEvent.payload, + target: label, + }); + } + } + + receive(message) { + if (message.type === "createWindow") this.create(message.options); + if (message.type === "desktopOperation") { + const window = this.windows.get(message.label); + if (window && !window.isDestroyed()) + this.operation(window, message.operation); + } + if (message.type === "event") { + for (const [label, window] of this.windows) { + if (!message.target || message.target === label) { + window.webContents.send("cap:event", message); + } + } + } + if (message.type === "channel") { + for (const window of this.windows.values()) { + window.webContents.send("cap:channel", message); + } + } + } + + operation(window, operation) { + const metadata = this.windowMetadata.get(window); + switch (operation.type) { + case "center": + window.center(); + break; + case "show": + metadata.pendingShow = true; + if (metadata.loaded) window.show(); + break; + case "hide": + metadata.pendingShow = false; + metadata.pendingFocus = false; + window.hide(); + break; + case "close": + this.allowedToClose.add(window); + window.close(); + break; + case "destroy": + window.destroy(); + break; + case "focus": + metadata.pendingFocus = true; + if (metadata.loaded) window.focus(); + break; + case "minimize": + window.minimize(); + break; + case "unminimize": + window.restore(); + break; + case "maximize": + window.maximize(); + break; + case "unmaximize": + window.unmaximize(); + break; + case "setFullscreen": + window.setFullScreen(operation.fullscreen); + break; + case "setAlwaysOnTop": + window.setAlwaysOnTop(operation.alwaysOnTop); + break; + case "setContentProtected": + window.setContentProtection(operation.enabled); + break; + case "setPosition": { + const point = operation.physical + ? this.physicalPointToDip({ x: operation.x, y: operation.y }, window) + : operation; + window.setPosition(Math.round(point.x), Math.round(point.y)); + break; + } + case "setSize": { + const size = operation.physical + ? this.physicalSizeToDip(operation, window) + : operation; + window.setSize(Math.round(size.width), Math.round(size.height)); + break; + } + case "setMinSize": + case "setMinimumSize": { + const size = operation.physical + ? this.physicalSizeToDip(operation, window) + : operation; + window.setMinimumSize( + Math.round(size.width ?? 0), + Math.round(size.height ?? 0), + ); + break; + } + case "setTitle": + window.setTitle(operation.title); + break; + case "setResizable": + window.setResizable(operation.resizable); + break; + case "setSkipTaskbar": + window.setSkipTaskbar(operation.skip); + break; + case "setIgnoreCursorEvents": + window.setIgnoreMouseEvents(operation.ignore, { forward: true }); + break; + case "setOpacity": + window.setOpacity(operation.opacity); + break; + case "setTheme": { + const source = ["system", "light", "dark"].includes(operation.theme) + ? operation.theme + : "system"; + if (nativeTheme.themeSource !== source) nativeTheme.themeSource = source; + break; + } + case "setTrafficLightPosition": + if (process.platform === "darwin") + window.setWindowButtonPosition( + operation.x === null || operation.x === undefined + ? null + : { x: Math.round(operation.x), y: Math.round(operation.y) }, + ); + break; + case "requestUserAttention": + window.flashFrame(true); + break; + case "setProgress": + window.setProgressBar(operation.progress ?? -1); + break; + } + } + + get(label) { + return this.windows.get(label); + } + + cursorPosition() { + return this.dipPointToPhysical(screen.getCursorScreenPoint()); + } + + windowState(window) { + const dipBounds = window.getBounds(); + const display = screen.getDisplayMatching(dipBounds); + const bounds = this.physicalBounds(window); + const monitor = this.normalizeDisplay(display); + return { + nativeWindowId: nativeWindowId(window), + ...bounds, + visible: window.isVisible(), + focused: window.isFocused(), + minimized: window.isMinimized(), + maximized: window.isMaximized(), + fullscreen: window.isFullScreen(), + alwaysOnTop: window.isAlwaysOnTop(), + resizable: window.isResizable(), + decorated: this.windowMetadata.get(window)?.decorated ?? false, + monitor: { + name: monitor.name, + x: monitor.position.x, + y: monitor.position.y, + width: monitor.size.width, + height: monitor.size.height, + workX: monitor.workArea.position.x, + workY: monitor.workArea.position.y, + workWidth: monitor.workArea.size.width, + workHeight: monitor.workArea.size.height, + scaleFactor: monitor.scaleFactor, + }, + }; + } + + physicalBounds(window) { + const bounds = window.getBounds(); + const display = screen.getDisplayMatching(bounds); + const origin = physicalDisplayOrigin(display); + return { + x: Math.round( + origin.x + (bounds.x - display.bounds.x) * display.scaleFactor, + ), + y: Math.round( + origin.y + (bounds.y - display.bounds.y) * display.scaleFactor, + ), + width: Math.round(bounds.width * display.scaleFactor), + height: Math.round(bounds.height * display.scaleFactor), + scaleFactor: display.scaleFactor, + }; + } + + physicalPointToDip(point, window) { + const display = + displayForPhysicalPoint(point) ?? + screen.getDisplayMatching( + window?.getBounds() ?? screen.getPrimaryDisplay().bounds, + ); + const origin = physicalDisplayOrigin(display); + return { + x: display.bounds.x + (point.x - origin.x) / display.scaleFactor, + y: display.bounds.y + (point.y - origin.y) / display.scaleFactor, + }; + } + + physicalSizeToDip(size, window) { + const display = screen.getDisplayMatching(window.getBounds()); + return { + width: + size.width === null || size.width === undefined + ? size.width + : size.width / display.scaleFactor, + height: + size.height === null || size.height === undefined + ? size.height + : size.height / display.scaleFactor, + }; + } + + dipPointToPhysical(point) { + const display = screen.getDisplayNearestPoint(point); + const origin = physicalDisplayOrigin(display); + return { + x: Math.round( + origin.x + (point.x - display.bounds.x) * display.scaleFactor, + ), + y: Math.round( + origin.y + (point.y - display.bounds.y) * display.scaleFactor, + ), + }; + } + + normalizeDisplay(display) { + const bounds = displayRectToPhysical(display.bounds, display); + const workArea = displayRectToPhysical(display.workArea, display); + return { + name: display.label || null, + scaleFactor: display.scaleFactor, + position: { x: bounds.x, y: bounds.y }, + size: { width: bounds.width, height: bounds.height }, + workArea: { + position: { x: workArea.x, y: workArea.y }, + size: { width: workArea.width, height: workArea.height }, + }, + }; + } + + displayFromPhysicalPoint(point) { + return displayForPhysicalPoint(point) ?? screen.getPrimaryDisplay(); + } + + beginQuit() { + this.allowAllClose = true; + } +} + +function finite(value) { + return Number.isFinite(value) ? Math.round(value) : undefined; +} + +function nativeWindowId(window) { + const sourceId = window.getMediaSourceId(); + const match = /^window:(\d+):/.exec(sourceId); + return match ? Number(match[1]) : null; +} + +function isDisplaySurface(label) { + return ( + label.startsWith("target-select-overlay-") || + label.startsWith("window-capture-occluder-") || + label === "capture-area" + ); +} + +function displayIdForOptions(options) { + const prefixes = ["target-select-overlay-", "window-capture-occluder-"]; + for (const prefix of prefixes) { + if (options.label.startsWith(prefix)) + return options.label.slice(prefix.length); + } + if (options.label !== "capture-area") return null; + try { + return new URL(options.route, "cap://app").searchParams.get("screenId"); + } catch { + return null; + } +} + +function shouldUseTransparentSurface(options) { + return ( + options.transparent === true && + !["main", "onboarding"].includes(options.label) + ); +} + +function nativeWindowChrome(label) { + if (process.platform !== "darwin") return { frame: false }; + if (label.startsWith("editor-") || label.startsWith("screenshot-editor-")) { + return { + frame: true, + titleBarStyle: "hidden", + trafficLightPosition: { x: 20, y: 24 }, + }; + } + if (label === "settings") { + return { + frame: true, + titleBarStyle: "hidden", + trafficLightPosition: { x: 22, y: 14 }, + }; + } + if (label === "teleprompter") { + return { + frame: true, + titleBarStyle: "hidden", + trafficLightPosition: { x: 14, y: 14 }, + }; + } + if (["upgrade", "mode-select", "debug"].includes(label)) { + return { frame: true, titleBarStyle: "hidden" }; + } + return { frame: false }; +} + +function physicalDisplayOrigin(display) { + if ( + Number.isFinite(display.nativeOrigin?.x) && + Number.isFinite(display.nativeOrigin?.y) + ) { + return display.nativeOrigin; + } + return { + x: Math.round(display.bounds.x * display.scaleFactor), + y: Math.round(display.bounds.y * display.scaleFactor), + }; +} + +function displayRectToPhysical(rect, display) { + const origin = physicalDisplayOrigin(display); + return { + x: Math.round(origin.x + (rect.x - display.bounds.x) * display.scaleFactor), + y: Math.round(origin.y + (rect.y - display.bounds.y) * display.scaleFactor), + width: Math.round(rect.width * display.scaleFactor), + height: Math.round(rect.height * display.scaleFactor), + }; +} + +function displayForPhysicalPoint(point) { + return screen.getAllDisplays().find((display) => { + const bounds = displayRectToPhysical(display.bounds, display); + return ( + point.x >= bounds.x && + point.x < bounds.x + bounds.width && + point.y >= bounds.y && + point.y < bounds.y + bounds.height + ); + }); +} + +function rendererWindowEvent(event, window) { + switch (event.type) { + case "closeRequested": + return { name: "tauri://close-requested", payload: null }; + case "destroyed": + return { name: "tauri://destroyed", payload: null }; + case "focused": + return { name: "tauri://focus", payload: event.focused }; + case "moved": + return { name: "tauri://move", payload: { x: event.x, y: event.y } }; + case "resized": + return { + name: "tauri://resize", + payload: { width: event.width, height: event.height }, + }; + case "scaleFactorChanged": { + const [width, height] = window.getSize(); + return { + name: "tauri://scale-change", + payload: { + scaleFactor: event.scaleFactor, + size: { + width: Math.round(width * event.scaleFactor), + height: Math.round(height * event.scaleFactor), + }, + }, + }; + } + case "themeChanged": + return { name: "tauri://theme-changed", payload: event.theme }; + case "dragDrop": + return { + name: "tauri://drag-drop", + payload: { type: "drop", paths: event.paths }, + }; + default: + return null; + } +} + +module.exports = { WindowManager }; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 17e7b4f846f..8cc12e758e3 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,19 +1,28 @@ { "name": "@cap/desktop", + "version": "0.5.9", + "description": "Cap screen recorder and editor", + "author": "Cap Software", "type": "module", + "main": "electron/main.cjs", "scripts": { - "dev": "pnpm -w cap-setup && pnpm build:sidecar && dotenv -e ../../.env -- pnpm run preparescript && dotenv -e ../../.env -- pnpm tauri dev", - "build:tauri": "pnpm build:sidecar && dotenv -e ../../.env -- pnpm run preparescript && dotenv -e ../../.env -- pnpm tauri build", + "dev": "pnpm -w cap-setup && pnpm build:sidecar && pnpm build:backend && pnpm build:electron-main && dotenv -e ../../.env -- pnpm run preparescript && concurrently -k -s first \"pnpm localdev\" \"wait-on http://localhost:3002 && electron .\"", + "build:backend": "cargo build --manifest-path ../../Cargo.toml --package cap-desktop", + "build:backend:release": "cargo build --manifest-path ../../Cargo.toml --package cap-desktop --release", + "build:electron": "pnpm build:sidecar && dotenv -e ../../.env -- pnpm run preparescript && pnpm build && pnpm build:electron-main && electron-builder", + "build:electron-main": "node scripts/build-electron.mjs", "build:sidecar": "node ../../scripts/build-desktop-binaries.mjs", "preparescript": "node scripts/prepare.js", "localdev": "dotenv -e ../../.env -- vinxi dev --port 3002", "build": "vinxi build", - "tauri": "tauri", "test:display-transport": "node scripts/desktop-display-transport-benchmark.js", "test:memory": "node scripts/desktop-memory-soak.js", "test:memory:unit": "vitest run scripts/desktop-memory-soak.test.js" }, "dependencies": { + "electron-updater": "6.8.9" + }, + "devDependencies": { "@aerofoil/rive-solid-canvas": "^2.1.4", "@cap/database": "workspace:*", "@cap/ui-solid": "workspace:*", @@ -43,19 +52,6 @@ "@solidjs/router": "^0.14.2", "@solidjs/start": "^1.1.3", "@tanstack/solid-query": "^5.51.21", - "@tauri-apps/api": "2.8.0", - "@tauri-apps/plugin-clipboard-manager": "^2.3.0", - "@tauri-apps/plugin-deep-link": "^2.4.1", - "@tauri-apps/plugin-dialog": "2.4.2", - "@tauri-apps/plugin-fs": "2.4.2", - "@tauri-apps/plugin-http": "2.5.2", - "@tauri-apps/plugin-notification": "^2.3.0", - "@tauri-apps/plugin-opener": "^2.5.0", - "@tauri-apps/plugin-os": "^2.3.0", - "@tauri-apps/plugin-process": "2.3.0", - "@tauri-apps/plugin-shell": "^2.3.0", - "@tauri-apps/plugin-store": "^2.4.0", - "@tauri-apps/plugin-updater": "2.9.0", "@ts-rest/core": "^3.52.1", "@types/react-tooltip": "^4.2.4", "cva": "npm:class-variance-authority@^0.7.0", @@ -72,20 +68,21 @@ "uuid": "^9.0.1", "vinxi": "^0.5.6", "webcodecs": "^0.1.0", - "zod": "^3.25.76" - }, - "devDependencies": { + "zod": "^3.25.76", "@fontsource/geist-sans": "^5.0.3", "@iconify/json": "^2.2.239", "@solid-devtools/overlay": "^0.33.5", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.9", - "@tauri-apps/cli": ">=2.1.0", "@total-typescript/ts-reset": "^0.6.1", "@types/dom-webcodecs": "^0.1.11", "@types/uuid": "^9.0.8", "@webgpu/types": "^0.1.44", + "concurrently": "9.2.1", "cross-env": "^7.0.3", + "electron": "43.4.0", + "electron-builder": "26.15.3", + "esbuild": "^0.25.12", "solid-devtools": "^0.34.5", "tailwind-scrollbar": "^3.1.0", "typescript": "^5.8.3", @@ -93,6 +90,7 @@ "vite-plugin-top-level-await": "^1.4.4", "vite-plugin-wasm": "^3.4.1", "vite-tsconfig-paths": "^5.0.1", - "vitest": "~2.1.9" + "vitest": "~2.1.9", + "wait-on": "9.0.1" } } diff --git a/apps/desktop/scripts/build-electron.mjs b/apps/desktop/scripts/build-electron.mjs new file mode 100644 index 00000000000..2f008346977 --- /dev/null +++ b/apps/desktop/scripts/build-electron.mjs @@ -0,0 +1,34 @@ +import { rm } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { build } from "esbuild"; + +const desktopDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const outputDir = path.join(desktopDir, ".electron-dist"); + +await rm(outputDir, { recursive: true, force: true }); + +const common = { + bundle: true, + platform: "node", + format: "cjs", + target: "node22", + external: ["electron"], + logLevel: "info", + legalComments: "none", + sourcemap: false, +}; + +await Promise.all([ + build({ + ...common, + entryPoints: [path.join(desktopDir, "electron", "main.cjs")], + outfile: path.join(outputDir, "main.cjs"), + }), + build({ + ...common, + entryPoints: [path.join(desktopDir, "electron", "preload.cjs")], + outfile: path.join(outputDir, "preload.cjs"), + }), +]); diff --git a/apps/desktop/scripts/prepare.js b/apps/desktop/scripts/prepare.js index dc3e28d9acf..21b2c0784b2 100644 --- a/apps/desktop/scripts/prepare.js +++ b/apps/desktop/scripts/prepare.js @@ -70,7 +70,7 @@ export async function createTauriPlatformConfigs( platform, configOptions = undefined, ) { - const srcTauri = path.join(__dirname, "../src-tauri/"); + const srcTauri = path.join(__dirname, "../src-backend/"); let baseConfig = {}; let configFileName = null; diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx index be56bd5ec9b..3eb053c327c 100644 --- a/apps/desktop/src/app.tsx +++ b/apps/desktop/src/app.tsx @@ -3,8 +3,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; import { getCurrentWebviewWindow, type WebviewWindow, -} from "@tauri-apps/api/webviewWindow"; -import { message } from "@tauri-apps/plugin-dialog"; +} from "~/electron/webviewWindow"; +import { message } from "~/electron/dialog"; import { createEffect, createSignal, diff --git a/apps/desktop/src/components/CapErrorBoundary.tsx b/apps/desktop/src/components/CapErrorBoundary.tsx index 2dca68d1d47..01960ca7701 100644 --- a/apps/desktop/src/components/CapErrorBoundary.tsx +++ b/apps/desktop/src/components/CapErrorBoundary.tsx @@ -1,6 +1,6 @@ import { Button } from "@cap/ui-solid"; -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; +import { getCurrentWebviewWindow } from "~/electron/webviewWindow"; +import { writeText } from "~/electron/clipboard"; import { ErrorBoundary, type ParentProps } from "solid-js"; export function CapErrorBoundary(props: ParentProps) { diff --git a/apps/desktop/src/components/Cropper.tsx b/apps/desktop/src/components/Cropper.tsx index 57462ee48cc..f8e3a9a3640 100644 --- a/apps/desktop/src/components/Cropper.tsx +++ b/apps/desktop/src/components/Cropper.tsx @@ -3,8 +3,8 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"; import type { CheckMenuItemOptions, PredefinedMenuItemOptions, -} from "@tauri-apps/api/menu"; -import { type as ostype } from "@tauri-apps/plugin-os"; +} from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; import { type Accessor, children, diff --git a/apps/desktop/src/components/Tooltip.tsx b/apps/desktop/src/components/Tooltip.tsx index 3b8897df7f5..6ba00b17263 100644 --- a/apps/desktop/src/components/Tooltip.tsx +++ b/apps/desktop/src/components/Tooltip.tsx @@ -1,5 +1,5 @@ import { Tooltip as KTooltip } from "@kobalte/core/tooltip"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import type { ComponentProps, JSX } from "solid-js"; diff --git a/apps/desktop/src/components/selection-hint.tsx b/apps/desktop/src/components/selection-hint.tsx index f27c72db54c..4c2bef078d9 100644 --- a/apps/desktop/src/components/selection-hint.tsx +++ b/apps/desktop/src/components/selection-hint.tsx @@ -1,4 +1,4 @@ -import { type as ostype } from "@tauri-apps/plugin-os"; +import { type as ostype } from "~/electron/os"; import { Match, Show, Switch } from "solid-js"; export type SelectionHintProps = { diff --git a/apps/desktop/src/components/titlebar/Titlebar.tsx b/apps/desktop/src/components/titlebar/Titlebar.tsx index a6522d3c015..885284f403c 100644 --- a/apps/desktop/src/components/titlebar/Titlebar.tsx +++ b/apps/desktop/src/components/titlebar/Titlebar.tsx @@ -1,5 +1,5 @@ // Credits: tauri-controls -import { type } from "@tauri-apps/plugin-os"; +import { type } from "~/electron/os"; import { cx } from "cva"; import { type ComponentProps, Match, Switch, splitProps } from "solid-js"; import titlebarState from "~/utils/titlebar-state"; diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx index 77b00e588eb..229d8075995 100644 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx @@ -1,4 +1,4 @@ -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "~/electron/window"; import { cx } from "cva"; import { type ComponentProps, diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx index d55b689bd17..1c9ecfe0875 100644 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx @@ -1,4 +1,4 @@ -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "~/electron/window"; import { cx } from "cva"; import { type ComponentProps, diff --git a/apps/desktop/src/electron/app.ts b/apps/desktop/src/electron/app.ts new file mode 100644 index 00000000000..ae949e9bb36 --- /dev/null +++ b/apps/desktop/src/electron/app.ts @@ -0,0 +1,8 @@ +import { native } from "./bridge"; +export const getName = () => native("app.name"); +export const getVersion = () => native("app.version"); +export const getTauriVersion = () => Promise.resolve("electron"); +export const getIdentifier = () => Promise.resolve("so.cap.desktop"); +export const show = () => native("window.action", { action: "show" }); +export const hide = () => native("window.action", { action: "hide" }); +export const defaultWindowIcon = () => Promise.resolve(null); diff --git a/apps/desktop/src/electron/bridge.ts b/apps/desktop/src/electron/bridge.ts new file mode 100644 index 00000000000..a0efe64381d --- /dev/null +++ b/apps/desktop/src/electron/bridge.ts @@ -0,0 +1,44 @@ +export interface CapElectronBridge { + windowLabel: string; + initialization: unknown; + os: { arch: string; platform: string; type: string; version: string }; + invoke(command: string, args?: Record): Promise; + native(operation: string, payload?: Record): Promise; + emit(event: string, payload?: unknown): void; + onEvent(listener: (message: BridgeEvent) => void): () => void; + onChannel(listener: (message: BridgeChannelMessage) => void): () => void; +} + +export interface BridgeEvent { + type: "event"; + event: string; + payload: unknown; + target?: string; +} + +export interface BridgeChannelMessage { + type: "channel"; + channelId: number; + index: number; + message?: unknown; + end: boolean; +} + +declare global { + interface Window { + capElectron: CapElectronBridge; + } +} + +export function bridge() { + if (!window.capElectron) throw new Error("Cap Electron preload bridge is unavailable"); + return window.capElectron; +} + +export function toIpcValue(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +export function native(operation: string, payload?: Record) { + return bridge().native(operation, toIpcValue(payload ?? {})); +} diff --git a/apps/desktop/src/electron/clipboard.ts b/apps/desktop/src/electron/clipboard.ts new file mode 100644 index 00000000000..12611b626f3 --- /dev/null +++ b/apps/desktop/src/electron/clipboard.ts @@ -0,0 +1,6 @@ +import { native } from "./bridge"; +export const readText = () => native("clipboard.readText"); +export const writeText = (text: string) => native("clipboard.writeText", { text }); +export const readImage = () => Promise.reject(new Error("readImage is not implemented")); +export const writeImage = (_image: unknown) => Promise.reject(new Error("writeImage is not implemented")); +export const clear = () => writeText(""); diff --git a/apps/desktop/src/electron/core.ts b/apps/desktop/src/electron/core.ts new file mode 100644 index 00000000000..fb9d8d1a7f6 --- /dev/null +++ b/apps/desktop/src/electron/core.ts @@ -0,0 +1,50 @@ +import { bridge, toIpcValue } from "./bridge"; + +let nextChannelId = 1; +const channels = new Map>(); + +bridge().onChannel((message) => { + const channel = channels.get(message.channelId); + if (!channel) return; + if (message.end) { + channels.delete(message.channelId); + return; + } + try { + channel.onmessage?.(message.message); + } catch (error) { + channel.onerror?.(error); + } +}); + +export class Channel { + id: number; + onmessage?: (message: T) => void; + onerror?: (error: unknown) => void; + + constructor(onmessage?: (message: T) => void) { + this.id = nextChannelId++; + this.onmessage = onmessage; + channels.set(this.id, this as Channel); + } + + toJSON() { + return `__CHANNEL__:${this.id}`; + } +} + +export async function invoke(command: string, args?: Record): Promise { + return bridge().invoke(command, toIpcValue(args ?? {})); +} + +export function convertFileSrc(filePath: string, protocol = "asset") { + if (protocol !== "asset") return `${protocol}://localhost/${encodeURIComponent(filePath)}`; + const bytes = new TextEncoder().encode(filePath); + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return `cap-asset://asset/${btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "")}`; +} + +export function isTauri() { + return false; +} diff --git a/apps/desktop/src/electron/deep-link.ts b/apps/desktop/src/electron/deep-link.ts new file mode 100644 index 00000000000..fe3b2831144 --- /dev/null +++ b/apps/desktop/src/electron/deep-link.ts @@ -0,0 +1,7 @@ +import { listen, type UnlistenFn } from "./event"; +export const getCurrent = () => Promise.resolve(null); +export const onOpenUrl = (handler: (urls: string[]) => void): Promise => + listen("deep-link://new-url", ({ payload }) => handler(payload)); +export const register = () => Promise.resolve(); +export const unregister = () => Promise.resolve(); +export const isRegistered = () => Promise.resolve(true); diff --git a/apps/desktop/src/electron/dialog.ts b/apps/desktop/src/electron/dialog.ts new file mode 100644 index 00000000000..e79d371b481 --- /dev/null +++ b/apps/desktop/src/electron/dialog.ts @@ -0,0 +1,27 @@ +import { native } from "./bridge"; + +export interface DialogFilter { name: string; extensions: string[]; } +export interface OpenDialogOptions { + title?: string; + filters?: DialogFilter[]; + defaultPath?: string; + multiple?: boolean; + directory?: boolean; + recursive?: boolean; + canCreateDirectories?: boolean; +} +export interface SaveDialogOptions { title?: string; filters?: DialogFilter[]; defaultPath?: string; } +export interface MessageDialogOptions { title?: string; kind?: "info" | "warning" | "error"; okLabel?: string; buttons?: string[] | Record; } +export interface ConfirmDialogOptions extends MessageDialogOptions { cancelLabel?: string; } + +export function open(options: OpenDialogOptions = {}) { return native("dialog.open", { ...options }); } +export function save(options: SaveDialogOptions = {}) { return native("dialog.save", { ...options }); } +export function message(message_: string, options: string | MessageDialogOptions = {}) { + return native("dialog.message", { message: message_, ...(typeof options === "string" ? { title: options } : options) }); +} +export function ask(message_: string, options: string | ConfirmDialogOptions = {}) { + return native("dialog.ask", { message: message_, ...(typeof options === "string" ? { title: options } : options) }); +} +export function confirm(message_: string, options: string | ConfirmDialogOptions = {}) { + return native("dialog.confirm", { message: message_, ...(typeof options === "string" ? { title: options } : options) }); +} diff --git a/apps/desktop/src/electron/dpi.ts b/apps/desktop/src/electron/dpi.ts new file mode 100644 index 00000000000..5bff4672f1c --- /dev/null +++ b/apps/desktop/src/electron/dpi.ts @@ -0,0 +1,30 @@ +export class LogicalPosition { + constructor(public x: T, public y: T) {} + toPhysical(scaleFactor: number) { + return new PhysicalPosition(this.x * scaleFactor, this.y * scaleFactor); + } +} + +export class PhysicalPosition { + constructor(public x: T, public y: T) {} + toLogical(scaleFactor: number) { + return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor); + } +} + +export class LogicalSize { + constructor(public width: T, public height: T) {} + toPhysical(scaleFactor: number) { + return new PhysicalSize(this.width * scaleFactor, this.height * scaleFactor); + } +} + +export class PhysicalSize { + constructor(public width: T, public height: T) {} + toLogical(scaleFactor: number) { + return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor); + } +} + +export type Position = LogicalPosition | PhysicalPosition; +export type Size = LogicalSize | PhysicalSize; diff --git a/apps/desktop/src/electron/event.ts b/apps/desktop/src/electron/event.ts new file mode 100644 index 00000000000..1dfe77a7cc7 --- /dev/null +++ b/apps/desktop/src/electron/event.ts @@ -0,0 +1,37 @@ +import { bridge, toIpcValue } from "./bridge"; + +export type UnlistenFn = () => void; + +export interface Event { + event: string; + id: number; + payload: T; +} + +export type EventCallback = (event: Event) => void; + +let nextEventId = 1; + +export async function listen(event: string, handler: EventCallback): Promise { + const id = nextEventId++; + return bridge().onEvent((message) => { + if (message.event === event) handler({ event, id, payload: message.payload as T }); + }); +} + +export async function once(event: string, handler: EventCallback): Promise { + let unlisten = () => {}; + unlisten = await listen(event, (payload) => { + unlisten(); + handler(payload); + }); + return unlisten; +} + +export async function emit(event: string, payload?: unknown): Promise { + bridge().emit(event, toIpcValue(payload ?? null)); +} + +export async function emitTo(_target: string, event: string, payload?: unknown): Promise { + bridge().emit(event, toIpcValue(payload ?? null)); +} diff --git a/apps/desktop/src/electron/fs.ts b/apps/desktop/src/electron/fs.ts new file mode 100644 index 00000000000..c8f843185d2 --- /dev/null +++ b/apps/desktop/src/electron/fs.ts @@ -0,0 +1,28 @@ +import { native } from "./bridge"; + +export enum BaseDirectory { + Audio = 1, Cache = 2, Config = 3, Data = 4, LocalData = 6, Desktop = 7, + Document = 8, Download = 9, Home = 11, Picture = 15, Public = 18, + Resource = 20, Temp = 21, Template = 22, Video = 24, AppConfig = 25, + AppData = 26, AppLocalData = 27, AppCache = 28, AppLog = 29, +} +export interface FsOptions { baseDir?: BaseDirectory; } +export interface RemoveOptions extends FsOptions { recursive?: boolean; } +export interface MkdirOptions extends FsOptions { recursive?: boolean; } +export interface DirEntry { name: string; isDirectory: boolean; isFile: boolean; isSymlink: boolean; } +export interface FileInfo { size: number; isFile: boolean; isDirectory: boolean; isSymlink: boolean; mtime: Date | null; atime: Date | null; birthtime: Date | null; } + +const options = (path: string, value?: FsOptions) => ({ path, baseDir: value?.baseDir }); +export const exists = (path: string, value?: FsOptions) => native("fs.exists", options(path, value)); +export const readFile = (path: string, value?: FsOptions) => native("fs.readFile", options(path, value)).then((bytes) => new Uint8Array(bytes)); +export const readTextFile = (path: string, value?: FsOptions) => native("fs.readTextFile", options(path, value)); +export const writeFile = (path: string, data: Uint8Array, value?: FsOptions) => native("fs.writeFile", { ...options(path, value), data: Array.from(data) }); +export const writeTextFile = (path: string, contents: string, value?: FsOptions) => native("fs.writeTextFile", { ...options(path, value), contents }); +export const remove = (path: string, value?: RemoveOptions) => native("fs.remove", { ...options(path, value), recursive: value?.recursive }); +export const mkdir = (path: string, value?: MkdirOptions) => native("fs.mkdir", { ...options(path, value), recursive: value?.recursive }); +export const readDir = (path: string, value?: FsOptions) => native("fs.readDir", options(path, value)); +export const stat = (path: string, value?: FsOptions) => native("fs.stat", options(path, value)); +export const lstat = stat; +export const copyFile = (_source: string, _destination: string, _options?: FsOptions) => Promise.reject(new Error("copyFile is not implemented")); +export const rename = (_oldPath: string, _newPath: string, _options?: FsOptions) => Promise.reject(new Error("rename is not implemented")); +export const truncate = (_path: string, _length?: number, _options?: FsOptions) => Promise.reject(new Error("truncate is not implemented")); diff --git a/apps/desktop/src/electron/http.ts b/apps/desktop/src/electron/http.ts new file mode 100644 index 00000000000..0a09b555305 --- /dev/null +++ b/apps/desktop/src/electron/http.ts @@ -0,0 +1 @@ +export const fetch = globalThis.fetch.bind(globalThis); diff --git a/apps/desktop/src/electron/menu.ts b/apps/desktop/src/electron/menu.ts new file mode 100644 index 00000000000..29ee5109268 --- /dev/null +++ b/apps/desktop/src/electron/menu.ts @@ -0,0 +1,103 @@ +import { native } from "./bridge"; +import { listen } from "./event"; +import type { LogicalPosition, PhysicalPosition } from "./dpi"; + +let nextMenuId = 1; + +export interface MenuItemOptions { + id?: string; + text?: string; + enabled?: boolean; + accelerator?: string; + action?: (id: string) => unknown; + items?: MenuEntry[]; +} + +export interface CheckMenuItemOptions extends MenuItemOptions { checked?: boolean; } +export interface PredefinedMenuItemOptions { item: string; text?: string; } + +export class MenuItem { + id: string; + text?: string; + enabled?: boolean; + accelerator?: string; + action?: (id: string) => unknown; + items?: MenuEntry[]; + + constructor(options: MenuItemOptions) { + this.id = options.id ?? `cap-menu-${nextMenuId++}`; + Object.assign(this, options); + } + + static async new(options: MenuItemOptions) { return new MenuItem(options); } + async setText(text: string) { this.text = text; } + async setEnabled(enabled: boolean) { this.enabled = enabled; } +} + +export class CheckMenuItem extends MenuItem { + checked: boolean; + constructor(options: CheckMenuItemOptions) { + super(options); + this.checked = options.checked ?? false; + } + static async new(options: CheckMenuItemOptions) { return new CheckMenuItem(options); } + async setChecked(checked: boolean) { this.checked = checked; } + async isChecked() { return this.checked; } +} + +export class PredefinedMenuItem extends MenuItem { + item: string; + constructor(options: PredefinedMenuItemOptions) { + super({ text: options.text }); + this.item = options.item; + } + static async new(options: PredefinedMenuItemOptions) { return new PredefinedMenuItem(options); } +} + +export class Submenu extends MenuItem { + static async new(options: MenuItemOptions) { return new Submenu(options); } +} + +export type MenuEntry = MenuItem | CheckMenuItem | PredefinedMenuItem | Submenu | MenuItemOptions; + +export class Menu { + constructor(public items: MenuEntry[]) {} + static async new(options: { id?: string; items?: MenuEntry[] } = {}) { return new Menu(options.items ?? []); } + + async popup(position?: LogicalPosition | PhysicalPosition) { + const unlisteners = await Promise.all(this.items.flatMap(flattenItems).filter((item) => item.action).map(async (item) => { + const id = item.id ?? `cap-menu-${nextMenuId++}`; + item.id = id; + return listen(`menu:${id}`, () => item.action?.(id)); + })); + await native("menu.popup", { + items: this.items.map(serializeItem), + position: position ? { x: position.x, y: position.y } : undefined, + }); + setTimeout(() => unlisteners.forEach((unlisten) => unlisten()), 30000); + } + + async append(item: MenuEntry) { this.items.push(item); } + async prepend(item: MenuEntry) { this.items.unshift(item); } + async insert(item: MenuEntry, position: number) { this.items.splice(position, 0, item); } + async remove(item: MenuEntry) { this.items = this.items.filter((entry) => entry !== item); } + async removeAt(position: number) { return this.items.splice(position, 1)[0] ?? null; } + async itemsList() { return this.items; } + async close() {} +} + +function flattenItems(item: MenuEntry): MenuEntry[] { + return [item, ...(item.items?.flatMap(flattenItems) ?? [])]; +} + +function serializeItem(item: MenuEntry): Record { + if (item instanceof PredefinedMenuItem && item.item.toLowerCase() === "separator") return { type: "separator" }; + return { + id: item.id, + text: item.text, + enabled: item.enabled, + accelerator: item.accelerator, + checked: item instanceof CheckMenuItem ? item.checked : undefined, + items: item.items?.map(serializeItem), + }; +} diff --git a/apps/desktop/src/electron/notification.ts b/apps/desktop/src/electron/notification.ts new file mode 100644 index 00000000000..1f00db9ee37 --- /dev/null +++ b/apps/desktop/src/electron/notification.ts @@ -0,0 +1,19 @@ +import { native } from "./bridge"; +export type Permission = "granted" | "denied" | "default"; +export const isPermissionGranted = () => native("notification.permission").then((value) => value === "granted"); +export const requestPermission = () => native("notification.requestPermission"); +export const sendNotification = (options: string | { title: string; body?: string; icon?: string }) => + native("notification.show", typeof options === "string" ? { title: options } : options); +export const registerActionTypes = () => Promise.resolve(); +export const pending = () => Promise.resolve([]); +export const cancel = () => Promise.resolve(); +export const cancelAll = () => Promise.resolve(); +export const active = () => Promise.resolve([]); +export const removeActive = () => Promise.resolve(); +export const removeAllActive = () => Promise.resolve(); +export const createChannel = () => Promise.resolve(); +export const deleteChannel = () => Promise.resolve(); +export const listChannels = () => Promise.resolve([]); +export const channels = listChannels; +export const onAction = () => Promise.resolve(() => {}); +export const onNotificationReceived = () => Promise.resolve(() => {}); diff --git a/apps/desktop/src/electron/opener.ts b/apps/desktop/src/electron/opener.ts new file mode 100644 index 00000000000..70383091215 --- /dev/null +++ b/apps/desktop/src/electron/opener.ts @@ -0,0 +1,4 @@ +import { native } from "./bridge"; +export const openPath = (path: string) => native("opener.openPath", { path }); +export const openUrl = (url: string) => native("shell.open", { path: url }); +export const revealItemInDir = (path: string) => native("opener.revealItemInDir", { path }); diff --git a/apps/desktop/src/electron/os.ts b/apps/desktop/src/electron/os.ts new file mode 100644 index 00000000000..af3a64ee499 --- /dev/null +++ b/apps/desktop/src/electron/os.ts @@ -0,0 +1,14 @@ +import { native } from "./bridge"; + +export type OsType = "linux" | "macos" | "ios" | "android" | "windows"; +export type Arch = "x86" | "x86_64" | "arm" | "aarch64" | "mips" | "mips64" | "powerpc" | "powerpc64" | "riscv64" | "s390x" | "sparc64"; +import { bridge } from "./bridge"; +export const arch = () => bridge().os.arch as Arch; +export const exeExtension = () => type() === "windows" ? ".exe" : ""; +export const family = () => type() === "windows" ? "windows" : "unix"; +export const hostname = () => native("os.hostname"); +export const locale = () => native("os.locale"); +export const platform = () => bridge().os.platform; +export const type = () => bridge().os.type as OsType; +export const version = () => bridge().os.version; +export const eol = () => "\n"; diff --git a/apps/desktop/src/electron/path.ts b/apps/desktop/src/electron/path.ts new file mode 100644 index 00000000000..6802967372d --- /dev/null +++ b/apps/desktop/src/electron/path.ts @@ -0,0 +1,37 @@ +import { native } from "./bridge"; + +export const appCacheDir = () => native("path.cacheDir"); +export const appConfigDir = () => native("path.configDir"); +export const appDataDir = () => native("path.appDataDir"); +export const appLocalDataDir = () => native("path.appLocalDataDir"); +export const appLogDir = () => native("path.appDataDir").then((value) => join(value, "logs")); +export const audioDir = () => native("path.audioDir"); +export const cacheDir = () => native("path.cacheDir"); +export const configDir = () => native("path.configDir"); +export const dataDir = () => native("path.dataDir"); +export const desktopDir = () => native("path.desktopDir"); +export const documentDir = () => native("path.documentDir"); +export const downloadDir = () => native("path.downloadDir"); +export const executableDir = () => native("path.resourceDir"); +export const homeDir = () => native("path.homeDir"); +export const localDataDir = () => native("path.appLocalDataDir"); +export const pictureDir = () => native("path.pictureDir"); +export const publicDir = () => native("path.homeDir"); +export const resourceDir = () => native("path.resourceDir"); +export const runtimeDir = () => native("path.tempDir"); +export const templateDir = () => native("path.homeDir"); +export const videoDir = () => native("path.videoDir"); +export const resolveResource = (resourcePath: string) => native("path.resolve", { paths: [resourcePath] }).then(async () => { + const root = await resourceDir(); + return join(root, resourcePath); +}); +export const tempDir = () => native("path.tempDir"); +export const join = (...paths: string[]) => native("path.join", { paths }); +export const resolve = (...paths: string[]) => native("path.resolve", { paths }); +export const normalize = (path: string) => native("path.resolve", { paths: [path] }); +export const dirname = (path: string) => native("path.resolve", { paths: [path, ".."] }); +export const extname = (path: string) => Promise.resolve(path.match(/\.[^./\\]+$/)?.[0] ?? ""); +export const basename = (path: string, ext?: string) => Promise.resolve(path.split(/[\\/]/).pop()?.replace(ext ?? /$^/, "") ?? ""); +export const isAbsolute = (path: string) => Promise.resolve(/^(?:[a-zA-Z]:[\\/]|\/)/.test(path)); +export const sep = () => native("os.type").then((value) => value === "windows" ? "\\" : "/"); +export const delimiter = () => native("os.type").then((value) => value === "windows" ? ";" : ":"); diff --git a/apps/desktop/src/electron/process.ts b/apps/desktop/src/electron/process.ts new file mode 100644 index 00000000000..1e0a1319e09 --- /dev/null +++ b/apps/desktop/src/electron/process.ts @@ -0,0 +1,3 @@ +import { native } from "./bridge"; +export const exit = (exitCode = 0) => native("app.exit", { exitCode }); +export const relaunch = () => native("app.relaunch"); diff --git a/apps/desktop/src/electron/shell.ts b/apps/desktop/src/electron/shell.ts new file mode 100644 index 00000000000..2e84d6245a8 --- /dev/null +++ b/apps/desktop/src/electron/shell.ts @@ -0,0 +1,14 @@ +import { native } from "./bridge"; + +export const open = (path: string, _openWith?: string) => native("shell.open", { path }); + +export class Command { + constructor(public program: string, public args: string[] = [], public options: Record = {}) {} + static create(program: string, args: string[] = [], options: Record = {}) { return new Command(program, args, options); } + static sidecar(program: string, args: string[] = [], options: Record = {}) { return new Command(program, args, options); } + execute() { return native("shell.execute", { program: this.program, args: this.args, options: this.options }); } + spawn() { return native("shell.spawn", { program: this.program, args: this.args, options: this.options }); } +} + +export interface CommandOutput { code: number; signal: number | null; stdout: string; stderr: string; } +export interface Child { pid: number; kill(): Promise; write(data: string | Uint8Array): Promise; } diff --git a/apps/desktop/src/electron/store.ts b/apps/desktop/src/electron/store.ts new file mode 100644 index 00000000000..f55f6b97a6f --- /dev/null +++ b/apps/desktop/src/electron/store.ts @@ -0,0 +1,38 @@ +import { native } from "./bridge"; + +export class Store { + private data = new Map(); + private loaded: Promise; + + constructor(public path: string, _options?: { autoSave?: boolean }) { + this.loaded = native>("store.load", { path }).then((values) => { + this.data = new Map(Object.entries(values)); + }); + } + + static async load(path: string, options?: { autoSave?: boolean }) { + const store = new Store(path, options); + await store.loaded; + return store; + } + + async set(key: string, value: unknown) { await this.loaded; this.data.set(key, value); } + async get(key: string): Promise { await this.loaded; return this.data.get(key) as T | undefined; } + async has(key: string) { await this.loaded; return this.data.has(key); } + async delete(key: string) { await this.loaded; return this.data.delete(key); } + async clear() { await this.loaded; this.data.clear(); } + async reset() { return this.clear(); } + async keys() { await this.loaded; return [...this.data.keys()]; } + async values() { await this.loaded; return [...this.data.values()]; } + async entries() { await this.loaded; return [...this.data.entries()]; } + async length() { await this.loaded; return this.data.size; } + async save() { await this.loaded; return native("store.save", { path: this.path, value: Object.fromEntries(this.data) }); } + async reload() { + const values = await native>("store.load", { path: this.path }); + this.data = new Map(Object.entries(values)); + } + async onKeyChange(_key: string, _callback: (value: T | undefined) => void) { return () => {}; } + async onChange(_callback: (key: string, value: T | undefined) => void) { return () => {}; } +} + +export const load = Store.load; diff --git a/apps/desktop/src/electron/updater.ts b/apps/desktop/src/electron/updater.ts new file mode 100644 index 00000000000..192303cb9e3 --- /dev/null +++ b/apps/desktop/src/electron/updater.ts @@ -0,0 +1,14 @@ +export interface CheckOptions { headers?: Record; timeout?: number; target?: string; } +export interface DownloadEvent { event: "Started" | "Progress" | "Finished"; data?: { contentLength?: number; chunkLength?: number }; } +export interface Update { + version: string; + currentVersion: string; + date?: string; + body?: string; + rawJson: Record; + download(listener?: (event: DownloadEvent) => void): Promise; + install(): Promise; + downloadAndInstall(listener?: (event: DownloadEvent) => void): Promise; + close(): Promise; +} +export async function check(_options?: CheckOptions): Promise { return null; } diff --git a/apps/desktop/src/electron/webviewWindow.ts b/apps/desktop/src/electron/webviewWindow.ts new file mode 100644 index 00000000000..99409f38d73 --- /dev/null +++ b/apps/desktop/src/electron/webviewWindow.ts @@ -0,0 +1,65 @@ +import { bridge, native } from "./bridge"; +import { Window } from "./window"; + +export interface WebviewWindowOptions { + url?: string; + title?: string; + x?: number; + y?: number; + width?: number; + height?: number; + minWidth?: number; + minHeight?: number; + transparent?: boolean; + decorations?: boolean; + resizable?: boolean; + alwaysOnTop?: boolean; + visibleOnAllWorkspaces?: boolean; + skipTaskbar?: boolean; + contentProtected?: boolean; + focus?: boolean; + visible?: boolean; + initialization?: Record; + [key: string]: unknown; +} + +export class WebviewWindow extends Window { + constructor(label: string, options?: WebviewWindowOptions) { + super(label); + if (options) { + void native("window.create", { + label, + route: options.url ?? "/", + ...options, + initialization: options.initialization ?? null, + }); + } + } + + static async getByLabel(label: string) { + const labels = await native("window.all"); + return labels.includes(label) ? new WebviewWindow(label) : null; + } + + static async getAll() { + return (await native("window.all")).map( + (label) => new WebviewWindow(label), + ); + } + + static getCurrent() { + return new WebviewWindow(bridge().windowLabel); + } + + emitTo(_target: string, event: string, payload?: unknown) { + return this.emit(event, payload); + } +} + +export function getCurrentWebviewWindow() { + return new WebviewWindow(bridge().windowLabel); +} + +export function getAllWebviewWindows() { + return WebviewWindow.getAll(); +} diff --git a/apps/desktop/src/electron/window.ts b/apps/desktop/src/electron/window.ts new file mode 100644 index 00000000000..77b0d57b259 --- /dev/null +++ b/apps/desktop/src/electron/window.ts @@ -0,0 +1,466 @@ +import { bridge, native } from "./bridge"; +import { + LogicalPosition, + LogicalSize, + PhysicalPosition, + PhysicalSize, + type Position, + type Size, +} from "./dpi"; +import { type Event, emit, listen, once, type UnlistenFn } from "./event"; + +export enum UserAttentionType { + Critical = 1, + Informational = 2, +} + +export enum ProgressBarStatus { + None = "none", + Normal = "normal", + Indeterminate = "indeterminate", + Paused = "paused", + Error = "error", +} + +export enum Effect { + AppearanceBased = "appearanceBased", + Light = "light", + Dark = "dark", + MediumLight = "mediumLight", + UltraDark = "ultraDark", + Titlebar = "titlebar", + Selection = "selection", + Menu = "menu", + Popover = "popover", + Sidebar = "sidebar", + HeaderView = "headerView", + Sheet = "sheet", + WindowBackground = "windowBackground", + HudWindow = "hudWindow", + FullScreenUI = "fullScreenUI", + Tooltip = "tooltip", + ContentBackground = "contentBackground", + UnderWindowBackground = "underWindowBackground", + UnderPageBackground = "underPageBackground", + Mica = "mica", + MicaDark = "micaDark", + MicaLight = "micaLight", + Tabbed = "tabbed", + TabbedDark = "tabbedDark", + TabbedLight = "tabbedLight", +} + +export enum EffectState { + FollowsWindowActiveState = "followsWindowActiveState", + Active = "active", + Inactive = "inactive", +} + +export interface Effects { + effects: Effect[]; + state?: EffectState; + radius?: number; + color?: string; +} + +export type Theme = "light" | "dark"; + +interface WindowState { + x: number; + y: number; + width: number; + height: number; + visible: boolean; + focused: boolean; + minimized: boolean; + maximized: boolean; + fullscreen: boolean; + alwaysOnTop: boolean; + resizable?: boolean; + decorated?: boolean; + scaleFactor: number; +} + +export interface Monitor { + name: string | null; + scaleFactor: number; + position: PhysicalPosition; + size: PhysicalSize; + workArea: { position: PhysicalPosition; size: PhysicalSize }; +} + +interface SerializedMonitor { + name: string | null; + scaleFactor: number; + position: { x: number; y: number }; + size: { width: number; height: number }; + workArea: { + position: { x: number; y: number }; + size: { width: number; height: number }; + }; +} + +function hydrateMonitor(monitor: SerializedMonitor | null): Monitor | null { + if (!monitor) return null; + return { + ...monitor, + position: new PhysicalPosition(monitor.position.x, monitor.position.y), + size: new PhysicalSize(monitor.size.width, monitor.size.height), + workArea: { + position: new PhysicalPosition( + monitor.workArea.position.x, + monitor.workArea.position.y, + ), + size: new PhysicalSize( + monitor.workArea.size.width, + monitor.workArea.size.height, + ), + }, + }; +} + +export class CloseRequestedEvent { + event = "tauri://close-requested"; + id = 0; + payload = undefined; + preventDefault() {} +} + +export class Window { + constructor(public label: string) {} + + static async getByLabel(label: string) { + const labels = await native("window.all"); + return labels.includes(label) ? new Window(label) : null; + } + + static async getAll() { + return (await native("window.all")).map( + (label) => new Window(label), + ); + } + + private action(action: string, value?: Record) { + return native("window.action", { label: this.label, action, value }); + } + + private state() { + return this.action("state"); + } + + async scaleFactor() { + return (await this.state()).scaleFactor; + } + async innerPosition() { + const state = await this.state(); + return new PhysicalPosition(state.x, state.y); + } + async outerPosition() { + return this.innerPosition(); + } + async innerSize() { + const state = await this.state(); + return new PhysicalSize(state.width, state.height); + } + async outerSize() { + return this.innerSize(); + } + async isFullscreen() { + return (await this.state()).fullscreen; + } + async isMinimized() { + return (await this.state()).minimized; + } + async isMaximized() { + return (await this.state()).maximized; + } + async isFocused() { + return (await this.state()).focused; + } + async isDecorated() { + return (await this.state()).decorated ?? false; + } + async isResizable() { + return (await this.state()).resizable ?? true; + } + async isMaximizable() { + return true; + } + async isMinimizable() { + return true; + } + async isClosable() { + return true; + } + async isVisible() { + return (await this.state()).visible; + } + async title() { + return document.title; + } + async currentMonitor() { + return hydrateMonitor( + await native("window.currentMonitor"), + ); + } + async primaryMonitor() { + return hydrateMonitor( + await native("window.primaryMonitor"), + ); + } + async monitorFromPoint(x: number, y: number) { + return hydrateMonitor( + await native("window.monitorFromPoint", { + x, + y, + }), + ); + } + async availableMonitors() { + return (await native("window.availableMonitors")).map( + (monitor) => hydrateMonitor(monitor) as Monitor, + ); + } + async theme(): Promise { + return native("theme.get"); + } + + center() { + return this.action("center"); + } + requestUserAttention(requestType: UserAttentionType | null) { + return this.action("requestUserAttention", { + critical: requestType === UserAttentionType.Critical, + }); + } + setResizable(resizable: boolean) { + return this.action("setResizable", { resizable }); + } + setEnabled(_enabled: boolean) { + return Promise.resolve(); + } + setMaximizable(_maximizable: boolean) { + return Promise.resolve(); + } + setMinimizable(_minimizable: boolean) { + return Promise.resolve(); + } + setClosable(_closable: boolean) { + return Promise.resolve(); + } + setTitle(title: string) { + return this.action("setTitle", { title }); + } + maximize() { + return this.action("maximize"); + } + unmaximize() { + return this.action("unmaximize"); + } + toggleMaximize = async () => + (await this.isMaximized()) ? this.unmaximize() : this.maximize(); + minimize = () => this.action("minimize"); + unminimize() { + return this.action("unminimize"); + } + show() { + return this.action("show"); + } + hide() { + return this.action("hide"); + } + close = () => this.action("close"); + destroy() { + return this.action("destroy"); + } + setFocus() { + return this.action("focus"); + } + setAlwaysOnTop(alwaysOnTop: boolean) { + return this.action("setAlwaysOnTop", { alwaysOnTop }); + } + setContentProtected(enabled: boolean) { + return this.action("setContentProtected", { enabled }); + } + setSize(size: Size) { + return this.action("setSize", { + width: size.width, + height: size.height, + physical: size instanceof PhysicalSize, + }); + } + setMinSize(size: Size | null) { + return this.action("setMinimumSize", { + width: size?.width, + height: size?.height, + physical: size instanceof PhysicalSize, + }); + } + setMaxSize(_size: Size | null) { + return Promise.resolve(); + } + setPosition(position: Position) { + return this.action("setPosition", { + x: position.x, + y: position.y, + physical: position instanceof PhysicalPosition, + }); + } + setFullscreen(fullscreen: boolean) { + return this.action("setFullscreen", { fullscreen }); + } + setEffects(_effects: Effects) { + return Promise.resolve(); + } + setProgressBar(options: { progress?: number; status?: ProgressBarStatus }) { + const progress = + options.status === ProgressBarStatus.None + ? null + : options.progress === undefined + ? 2 + : options.progress / 100; + return this.action("setProgress", { progress }); + } + setIcon(_icon: string | Uint8Array | ArrayBuffer | number[]) { + return Promise.resolve(); + } + setSkipTaskbar(skip: boolean) { + return this.action("setSkipTaskbar", { skip }); + } + setCursorGrab(_grab: boolean) { + return Promise.resolve(); + } + setCursorVisible(_visible: boolean) { + return Promise.resolve(); + } + setCursorIcon(_icon: string) { + return Promise.resolve(); + } + setCursorPosition(position: Position) { + return this.action("setCursorPosition", { + x: position.x, + y: position.y, + }); + } + setIgnoreCursorEvents(ignore: boolean) { + return this.action("setIgnoreCursorEvents", { ignore }); + } + startDragging() { + return Promise.resolve(); + } + startResizeDragging(_direction: string) { + return Promise.resolve(); + } + setBadgeCount(_count?: number) { + return Promise.resolve(); + } + setBadgeLabel(_label?: string) { + return Promise.resolve(); + } + setOverlayIcon(_icon?: unknown) { + return Promise.resolve(); + } + setShadow(_enable: boolean) { + return Promise.resolve(); + } + setTheme(_theme: Theme | null) { + return Promise.resolve(); + } + + listen(event: string, handler: (event: Event) => void) { + return listen(event, handler); + } + once(event: string, handler: (event: Event) => void) { + return once(event, handler); + } + emit(event: string, payload?: unknown) { + return emit(event, payload); + } + onResized( + handler: (event: Event) => void, + ): Promise { + return listen<{ width: number; height: number }>( + "tauri://resize", + (event) => + handler({ + ...event, + payload: new PhysicalSize(event.payload.width, event.payload.height), + }), + ); + } + onMoved( + handler: (event: Event) => void, + ): Promise { + return listen<{ x: number; y: number }>("tauri://move", (event) => + handler({ + ...event, + payload: new PhysicalPosition(event.payload.x, event.payload.y), + }), + ); + } + onCloseRequested( + handler: (event: CloseRequestedEvent) => void, + ): Promise { + return listen("tauri://close-requested", handler as never); + } + onDestroyed(handler: (event: Event) => void): Promise { + return listen("tauri://destroyed", handler); + } + onFocusChanged( + handler: (event: Event) => void, + ): Promise { + return listen("tauri://focus", handler); + } + onScaleChanged( + handler: ( + event: Event<{ scaleFactor: number; size: PhysicalSize }>, + ) => void, + ): Promise { + return listen<{ + scaleFactor: number; + size: { width: number; height: number }; + }>("tauri://scale-change", (event) => + handler({ + ...event, + payload: { + scaleFactor: event.payload.scaleFactor, + size: new PhysicalSize( + event.payload.size.width, + event.payload.size.height, + ), + }, + }), + ); + } + onThemeChanged(handler: (event: Event) => void): Promise { + return listen("tauri://theme-changed", handler); + } + onDragDropEvent( + handler: (event: Event) => void, + ): Promise { + return listen("tauri://drag-drop", handler); + } +} + +export function getCurrentWindow() { + return new Window(bridge().windowLabel); +} + +export function getAllWindows() { + return Window.getAll(); +} + +export async function currentMonitor() { + return getCurrentWindow().currentMonitor(); +} +export async function primaryMonitor() { + return getCurrentWindow().primaryMonitor(); +} +export async function monitorFromPoint(x: number, y: number) { + return getCurrentWindow().monitorFromPoint(x, y); +} +export async function availableMonitors() { + return getCurrentWindow().availableMonitors(); +} + +export { LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize }; diff --git a/apps/desktop/src/entry-client.tsx b/apps/desktop/src/entry-client.tsx index 4891061a043..344c2df455c 100644 --- a/apps/desktop/src/entry-client.tsx +++ b/apps/desktop/src/entry-client.tsx @@ -2,7 +2,7 @@ import { mount, StartClient } from "@solidjs/start/client"; function initPlatformClass() { - import("@tauri-apps/plugin-os") + import("~/electron/os") .then(({ type }) => { const osType = type(); document.documentElement.classList.add(`platform-${osType}`); diff --git a/apps/desktop/src/routes/(window-chrome).tsx b/apps/desktop/src/routes/(window-chrome).tsx index 8bb12b3c179..b4f581b5976 100644 --- a/apps/desktop/src/routes/(window-chrome).tsx +++ b/apps/desktop/src/routes/(window-chrome).tsx @@ -1,7 +1,7 @@ import { type RouteSectionProps, useLocation } from "@solidjs/router"; -import type { UnlistenFn } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import type { UnlistenFn } from "~/electron/event"; +import { getCurrentWindow } from "~/electron/window"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx index 4321547bb6d..5a5f9ecb93d 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx @@ -1,5 +1,5 @@ import { createTimer } from "@solid-primitives/timer"; -import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu"; +import { CheckMenuItem, Menu, PredefinedMenuItem } from "~/electron/menu"; import { cx } from "cva"; import { type Component, diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx index 132b271d45e..2bd8dbbc450 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx @@ -1,6 +1,6 @@ import { makePersisted } from "@solid-primitives/storage"; -import { getVersion } from "@tauri-apps/api/app"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getVersion } from "~/electron/app"; +import { getCurrentWindow } from "~/electron/window"; import { createEffect, createResource } from "solid-js"; import { createStore } from "solid-js/store"; import Tooltip from "~/components/Tooltip"; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx index d7365f9e338..fe7906de0cc 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx @@ -1,4 +1,4 @@ -import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu"; +import { CheckMenuItem, Menu, PredefinedMenuItem } from "~/electron/menu"; import { cx } from "cva"; import { type Component, diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx index 6d7fe4d83bf..a1375d73431 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx @@ -1,4 +1,4 @@ -import { convertFileSrc } from "@tauri-apps/api/core"; +import { convertFileSrc } from "~/electron/core"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx index c01e68f7593..0f2cf2bd102 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx @@ -1,8 +1,8 @@ import { ProgressCircle } from "@cap/ui-solid"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { ask, save } from "@tauri-apps/plugin-dialog"; -import { remove } from "@tauri-apps/plugin-fs"; -import * as shell from "@tauri-apps/plugin-shell"; +import { convertFileSrc } from "~/electron/core"; +import { ask, save } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import * as shell from "~/electron/shell"; import { cx } from "cva"; import type { ComponentProps } from "solid-js"; import { createMemo, createSignal, Show, splitProps } from "solid-js"; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index 3beeea4d6c4..2dbe56eb8e3 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -6,21 +6,21 @@ import { useQuery, useQueryClient, } from "@tanstack/solid-query"; -import { Channel } from "@tauri-apps/api/core"; -import { emit, listen } from "@tauri-apps/api/event"; +import { Channel } from "~/electron/core"; +import { emit, listen } from "~/electron/event"; import { getAllWebviewWindows, WebviewWindow, -} from "@tauri-apps/api/webviewWindow"; +} from "~/electron/webviewWindow"; import { currentMonitor, getCurrentWindow, LogicalSize, PhysicalPosition, -} from "@tauri-apps/api/window"; -import * as dialog from "@tauri-apps/plugin-dialog"; -import { relaunch } from "@tauri-apps/plugin-process"; -import * as shell from "@tauri-apps/plugin-shell"; +} from "~/electron/window"; +import * as dialog from "~/electron/dialog"; +import { relaunch } from "~/electron/process"; +import * as shell from "~/electron/shell"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/useRequestPermission.ts b/apps/desktop/src/routes/(window-chrome)/new-main/useRequestPermission.ts index cc448962f78..19842e3f23a 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/useRequestPermission.ts +++ b/apps/desktop/src/routes/(window-chrome)/new-main/useRequestPermission.ts @@ -1,5 +1,5 @@ import { useQueryClient } from "@tanstack/solid-query"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "~/electron/window"; import { devicesSnapshot } from "~/utils/devices"; import { requestAndVerifyPermission } from "~/utils/os-permissions"; import { commands, type OSPermissionStatus } from "~/utils/tauri"; diff --git a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx index b6383987a8f..fd195b5b5e9 100644 --- a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx +++ b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx @@ -1,10 +1,10 @@ import { Button } from "@cap/ui-solid"; import { makePersisted } from "@solid-primitives/storage"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { ask } from "@tauri-apps/plugin-dialog"; -import { type as ostype } from "@tauri-apps/plugin-os"; -import { relaunch } from "@tauri-apps/plugin-process"; -import * as shell from "@tauri-apps/plugin-shell"; +import { getCurrentWindow } from "~/electron/window"; +import { ask } from "~/electron/dialog"; +import { type as ostype } from "~/electron/os"; +import { relaunch } from "~/electron/process"; +import * as shell from "~/electron/shell"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/settings.tsx b/apps/desktop/src/routes/(window-chrome)/settings.tsx index ea3edf9ddea..8cb3d0bcb95 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings.tsx @@ -1,11 +1,11 @@ import { Button } from "@cap/ui-solid"; import { A, type RouteSectionProps, useNavigate } from "@solidjs/router"; import { createQuery, useQueryClient } from "@tanstack/solid-query"; -import { getVersion } from "@tauri-apps/api/app"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import * as dialog from "@tauri-apps/plugin-dialog"; -import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; -import * as shell from "@tauri-apps/plugin-shell"; +import { getVersion } from "~/electron/app"; +import { writeText } from "~/electron/clipboard"; +import * as dialog from "~/electron/dialog"; +import { fetch as tauriFetch } from "~/electron/http"; +import * as shell from "~/electron/shell"; import { createEffect, createMemo, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx b/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx index ff1a8555740..8300b1689cc 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx @@ -1,6 +1,6 @@ import { Button } from "@cap/ui-solid"; -import { CheckMenuItem, Menu } from "@tauri-apps/api/menu"; -import { open } from "@tauri-apps/plugin-dialog"; +import { CheckMenuItem, Menu } from "~/electron/menu"; +import { open } from "~/electron/dialog"; import { cx } from "cva"; import { type Component, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx b/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx index 715bf935db0..5e353bf7867 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx @@ -1,6 +1,6 @@ import { Button } from "@cap/ui-solid"; -import { invoke } from "@tauri-apps/api/core"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; +import { invoke } from "~/electron/core"; +import { writeText } from "~/electron/clipboard"; import { createResource, createSignal, Show } from "solid-js"; import toast from "solid-toast"; import { Section, SectionCard, SettingsPageContent } from "./Setting"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index 2e4356f9d0d..ce46f075e98 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -1,5 +1,5 @@ -import { invoke } from "@tauri-apps/api/core"; -import { type } from "@tauri-apps/plugin-os"; +import { invoke } from "~/electron/core"; +import { type } from "~/electron/os"; import { createResource, Show } from "solid-js"; import { createStore } from "solid-js/store"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx index 7a8d83b6d76..991bb11ce2c 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx @@ -1,8 +1,8 @@ import { Button } from "@cap/ui-solid"; import { action, useAction, useSubmission } from "@solidjs/router"; -import { getVersion } from "@tauri-apps/api/app"; -import { type OsType, type as ostype } from "@tauri-apps/plugin-os"; -import * as shell from "@tauri-apps/plugin-shell"; +import { getVersion } from "~/electron/app"; +import { type OsType, type as ostype } from "~/electron/os"; +import * as shell from "~/electron/shell"; import { createResource, createSignal, For, Show } from "solid-js"; import toast from "solid-toast"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx index 46e7b205259..6359dddb82a 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx @@ -3,12 +3,12 @@ import { createWritableMemo } from "@solid-primitives/memo"; import { isPermissionGranted, requestPermission, -} from "@tauri-apps/plugin-notification"; -import { type OsType, type } from "@tauri-apps/plugin-os"; +} from "~/electron/notification"; +import { type OsType, type } from "~/electron/os"; import "@total-typescript/ts-reset/filter-boolean"; import { Collapsible } from "@kobalte/core/collapsible"; -import { CheckMenuItem, Menu, MenuItem } from "@tauri-apps/api/menu"; -import { confirm } from "@tauri-apps/plugin-dialog"; +import { CheckMenuItem, Menu, MenuItem } from "~/electron/menu"; +import { confirm } from "~/electron/dialog"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx b/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx index 055d131fa29..a6069288173 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx @@ -1,5 +1,5 @@ import { createEventListener } from "@solid-primitives/event-listener"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { type as ostype } from "~/electron/os"; import { batch, createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index 461b804557c..c83797e0eb9 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -6,10 +6,10 @@ import { queryOptions, useQueryClient, } from "@tanstack/solid-query"; -import { Channel, convertFileSrc } from "@tauri-apps/api/core"; -import { ask, confirm } from "@tauri-apps/plugin-dialog"; -import { remove } from "@tauri-apps/plugin-fs"; -import * as shell from "@tauri-apps/plugin-shell"; +import { Channel, convertFileSrc } from "~/electron/core"; +import { ask, confirm } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import * as shell from "~/electron/shell"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx b/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx index 98363de3859..7e3e29c64a5 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx @@ -5,9 +5,9 @@ import { queryOptions, useQueryClient, } from "@tanstack/solid-query"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { ask } from "@tauri-apps/plugin-dialog"; -import { remove } from "@tauri-apps/plugin-fs"; +import { convertFileSrc } from "~/electron/core"; +import { ask } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; import { createEffect, createMemo, diff --git a/apps/desktop/src/routes/(window-chrome)/update.tsx b/apps/desktop/src/routes/(window-chrome)/update.tsx index ca9cb960f44..de61927714f 100644 --- a/apps/desktop/src/routes/(window-chrome)/update.tsx +++ b/apps/desktop/src/routes/(window-chrome)/update.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { useNavigate } from "@solidjs/router"; -import { getCurrentWindow, UserAttentionType } from "@tauri-apps/api/window"; -import { relaunch } from "@tauri-apps/plugin-process"; +import { getCurrentWindow, UserAttentionType } from "~/electron/window"; +import { relaunch } from "~/electron/process"; import { createResource, createSignal, diff --git a/apps/desktop/src/routes/(window-chrome)/upgrade.tsx b/apps/desktop/src/routes/(window-chrome)/upgrade.tsx index c9fd22ef7c8..f65364d1eea 100644 --- a/apps/desktop/src/routes/(window-chrome)/upgrade.tsx +++ b/apps/desktop/src/routes/(window-chrome)/upgrade.tsx @@ -1,6 +1,6 @@ import { Button } from "@cap/ui-solid"; import { createMutation, useQueryClient } from "@tanstack/solid-query"; -import { getCurrentWindow, Window } from "@tauri-apps/api/window"; +import { getCurrentWindow, Window } from "~/electron/window"; import { type Accessor, createSignal, Show } from "solid-js"; import { generalSettingsStore } from "~/store"; import { getProPlanId } from "~/utils/plans"; diff --git a/apps/desktop/src/routes/camera.tsx b/apps/desktop/src/routes/camera.tsx index f8e88825f45..58f8c630791 100644 --- a/apps/desktop/src/routes/camera.tsx +++ b/apps/desktop/src/routes/camera.tsx @@ -1,13 +1,13 @@ import { makePersisted } from "@solid-primitives/storage"; -import { listen } from "@tauri-apps/api/event"; +import { listen } from "~/electron/event"; import { availableMonitors, currentMonitor, getCurrentWindow, LogicalPosition, LogicalSize, -} from "@tauri-apps/api/window"; -import { type } from "@tauri-apps/plugin-os"; +} from "~/electron/window"; +import { type } from "~/electron/os"; import { cx } from "cva"; import { type Accessor, diff --git a/apps/desktop/src/routes/capture-area.tsx b/apps/desktop/src/routes/capture-area.tsx index 0a2a3feeb17..014342121c7 100644 --- a/apps/desktop/src/routes/capture-area.tsx +++ b/apps/desktop/src/routes/capture-area.tsx @@ -1,13 +1,13 @@ import { createEventListener } from "@solid-primitives/event-listener"; import { createScheduled, debounce } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; -import { LogicalPosition } from "@tauri-apps/api/dpi"; -import { Menu } from "@tauri-apps/api/menu"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu } from "~/electron/menu"; import { getCurrentWebviewWindow, WebviewWindow, -} from "@tauri-apps/api/webviewWindow"; -import { type as ostype } from "@tauri-apps/plugin-os"; +} from "~/electron/webviewWindow"; +import { type as ostype } from "~/electron/os"; import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { Transition } from "solid-transition-group"; diff --git a/apps/desktop/src/routes/debug.tsx b/apps/desktop/src/routes/debug.tsx index dcb1c4a2bce..e0cc57c36cd 100644 --- a/apps/desktop/src/routes/debug.tsx +++ b/apps/desktop/src/routes/debug.tsx @@ -1,8 +1,8 @@ import { useNavigate } from "@solidjs/router"; import { createQuery } from "@tanstack/solid-query"; -import { getVersion } from "@tauri-apps/api/app"; -import * as dialog from "@tauri-apps/plugin-dialog"; -import { check } from "@tauri-apps/plugin-updater"; +import { getVersion } from "~/electron/app"; +import * as dialog from "~/electron/dialog"; +import { check } from "~/electron/updater"; import { createSignal, createUniqueId, For, onMount } from "solid-js"; import { commands } from "~/utils/tauri"; diff --git a/apps/desktop/src/routes/editor/AudioLibrary.tsx b/apps/desktop/src/routes/editor/AudioLibrary.tsx index 550dacd6ecd..269409f2425 100644 --- a/apps/desktop/src/routes/editor/AudioLibrary.tsx +++ b/apps/desktop/src/routes/editor/AudioLibrary.tsx @@ -1,4 +1,4 @@ -import { open } from "@tauri-apps/plugin-dialog"; +import { open } from "~/electron/dialog"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/editor/CaptionsTab.tsx b/apps/desktop/src/routes/editor/CaptionsTab.tsx index cb78dd7b6ed..3e767e35b4d 100644 --- a/apps/desktop/src/routes/editor/CaptionsTab.tsx +++ b/apps/desktop/src/routes/editor/CaptionsTab.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { Select as KSelect } from "@kobalte/core/select"; -import { appLocalDataDir, join } from "@tauri-apps/api/path"; -import { exists } from "@tauri-apps/plugin-fs"; +import { appLocalDataDir, join } from "~/electron/path"; +import { exists } from "~/electron/fs"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/editor/ClipsSidebar.tsx b/apps/desktop/src/routes/editor/ClipsSidebar.tsx index 74b5e361178..7abeac2121a 100644 --- a/apps/desktop/src/routes/editor/ClipsSidebar.tsx +++ b/apps/desktop/src/routes/editor/ClipsSidebar.tsx @@ -1,13 +1,13 @@ import { Button } from "@cap/ui-solid"; import { createEventListener } from "@solid-primitives/event-listener"; import { useQuery } from "@tanstack/solid-query"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { LogicalPosition } from "@tauri-apps/api/dpi"; -import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import { appDataDir, join } from "@tauri-apps/api/path"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { open } from "@tauri-apps/plugin-dialog"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { convertFileSrc } from "~/electron/core"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu, MenuItem } from "~/electron/menu"; +import { appDataDir, join } from "~/electron/path"; +import { getCurrentWindow } from "~/electron/window"; +import { open } from "~/electron/dialog"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { type Component, diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 67774872432..b4618bab648 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -13,15 +13,15 @@ import { Tabs as KTabs } from "@kobalte/core/tabs"; import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListenerMap } from "@solid-primitives/event-listener"; import { createWritableMemo } from "@solid-primitives/memo"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { appDataDir, resolveResource } from "@tauri-apps/api/path"; +import { convertFileSrc } from "~/electron/core"; +import { appDataDir, resolveResource } from "~/electron/path"; import { BaseDirectory, exists, readDir, writeFile, -} from "@tauri-apps/plugin-fs"; -import { type as ostype } from "@tauri-apps/plugin-os"; +} from "~/electron/fs"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { batch, diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index 52c839204d4..6e54efb8f9f 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -5,11 +5,11 @@ import { trackDeep } from "@solid-primitives/deep"; import { debounce, throttle } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation, createQuery, skipToken } from "@tanstack/solid-query"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { LogicalPosition } from "@tauri-apps/api/dpi"; -import { Menu } from "@tauri-apps/api/menu"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { ask } from "@tauri-apps/plugin-dialog"; +import { convertFileSrc } from "~/electron/core"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu } from "~/electron/menu"; +import { getCurrentWindow } from "~/electron/window"; +import { ask } from "~/electron/dialog"; import { createEffect, createMemo, diff --git a/apps/desktop/src/routes/editor/EditorErrorScreen.tsx b/apps/desktop/src/routes/editor/EditorErrorScreen.tsx index 197891c257d..41c8d130621 100644 --- a/apps/desktop/src/routes/editor/EditorErrorScreen.tsx +++ b/apps/desktop/src/routes/editor/EditorErrorScreen.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { createMutation } from "@tanstack/solid-query"; -import { revealItemInDir } from "@tauri-apps/plugin-opener"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { revealItemInDir } from "~/electron/opener"; +import { type as ostype } from "~/electron/os"; import { Show } from "solid-js"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; import { commands } from "~/utils/tauri"; diff --git a/apps/desktop/src/routes/editor/ExportPage.tsx b/apps/desktop/src/routes/editor/ExportPage.tsx index 370a891c062..f415e455d02 100644 --- a/apps/desktop/src/routes/editor/ExportPage.tsx +++ b/apps/desktop/src/routes/editor/ExportPage.tsx @@ -2,11 +2,11 @@ import { Button } from "@cap/ui-solid"; import { debounce } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation } from "@tanstack/solid-query"; -import { Channel } from "@tauri-apps/api/core"; -import { CheckMenuItem, Menu } from "@tauri-apps/api/menu"; -import { ask } from "@tauri-apps/plugin-dialog"; -import { remove } from "@tauri-apps/plugin-fs"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { Channel } from "~/electron/core"; +import { CheckMenuItem, Menu } from "~/electron/menu"; +import { ask } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index 549e512af9e..dd9216e81d4 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -1,8 +1,8 @@ import { Button } from "@cap/ui-solid"; -import type { UnlistenFn } from "@tauri-apps/api/event"; -import { ask } from "@tauri-apps/plugin-dialog"; -import { revealItemInDir } from "@tauri-apps/plugin-opener"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import type { UnlistenFn } from "~/electron/event"; +import { ask } from "~/electron/dialog"; +import { revealItemInDir } from "~/electron/opener"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { type ComponentProps, diff --git a/apps/desktop/src/routes/editor/ImportProgress.tsx b/apps/desktop/src/routes/editor/ImportProgress.tsx index d69ef337873..cf3537f6d7a 100644 --- a/apps/desktop/src/routes/editor/ImportProgress.tsx +++ b/apps/desktop/src/routes/editor/ImportProgress.tsx @@ -1,5 +1,5 @@ import { Button } from "@cap/ui-solid"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "~/electron/window"; import { createSignal, Match, onCleanup, onMount, Switch } from "solid-js"; import { events, diff --git a/apps/desktop/src/routes/editor/PerformanceOverlay.tsx b/apps/desktop/src/routes/editor/PerformanceOverlay.tsx index 2a5f1438376..03a17b0a0ab 100644 --- a/apps/desktop/src/routes/editor/PerformanceOverlay.tsx +++ b/apps/desktop/src/routes/editor/PerformanceOverlay.tsx @@ -1,4 +1,4 @@ -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; +import { writeText } from "~/electron/clipboard"; import { createEffect, createMemo, diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index 1da2c7e40f4..0782d38aa30 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -2,8 +2,8 @@ import { Select as KSelect } from "@kobalte/core/select"; import { ToggleButton as KToggleButton } from "@kobalte/core/toggle-button"; import { createElementBounds } from "@solid-primitives/bounds"; import { debounce } from "@solid-primitives/scheduled"; -import { Menu } from "@tauri-apps/api/menu"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { Menu } from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, createSignal, onMount, Show } from "solid-js"; diff --git a/apps/desktop/src/routes/editor/ShareButton.tsx b/apps/desktop/src/routes/editor/ShareButton.tsx index a53fc08a6a7..922b1f548b7 100644 --- a/apps/desktop/src/routes/editor/ShareButton.tsx +++ b/apps/desktop/src/routes/editor/ShareButton.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { Select as KSelect } from "@kobalte/core/select"; import { createMutation } from "@tanstack/solid-query"; -import { Channel } from "@tauri-apps/api/core"; +import { Channel } from "~/electron/core"; import { createSignal, Show } from "solid-js"; import { createStore, produce, reconcile } from "solid-js/store"; import Tooltip from "~/components/Tooltip"; diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 7f815c0dc53..3a3376e0303 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -1,6 +1,6 @@ import { Button } from "@cap/ui-solid"; import { createEventListenerMap } from "@solid-primitives/event-listener"; -import { Menu } from "@tauri-apps/api/menu"; +import { Menu } from "~/electron/menu"; import { cx } from "cva"; import { Array, Option } from "effect"; import { diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 4cd4af15822..492b33e7e7a 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1,8 +1,8 @@ import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListener } from "@solid-primitives/event-listener"; -import { LogicalPosition } from "@tauri-apps/api/dpi"; -import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import { platform } from "@tauri-apps/plugin-os"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu, MenuItem } from "~/electron/menu"; +import { platform } from "~/electron/os"; import { cx } from "cva"; import { batch, diff --git a/apps/desktop/src/routes/editor/TranscriptPage.tsx b/apps/desktop/src/routes/editor/TranscriptPage.tsx index dd3b7710e73..9047b87c25a 100644 --- a/apps/desktop/src/routes/editor/TranscriptPage.tsx +++ b/apps/desktop/src/routes/editor/TranscriptPage.tsx @@ -1,7 +1,7 @@ import { createEventListener } from "@solid-primitives/event-listener"; import { makePersisted } from "@solid-primitives/storage"; -import { save } from "@tauri-apps/plugin-dialog"; -import { writeTextFile } from "@tauri-apps/plugin-fs"; +import { save } from "~/electron/dialog"; +import { writeTextFile } from "~/electron/fs"; import { cx } from "cva"; import { batch, diff --git a/apps/desktop/src/routes/editor/captions.ts b/apps/desktop/src/routes/editor/captions.ts index c500e407950..9a3abb7e3b0 100644 --- a/apps/desktop/src/routes/editor/captions.ts +++ b/apps/desktop/src/routes/editor/captions.ts @@ -1,5 +1,5 @@ -import { appLocalDataDir, join } from "@tauri-apps/api/path"; -import { arch, type as osType } from "@tauri-apps/plugin-os"; +import { appLocalDataDir, join } from "~/electron/path"; +import { arch, type as osType } from "~/electron/os"; import { defaultCaptionSettings } from "~/store/captions"; import { diff --git a/apps/desktop/src/routes/editor/editor-skeleton.tsx b/apps/desktop/src/routes/editor/editor-skeleton.tsx index 94b57eaacc6..fc4eb2692c1 100644 --- a/apps/desktop/src/routes/editor/editor-skeleton.tsx +++ b/apps/desktop/src/routes/editor/editor-skeleton.tsx @@ -1,4 +1,4 @@ -import { type as ostype } from "@tauri-apps/plugin-os"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; diff --git a/apps/desktop/src/routes/editor/existing-recording-picker.ts b/apps/desktop/src/routes/editor/existing-recording-picker.ts index 5c443399d43..cccb87131cc 100644 --- a/apps/desktop/src/routes/editor/existing-recording-picker.ts +++ b/apps/desktop/src/routes/editor/existing-recording-picker.ts @@ -1,5 +1,5 @@ -import type { OpenDialogOptions } from "@tauri-apps/plugin-dialog"; -import type { OsType } from "@tauri-apps/plugin-os"; +import type { OpenDialogOptions } from "~/electron/dialog"; +import type { OsType } from "~/electron/os"; export const getExistingRecordingPickerOptions = ( platform: OsType, diff --git a/apps/desktop/src/routes/editor/index.tsx b/apps/desktop/src/routes/editor/index.tsx index b4cabd2d1e8..2edaac2ac52 100644 --- a/apps/desktop/src/routes/editor/index.tsx +++ b/apps/desktop/src/routes/editor/index.tsx @@ -1,5 +1,5 @@ -import { Effect, getCurrentWindow } from "@tauri-apps/api/window"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { Effect, getCurrentWindow } from "~/electron/window"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, onMount, Suspense } from "solid-js"; import { generalSettingsStore } from "~/store"; diff --git a/apps/desktop/src/routes/editor/utils.ts b/apps/desktop/src/routes/editor/utils.ts index 17b265a9cc2..3280328925c 100644 --- a/apps/desktop/src/routes/editor/utils.ts +++ b/apps/desktop/src/routes/editor/utils.ts @@ -12,7 +12,7 @@ export function formatTime(secs: number, fps?: number) { return str; } -import { getCurrentWindow, ProgressBarStatus } from "@tauri-apps/api/window"; +import { getCurrentWindow, ProgressBarStatus } from "~/electron/window"; import { createEffect } from "solid-js"; export function createProgressBar(progress: () => number | undefined) { diff --git a/apps/desktop/src/routes/in-progress-recording.tsx b/apps/desktop/src/routes/in-progress-recording.tsx index 7afd88628d8..cb19116d94e 100644 --- a/apps/desktop/src/routes/in-progress-recording.tsx +++ b/apps/desktop/src/routes/in-progress-recording.tsx @@ -1,15 +1,15 @@ import { createTimer } from "@solid-primitives/timer"; import { createMutation } from "@tanstack/solid-query"; -import { LogicalPosition } from "@tauri-apps/api/dpi"; +import { LogicalPosition } from "~/electron/dpi"; import { CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, -} from "@tauri-apps/api/menu"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import * as dialog from "@tauri-apps/plugin-dialog"; -import { type as ostype } from "@tauri-apps/plugin-os"; +} from "~/electron/menu"; +import { getCurrentWindow } from "~/electron/window"; +import * as dialog from "~/electron/dialog"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { type ComponentProps, diff --git a/apps/desktop/src/routes/mode-select.tsx b/apps/desktop/src/routes/mode-select.tsx index 669cb414dba..48d34e92fcc 100644 --- a/apps/desktop/src/routes/mode-select.tsx +++ b/apps/desktop/src/routes/mode-select.tsx @@ -1,6 +1,6 @@ -import type { UnlistenFn } from "@tauri-apps/api/event"; -import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import type { UnlistenFn } from "~/electron/event"; +import { getCurrentWindow, LogicalSize } from "~/electron/window"; +import { type as ostype } from "~/electron/os"; import { onCleanup, onMount } from "solid-js"; import ModeSelect from "~/components/ModeSelect"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; diff --git a/apps/desktop/src/routes/recordings-overlay.tsx b/apps/desktop/src/routes/recordings-overlay.tsx index a9848daeaf3..efa4652cbc1 100644 --- a/apps/desktop/src/routes/recordings-overlay.tsx +++ b/apps/desktop/src/routes/recordings-overlay.tsx @@ -3,7 +3,7 @@ import Tooltip from "@corvu/tooltip"; import { createElementBounds } from "@solid-primitives/bounds"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation, createQuery } from "@tanstack/solid-query"; -import { Channel, convertFileSrc } from "@tauri-apps/api/core"; +import { Channel, convertFileSrc } from "~/electron/core"; import { cx } from "cva"; import { type Accessor, diff --git a/apps/desktop/src/routes/screenshot-editor/Editor.tsx b/apps/desktop/src/routes/screenshot-editor/Editor.tsx index 53cf42cb508..03361cbbfa9 100644 --- a/apps/desktop/src/routes/screenshot-editor/Editor.tsx +++ b/apps/desktop/src/routes/screenshot-editor/Editor.tsx @@ -1,9 +1,9 @@ import { Button } from "@cap/ui-solid"; import { NumberField } from "@kobalte/core/number-field"; import { makePersisted } from "@solid-primitives/storage"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { LogicalPosition } from "@tauri-apps/api/dpi"; -import { Menu } from "@tauri-apps/api/menu"; +import { convertFileSrc } from "~/electron/core"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu } from "~/electron/menu"; import { createEffect, createSignal, diff --git a/apps/desktop/src/routes/screenshot-editor/Header.tsx b/apps/desktop/src/routes/screenshot-editor/Header.tsx index a41171e16a5..d0a3069b8f4 100644 --- a/apps/desktop/src/routes/screenshot-editor/Header.tsx +++ b/apps/desktop/src/routes/screenshot-editor/Header.tsx @@ -1,9 +1,9 @@ import { DropdownMenu } from "@kobalte/core/dropdown-menu"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { ask } from "@tauri-apps/plugin-dialog"; -import { remove } from "@tauri-apps/plugin-fs"; -import { revealItemInDir } from "@tauri-apps/plugin-opener"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { getCurrentWindow } from "~/electron/window"; +import { ask } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import { revealItemInDir } from "~/electron/opener"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, onCleanup, Suspense } from "solid-js"; import CaptionControlsMacOS from "~/components/titlebar/controls/CaptionControlsMacOS"; diff --git a/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx b/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx index 3e2a7844c5c..091d76708ed 100644 --- a/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx +++ b/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke } from "~/electron/core"; import { createEffect, createMemo, createSignal, For } from "solid-js"; import { type ScreenshotProject, useScreenshotEditorContext } from "./context"; diff --git a/apps/desktop/src/routes/screenshot-editor/context.tsx b/apps/desktop/src/routes/screenshot-editor/context.tsx index b7e5bcef3e1..96c8ff35138 100644 --- a/apps/desktop/src/routes/screenshot-editor/context.tsx +++ b/apps/desktop/src/routes/screenshot-editor/context.tsx @@ -2,7 +2,7 @@ import { createContextProvider } from "@solid-primitives/context"; import { trackStore } from "@solid-primitives/deep"; import { debounce, throttle } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; -import { convertFileSrc, invoke } from "@tauri-apps/api/core"; +import { convertFileSrc, invoke } from "~/electron/core"; import { createEffect, createResource, diff --git a/apps/desktop/src/routes/screenshot-editor/index.tsx b/apps/desktop/src/routes/screenshot-editor/index.tsx index f1d993c6033..005164dd3ef 100644 --- a/apps/desktop/src/routes/screenshot-editor/index.tsx +++ b/apps/desktop/src/routes/screenshot-editor/index.tsx @@ -1,5 +1,5 @@ -import { Effect, getCurrentWindow } from "@tauri-apps/api/window"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { Effect, getCurrentWindow } from "~/electron/window"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect } from "solid-js"; import { generalSettingsStore } from "~/store"; diff --git a/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx b/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx index 4023be1224e..659d8bc830a 100644 --- a/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx +++ b/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx @@ -1,9 +1,9 @@ import { Popover } from "@kobalte/core/popover"; import { RadioGroup as KRadioGroup } from "@kobalte/core/radio-group"; import { Tabs as KTabs } from "@kobalte/core/tabs"; -import { convertFileSrc, invoke } from "@tauri-apps/api/core"; -import { appDataDir, resolveResource } from "@tauri-apps/api/path"; -import { BaseDirectory, writeFile } from "@tauri-apps/plugin-fs"; +import { convertFileSrc, invoke } from "~/electron/core"; +import { appDataDir, resolveResource } from "~/electron/path"; +import { BaseDirectory, writeFile } from "~/electron/fs"; import { batch, createMemo, diff --git a/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx b/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx index 28257ed849c..9f3f0ad57fd 100644 --- a/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx +++ b/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx @@ -1,4 +1,4 @@ -import { type as ostype } from "@tauri-apps/plugin-os"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import IconCapLogo from "~icons/cap/logo"; diff --git a/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts b/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts index dad98445232..35df707b0cf 100644 --- a/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts +++ b/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts @@ -1,5 +1,5 @@ -import { save } from "@tauri-apps/plugin-dialog"; -import { writeFile } from "@tauri-apps/plugin-fs"; +import { save } from "~/electron/dialog"; +import { writeFile } from "~/electron/fs"; import { createSignal } from "solid-js"; import { unwrap } from "solid-js/store"; import toast from "solid-toast"; diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index 772fc3f0051..5f32f7513a2 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -9,16 +9,16 @@ import { LogicalPosition, type PhysicalPosition, type PhysicalSize, -} from "@tauri-apps/api/dpi"; -import { emit } from "@tauri-apps/api/event"; +} from "~/electron/dpi"; +import { emit } from "~/electron/event"; import { CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, -} from "@tauri-apps/api/menu"; -import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { type as ostype } from "@tauri-apps/plugin-os"; +} from "~/electron/menu"; +import { WebviewWindow } from "~/electron/webviewWindow"; +import { type as ostype } from "~/electron/os"; import { createEffect, createMemo, diff --git a/apps/desktop/src/routes/teleprompter.tsx b/apps/desktop/src/routes/teleprompter.tsx index 2bc2bf2806b..ea77390e73f 100644 --- a/apps/desktop/src/routes/teleprompter.tsx +++ b/apps/desktop/src/routes/teleprompter.tsx @@ -1,6 +1,6 @@ -import type { UnlistenFn } from "@tauri-apps/api/event"; -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import type { UnlistenFn } from "~/electron/event"; +import { getCurrentWebviewWindow } from "~/electron/webviewWindow"; +import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, diff --git a/apps/desktop/src/routes/window-capture-occluder.tsx b/apps/desktop/src/routes/window-capture-occluder.tsx index a67f95202b9..7586785cc02 100644 --- a/apps/desktop/src/routes/window-capture-occluder.tsx +++ b/apps/desktop/src/routes/window-capture-occluder.tsx @@ -1,4 +1,4 @@ -import { getAllWindows } from "@tauri-apps/api/window"; +import { getAllWindows } from "~/electron/window"; import { Show, Suspense } from "solid-js"; import { isCameraWindowLabel } from "~/utils/camera-window"; import { createCurrentRecordingQuery } from "~/utils/queries"; diff --git a/apps/desktop/src/store.ts b/apps/desktop/src/store.ts index c2b30eb6c56..f07f2a729d8 100644 --- a/apps/desktop/src/store.ts +++ b/apps/desktop/src/store.ts @@ -1,5 +1,5 @@ import { createQuery } from "@tanstack/solid-query"; -import { Store } from "@tauri-apps/plugin-store"; +import { Store } from "~/electron/store"; import { onCleanup } from "solid-js"; import type { AutomationsStore } from "~/utils/automations"; import { diff --git a/apps/desktop/src/styles/theme.css b/apps/desktop/src/styles/theme.css index 330b4fd1284..546370cb629 100644 --- a/apps/desktop/src/styles/theme.css +++ b/apps/desktop/src/styles/theme.css @@ -19,6 +19,24 @@ @tailwind utilities; +/* Tauri handled this attribute internally. Electron frameless windows use + * Chromium's app-region CSS contract for the same behavior. */ +[data-tauri-drag-region]:not([data-tauri-drag-region="false"]) { + -webkit-app-region: drag; + app-region: drag; +} + +[data-tauri-drag-region="false"], +[data-tauri-drag-region] button, +[data-tauri-drag-region] a, +[data-tauri-drag-region] input, +[data-tauri-drag-region] select, +[data-tauri-drag-region] textarea, +[data-tauri-drag-region] [role="button"] { + -webkit-app-region: no-drag; + app-region: no-drag; +} + /* Single source of truth for timeline track colours. Both the timeline * segments and the "Add track" picker read these, so they always match. */ :root { diff --git a/apps/desktop/src/utils/analytics.ts b/apps/desktop/src/utils/analytics.ts index fb213508316..ff1087e1541 100644 --- a/apps/desktop/src/utils/analytics.ts +++ b/apps/desktop/src/utils/analytics.ts @@ -1,5 +1,5 @@ import { OpenPanel } from "@openpanel/web"; -import { Store } from "@tauri-apps/plugin-store"; +import { Store } from "~/electron/store"; const clientId = import.meta.env.VITE_OPENPANEL_CLIENT_ID as string | undefined; const apiUrl = import.meta.env.VITE_OPENPANEL_API_URL as string | undefined; diff --git a/apps/desktop/src/utils/auth.ts b/apps/desktop/src/utils/auth.ts index 60c55d0b8d3..5b1ae9c3f34 100644 --- a/apps/desktop/src/utils/auth.ts +++ b/apps/desktop/src/utils/auth.ts @@ -1,9 +1,9 @@ import { createMutation } from "@tanstack/solid-query"; -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; -import * as shell from "@tauri-apps/plugin-shell"; +import { invoke } from "~/electron/core"; +import { listen } from "~/electron/event"; +import { getCurrentWindow } from "~/electron/window"; +import { onOpenUrl } from "~/electron/deep-link"; +import * as shell from "~/electron/shell"; import { z } from "zod"; import callbackTemplate from "~/components/callback.template"; import { authStore, generalSettingsStore } from "~/store"; diff --git a/apps/desktop/src/utils/camera-window.ts b/apps/desktop/src/utils/camera-window.ts index 7e5d08dafcd..b25179dc059 100644 --- a/apps/desktop/src/utils/camera-window.ts +++ b/apps/desktop/src/utils/camera-window.ts @@ -1,4 +1,4 @@ -import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { WebviewWindow } from "~/electron/webviewWindow"; const CAMERA_WINDOW_PREFIX = "camera-"; diff --git a/apps/desktop/src/utils/export.ts b/apps/desktop/src/utils/export.ts index 85be0b9cb3c..9645320168a 100644 --- a/apps/desktop/src/utils/export.ts +++ b/apps/desktop/src/utils/export.ts @@ -1,4 +1,4 @@ -import { Channel } from "@tauri-apps/api/core"; +import { Channel } from "~/electron/core"; import { commands, type ExportSettings, type FramesRendered } from "./tauri"; export async function beginExportSessionGuard() { diff --git a/apps/desktop/src/utils/importMedia.ts b/apps/desktop/src/utils/importMedia.ts index 59401c42234..0c88349842d 100644 --- a/apps/desktop/src/utils/importMedia.ts +++ b/apps/desktop/src/utils/importMedia.ts @@ -1,6 +1,6 @@ -import { invoke } from "@tauri-apps/api/core"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import * as dialog from "@tauri-apps/plugin-dialog"; +import { invoke } from "~/electron/core"; +import { getCurrentWindow } from "~/electron/window"; +import * as dialog from "~/electron/dialog"; import { commands } from "~/utils/tauri"; const videoExtensions = [ diff --git a/apps/desktop/src/utils/macos-window-material.ts b/apps/desktop/src/utils/macos-window-material.ts index 2a9993d036e..7c92ee112fb 100644 --- a/apps/desktop/src/utils/macos-window-material.ts +++ b/apps/desktop/src/utils/macos-window-material.ts @@ -1,12 +1,12 @@ -import { invoke } from "@tauri-apps/api/core"; -import type { UnlistenFn } from "@tauri-apps/api/event"; +import { invoke } from "~/electron/core"; +import type { UnlistenFn } from "~/electron/event"; import { Effect, EffectState, type Effects, getCurrentWindow, -} from "@tauri-apps/api/window"; -import { type as osType, version as osVersion } from "@tauri-apps/plugin-os"; +} from "~/electron/window"; +import { type as osType, version as osVersion } from "~/electron/os"; type MacOSWindowMaterial = "panel" | "settings" | "teleprompter"; diff --git a/apps/desktop/src/utils/queries.ts b/apps/desktop/src/utils/queries.ts index 4118d0cb9f6..915cd59c13d 100644 --- a/apps/desktop/src/utils/queries.ts +++ b/apps/desktop/src/utils/queries.ts @@ -6,7 +6,7 @@ import { useMutation, useQuery, } from "@tanstack/solid-query"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "~/electron/window"; import { batch, createEffect, createMemo, onCleanup } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { useRecordingOptions } from "~/routes/(window-chrome)/OptionsContext"; diff --git a/apps/desktop/src/utils/recording.ts b/apps/desktop/src/utils/recording.ts index ce1ef9f06f1..db9e7917d03 100644 --- a/apps/desktop/src/utils/recording.ts +++ b/apps/desktop/src/utils/recording.ts @@ -1,6 +1,6 @@ -import { emit } from "@tauri-apps/api/event"; -import * as dialog from "@tauri-apps/plugin-dialog"; -import { revealItemInDir } from "@tauri-apps/plugin-opener"; +import { emit } from "~/electron/event"; +import * as dialog from "~/electron/dialog"; +import { revealItemInDir } from "~/electron/opener"; import type { createOptionsQuery } from "./queries"; import { commands, type RecordingAction, type RecordingMode } from "./tauri"; diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 62f92eace7c..20522d91015 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1164,9 +1164,9 @@ export type ZoomSegment = { start: number; end: number; amount: number; mode: Zo import { invoke as TAURI_INVOKE, Channel as TAURI_CHANNEL, -} from "@tauri-apps/api/core"; -import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +} from "~/electron/core"; +import * as TAURI_API_EVENT from "~/electron/event"; +import { type WebviewWindow as __WebviewWindow__ } from "~/electron/webviewWindow"; type __EventObj__ = { listen: ( diff --git a/apps/desktop/src/utils/tauriSpectaHack.ts b/apps/desktop/src/utils/tauriSpectaHack.ts index 51cb29ed644..6d8394cc3e9 100644 --- a/apps/desktop/src/utils/tauriSpectaHack.ts +++ b/apps/desktop/src/utils/tauriSpectaHack.ts @@ -1,7 +1,7 @@ // Workaround for: https://github.com/specta-rs/tauri-specta/issues/187 -import type * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import type { WebviewWindow } from "@tauri-apps/api/webviewWindow"; +import type * as TAURI_API_EVENT from "~/electron/event"; +import type { WebviewWindow } from "~/electron/webviewWindow"; import type { events } from "./tauri"; type __EventObj__ = { diff --git a/apps/desktop/src/utils/teleprompter-window-options.ts b/apps/desktop/src/utils/teleprompter-window-options.ts index 0a78e32dca7..aae17a115d0 100644 --- a/apps/desktop/src/utils/teleprompter-window-options.ts +++ b/apps/desktop/src/utils/teleprompter-window-options.ts @@ -1,6 +1,6 @@ -import { LogicalPosition } from "@tauri-apps/api/dpi"; -import type { WebviewWindow } from "@tauri-apps/api/webviewWindow"; -import type { OsType } from "@tauri-apps/plugin-os"; +import { LogicalPosition } from "~/electron/dpi"; +import type { WebviewWindow } from "~/electron/webviewWindow"; +import type { OsType } from "~/electron/os"; export type TeleprompterWindowOptions = NonNullable< ConstructorParameters[1] diff --git a/apps/desktop/src/utils/teleprompter.test.ts b/apps/desktop/src/utils/teleprompter.test.ts index a5e0df72bf1..0f4a6cada83 100644 --- a/apps/desktop/src/utils/teleprompter.test.ts +++ b/apps/desktop/src/utils/teleprompter.test.ts @@ -51,7 +51,7 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock("@tauri-apps/api/webviewWindow", () => ({ +vi.mock("~/electron/webviewWindow", () => ({ WebviewWindow: class { static getCurrent = mocks.getCurrent; static getByLabel = vi.fn(async () => mocks.state.existingWindow); @@ -73,7 +73,7 @@ vi.mock("@tauri-apps/api/webviewWindow", () => ({ }, })); -vi.mock("@tauri-apps/plugin-os", () => ({ type: mocks.osType })); +vi.mock("~/electron/os", () => ({ type: mocks.osType })); vi.mock("./tauri", () => ({ commands: { diff --git a/apps/desktop/src/utils/teleprompter.ts b/apps/desktop/src/utils/teleprompter.ts index 39487c0683d..ed7a89d7042 100644 --- a/apps/desktop/src/utils/teleprompter.ts +++ b/apps/desktop/src/utils/teleprompter.ts @@ -1,5 +1,5 @@ -import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import { WebviewWindow } from "~/electron/webviewWindow"; +import { type as ostype } from "~/electron/os"; import { commands } from "./tauri"; import { getTeleprompterWindowOptions } from "./teleprompter-window-options"; diff --git a/apps/desktop/src/utils/titlebar-state.ts b/apps/desktop/src/utils/titlebar-state.ts index 80fc8beb2c3..7ff0eb0318f 100644 --- a/apps/desktop/src/utils/titlebar-state.ts +++ b/apps/desktop/src/utils/titlebar-state.ts @@ -1,6 +1,6 @@ -import type { UnlistenFn } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { type as ostype } from "@tauri-apps/plugin-os"; +import type { UnlistenFn } from "~/electron/event"; +import { getCurrentWindow } from "~/electron/window"; +import { type as ostype } from "~/electron/os"; import type { JSX } from "solid-js"; import { createStore } from "solid-js/store"; diff --git a/apps/desktop/src/utils/updater.ts b/apps/desktop/src/utils/updater.ts index cf0f2a75aad..2377b66925d 100644 --- a/apps/desktop/src/utils/updater.ts +++ b/apps/desktop/src/utils/updater.ts @@ -1,5 +1,5 @@ -import { arch, type as ostype } from "@tauri-apps/plugin-os"; -import type { CheckOptions } from "@tauri-apps/plugin-updater"; +import { arch, type as ostype } from "~/electron/os"; +import type { CheckOptions } from "~/electron/updater"; function updaterArch() { const currentArch = arch(); diff --git a/apps/desktop/src/utils/web-api.ts b/apps/desktop/src/utils/web-api.ts index 2bb70f47f03..7cef95da703 100644 --- a/apps/desktop/src/utils/web-api.ts +++ b/apps/desktop/src/utils/web-api.ts @@ -3,7 +3,7 @@ import { licenseContract, orgCustomDomainContract, } from "@cap/web-api-contract"; -import { fetch } from "@tauri-apps/plugin-http"; +import { fetch } from "~/electron/http"; import { type ApiFetcher, initClient } from "@ts-rest/core"; import { authStore, generalSettingsStore } from "~/store"; diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index faa3ffe3aec..52f6d6811dc 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -28,5 +28,5 @@ "~/*": ["./src/*"] } }, - "include": ["src"] + "include": ["src", "electron"] } diff --git a/package.json b/package.json index 55c4e1879e7..b2e7b1b5400 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "docker:up": "turbo run docker:up", "format": "pnpm exec biome check --write", "lint": "pnpm exec biome lint", - "tauri:build": "node scripts/build-desktop-binaries.mjs && dotenv -e .env -- pnpm --dir apps/desktop run preparescript && dotenv -e .env -- pnpm --dir apps/desktop tauri build --verbose", + "electron:build": "dotenv -e .env -- pnpm --dir apps/desktop build:electron", "typecheck": "pnpm --dir apps/web exec next typegen && pnpm tsc -b", "test": "turbo run test", "test:web": "pnpm --filter=@cap/web test", @@ -46,7 +46,6 @@ "test:desktop:memory:unit": "pnpm --dir apps/desktop test:memory:unit", "web": "pnpm --filter=@cap/web", "env-setup": "node scripts/env-cli.js", - "check-tauri-versions": "node scripts/check-tauri-plugin-versions.js", "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", "lgtm-otel": "docker run -p 3010:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm", "with-env": "dotenv -e .env --" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bfe945d8f1..a34e0357ce3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 11.0.0 mysql2: specifier: ^3.15.2 - version: 3.22.5(@types/node@22.20.0) + version: 3.22.5(@types/node@24.13.3) tsdown: specifier: ^0.15.6 version: 0.15.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(typescript@5.9.3) @@ -86,7 +86,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 4.4.1 - version: 4.4.1(vite@6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 4.4.1(vite@6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) autoprefixer: specifier: ^10.4.16 version: 10.5.2(postcss@8.5.16) @@ -101,13 +101,17 @@ importers: version: 5.9.3 vite: specifier: 6.3.5 - version: 6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + version: 6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) vitest: specifier: ^3.2.0 - version: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + version: 3.2.6(@types/debug@4.1.13)(@types/node@24.13.3)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) apps/desktop: dependencies: + electron-updater: + specifier: 6.8.9 + version: 6.8.9 + devDependencies: '@aerofoil/rive-solid-canvas': specifier: ^2.1.4 version: 2.1.4 @@ -126,6 +130,12 @@ importers: '@corvu/tooltip': specifier: ^0.2.1 version: 0.2.2(solid-js@1.9.13) + '@fontsource/geist-sans': + specifier: ^5.0.3 + version: 5.2.5 + '@iconify/json': + specifier: ^2.2.239 + version: 2.2.493 '@kobalte/core': specifier: ^0.13.11 version: 0.13.11(solid-js@1.9.13) @@ -138,6 +148,9 @@ importers: '@rive-app/canvas': specifier: ^2.32.2 version: 2.38.3 + '@solid-devtools/overlay': + specifier: ^0.33.5 + version: 0.33.5(solid-js@1.9.13) '@solid-primitives/bounds': specifier: ^0.0.122 version: 0.0.122(solid-js@1.9.13) @@ -191,67 +204,64 @@ importers: version: 0.14.10(solid-js@1.9.13) '@solidjs/start': specifier: ^1.1.3 - version: 1.3.2(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 1.3.2(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@tailwindcss/postcss': + specifier: ^4.2.2 + version: 4.3.2 + '@tailwindcss/typography': + specifier: ^0.5.9 + version: 0.5.20(tailwindcss@4.3.2) '@tanstack/solid-query': specifier: ^5.51.21 version: 5.101.2(solid-js@1.9.13) - '@tauri-apps/api': - specifier: 2.8.0 - version: 2.8.0 - '@tauri-apps/plugin-clipboard-manager': - specifier: ^2.3.0 - version: 2.3.2 - '@tauri-apps/plugin-deep-link': - specifier: ^2.4.1 - version: 2.4.9 - '@tauri-apps/plugin-dialog': - specifier: 2.4.2 - version: 2.4.2 - '@tauri-apps/plugin-fs': - specifier: 2.4.2 - version: 2.4.2 - '@tauri-apps/plugin-http': - specifier: 2.5.2 - version: 2.5.2 - '@tauri-apps/plugin-notification': - specifier: ^2.3.0 - version: 2.3.3 - '@tauri-apps/plugin-opener': - specifier: ^2.5.0 - version: 2.5.4 - '@tauri-apps/plugin-os': - specifier: ^2.3.0 - version: 2.3.2 - '@tauri-apps/plugin-process': - specifier: 2.3.0 - version: 2.3.0 - '@tauri-apps/plugin-shell': - specifier: ^2.3.0 - version: 2.3.5 - '@tauri-apps/plugin-store': - specifier: ^2.4.0 - version: 2.4.3 - '@tauri-apps/plugin-updater': - specifier: 2.9.0 - version: 2.9.0 + '@total-typescript/ts-reset': + specifier: ^0.6.1 + version: 0.6.1 '@ts-rest/core': specifier: ^3.52.1 - version: 3.52.1(@types/node@22.20.0)(zod@3.25.76) + version: 3.52.1(@types/node@24.13.3)(zod@3.25.76) + '@types/dom-webcodecs': + specifier: ^0.1.11 + version: 0.1.18 '@types/react-tooltip': specifier: ^4.2.4 version: 4.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 + '@webgpu/types': + specifier: ^0.1.44 + version: 0.1.71 + concurrently: + specifier: 9.2.1 + version: 9.2.1 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 cva: specifier: npm:class-variance-authority@^0.7.0 version: class-variance-authority@0.7.1 effect: specifier: ^3.18.4 version: 3.21.4 + electron: + specifier: 43.4.0 + version: 43.4.0 + electron-builder: + specifier: 26.15.3 + version: 26.15.3(electron-builder-squirrel-windows@26.15.3) + esbuild: + specifier: ^0.25.12 + version: 0.25.12 lz4-wasm: specifier: ^0.9.2 version: 0.9.2 mp4box: specifier: ^0.5.2 version: 0.5.4 + solid-devtools: + specifier: ^0.34.5 + version: 0.34.5(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) solid-js: specifier: ^1.9.3 version: 1.9.13 @@ -267,6 +277,12 @@ importers: solid-transition-group: specifier: ^0.2.3 version: 0.2.3(solid-js@1.9.13) + tailwind-scrollbar: + specifier: ^3.1.0 + version: 3.1.0(tailwindcss@3.4.19(yaml@2.9.0)) + typescript: + specifier: ^5.8.3 + version: 5.9.3 unplugin-auto-import: specifier: ^0.18.2 version: 0.18.6(rollup@4.62.2) @@ -278,71 +294,31 @@ importers: version: 9.0.1 vinxi: specifier: ^0.5.6 - version: 0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) - webcodecs: - specifier: ^0.1.0 - version: 0.1.0 - zod: - specifier: ^3.25.76 - version: 3.25.76 - devDependencies: - '@fontsource/geist-sans': - specifier: ^5.0.3 - version: 5.2.5 - '@iconify/json': - specifier: ^2.2.239 - version: 2.2.493 - '@solid-devtools/overlay': - specifier: ^0.33.5 - version: 0.33.5(solid-js@1.9.13) - '@tailwindcss/postcss': - specifier: ^4.2.2 - version: 4.3.2 - '@tailwindcss/typography': - specifier: ^0.5.9 - version: 0.5.20(tailwindcss@4.3.2) - '@tauri-apps/cli': - specifier: '>=2.1.0' - version: 2.11.4 - '@total-typescript/ts-reset': - specifier: ^0.6.1 - version: 0.6.1 - '@types/dom-webcodecs': - specifier: ^0.1.11 - version: 0.1.18 - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 - '@webgpu/types': - specifier: ^0.1.44 - version: 0.1.71 - cross-env: - specifier: ^7.0.3 - version: 7.0.3 - solid-devtools: - specifier: ^0.34.5 - version: 0.34.5(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) - tailwind-scrollbar: - specifier: ^3.1.0 - version: 3.1.0(tailwindcss@3.4.19(yaml@2.9.0)) - typescript: - specifier: ^5.8.3 - version: 5.9.3 + version: 0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + version: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) vite-plugin-top-level-await: specifier: ^1.4.4 - version: 1.6.0(@swc/helpers@0.5.23)(rollup@4.62.2)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 1.6.0(@swc/helpers@0.5.23)(rollup@4.62.2)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) vite-plugin-wasm: specifier: ^3.4.1 - version: 3.6.0(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 3.6.0(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) vite-tsconfig-paths: specifier: ^5.0.1 - version: 5.1.4(typescript@5.9.3)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 5.1.4(typescript@5.9.3)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) vitest: specifier: ~2.1.9 - version: 2.1.9(@types/node@22.20.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0) + version: 2.1.9(@types/node@24.13.3)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0) + wait-on: + specifier: 9.0.1 + version: 9.0.1 + webcodecs: + specifier: ^0.1.0 + version: 0.1.0 + zod: + specifier: ^3.25.76 + version: 3.25.76 apps/discord-bot: dependencies: @@ -373,7 +349,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.6.4 - version: 0.6.16(@cloudflare/workers-types@4.20260630.1)(@vitest/runner@3.2.6)(@vitest/snapshot@3.2.6)(vitest@2.1.9(@types/node@22.20.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 0.6.16(@cloudflare/workers-types@4.20260630.1)(@vitest/runner@3.2.6)(@vitest/snapshot@3.2.6)(vitest@2.1.9(@types/node@24.13.3)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)) '@cloudflare/workers-types': specifier: ^4.20250214.0 version: 4.20260630.1 @@ -382,7 +358,7 @@ importers: version: 5.9.3 vitest: specifier: ~2.1.9 - version: 2.1.9(@types/node@22.20.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0) + version: 2.1.9(@types/node@24.13.3)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0) wrangler: specifier: ^3.109.1 version: 3.114.17(@cloudflare/workers-types@4.20260630.1) @@ -549,7 +525,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.0 - version: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + version: 3.2.6(@types/debug@4.1.13)(@types/node@24.13.3)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) apps/storybook: dependencies: @@ -595,16 +571,16 @@ importers: version: 1.0.0-beta.7(@storybook/test@8.6.14(storybook@8.6.18(prettier@3.9.4)))(solid-js@1.9.13)(storybook@8.6.18(prettier@3.9.4)) storybook-solidjs-vite: specifier: ^1.0.0-beta.2 - version: 1.0.0-beta.7(@storybook/test@8.6.14(storybook@8.6.18(prettier@3.9.4)))(esbuild@0.25.12)(rollup@4.62.2)(solid-js@1.9.13)(storybook@8.6.18(prettier@3.9.4))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 1.0.0-beta.7(@storybook/test@8.6.14(storybook@8.6.18(prettier@3.9.4)))(esbuild@0.25.12)(rollup@4.62.2)(solid-js@1.9.13)(storybook@8.6.18(prettier@3.9.4))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) typescript: specifier: ^5.8.3 version: 5.9.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + version: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) vite-plugin-solid: specifier: ^2.10.2 - version: 2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) apps/web: dependencies: @@ -1106,7 +1082,7 @@ importers: version: 0.71.2(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4) '@effect/sql-mysql2': specifier: ^0.47.0 - version: 0.47.1(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(@effect/sql@0.46.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4))(@types/node@22.20.0)(effect@3.21.4) + version: 0.47.1(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(@effect/sql@0.46.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4))(@types/node@24.13.3)(effect@3.21.4) '@effect/workflow': specifier: ^0.11.3 version: 0.11.5(@effect/platform@0.92.1(effect@3.21.4))(@effect/rpc@0.71.2(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) @@ -1338,7 +1314,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.0 - version: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + version: 3.2.6(@types/debug@4.1.13)(@types/node@24.13.3)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) packages/sdk-embed: dependencies: @@ -1545,7 +1521,7 @@ importers: version: 0.18.6(rollup@4.62.2) unplugin-fonts: specifier: ^1.1.1 - version: 1.4.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + version: 1.4.0(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) unplugin-icons: specifier: ^0.19.2 version: 0.19.3 @@ -1594,7 +1570,7 @@ importers: dependencies: '@ts-rest/core': specifier: ^3.52.1 - version: 3.52.1(@types/node@22.20.0)(zod@3.25.76) + version: 3.52.1(@types/node@24.13.3)(zod@3.25.76) zod: specifier: ^3.25.76 version: 3.25.76 @@ -1657,13 +1633,13 @@ importers: version: 3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0) drizzle-orm: specifier: 0.44.6 - version: 0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)) + version: 0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)) effect: specifier: ^3.18.4 version: 3.21.4 next: specifier: 16.3.0 - version: 16.3.0(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(@types/node@22.20.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.3.0(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -2824,6 +2800,50 @@ packages: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} + '@electron-internal/extract-zip@1.0.5': + resolution: {integrity: sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==} + engines: {node: '>=22.12.0'} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@3.1.0': + resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==} + engines: {node: '>=14'} + + '@electron/get@5.1.0': + resolution: {integrity: sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==} + engines: {node: '>=22.12.0'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@4.2.0': + resolution: {integrity: sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@electron/universal@2.0.3': + resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} + engines: {node: '>=16.4'} + + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} + hasBin: true + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -4022,6 +4042,26 @@ packages: engines: {node: '>=6'} hasBin: true + '@hapi/address@5.1.1': + resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} + engines: {node: '>=14.0.0'} + + '@hapi/formula@3.0.2': + resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/pinpoint@2.0.1': + resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + + '@hapi/tlds@1.1.7': + resolution: {integrity: sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==} + engines: {node: '>=14.0.0'} + + '@hapi/topo@6.0.2': + resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@headlessui/react@1.7.19': resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==} engines: {node: '>=10'} @@ -4480,6 +4520,14 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} @@ -4746,10 +4794,18 @@ packages: resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -6805,6 +6861,10 @@ packages: '@sinclair/typebox@0.34.49': resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -7429,6 +7489,10 @@ packages: '@swc/wasm@1.15.43': resolution: {integrity: sha512-jYqeckrzZGAU+9OSfmL15MWfhkKWRCC8QMssL/MZu/MaIP76mU3VHjzqxlwKIz9DOSR/9jCqi1+QlWE0ILNehA==} + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + '@t3-oss/env-core@0.12.0': resolution: {integrity: sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==} peerDependencies: @@ -7644,181 +7708,74 @@ packages: '@tauri-apps/api@2.11.1': resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} - '@tauri-apps/api@2.8.0': - resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} - '@tauri-apps/cli-darwin-arm64@1.6.3': resolution: {integrity: sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-arm64@2.11.4': - resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@tauri-apps/cli-darwin-x64@1.6.3': resolution: {integrity: sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.11.4': - resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@1.6.3': resolution: {integrity: sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@1.6.3': resolution: {integrity: sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@tauri-apps/cli-linux-arm64-musl@1.6.3': resolution: {integrity: sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - '@tauri-apps/cli-linux-x64-gnu@1.6.3': resolution: {integrity: sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@tauri-apps/cli-linux-x64-musl@1.6.3': resolution: {integrity: sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.11.4': - resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@1.6.3': resolution: {integrity: sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@1.6.3': resolution: {integrity: sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - '@tauri-apps/cli-win32-x64-msvc@1.6.3': resolution: {integrity: sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@tauri-apps/cli@1.6.3': resolution: {integrity: sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/cli@2.11.4': - resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} - engines: {node: '>= 10'} - hasBin: true - - '@tauri-apps/plugin-clipboard-manager@2.3.2': - resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} - - '@tauri-apps/plugin-deep-link@2.4.9': - resolution: {integrity: sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA==} - - '@tauri-apps/plugin-dialog@2.4.2': - resolution: {integrity: sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==} - - '@tauri-apps/plugin-fs@2.4.2': - resolution: {integrity: sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==} - - '@tauri-apps/plugin-http@2.5.2': - resolution: {integrity: sha512-x1mQKHSLDk4mS2S938OTeyk8L7QyLpCrKZCZcjkljGsvTvRMojCvI9SeJ1kaxc7t8xSilkC7WdId8xER9TIGLg==} - - '@tauri-apps/plugin-notification@2.3.3': - resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} - - '@tauri-apps/plugin-opener@2.5.4': - resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} - - '@tauri-apps/plugin-os@2.3.2': - resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} - - '@tauri-apps/plugin-process@2.3.0': - resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} - - '@tauri-apps/plugin-shell@2.3.5': - resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} - '@tauri-apps/plugin-store@2.4.3': resolution: {integrity: sha512-9LWPj9yMphRi9czEtUv87XHbl1b6xgd9EXpPrUnq6nG7+nbtoF84d4Kwz9xhAv/Hf30sr58pq7EOlyI936y8qw==} - '@tauri-apps/plugin-updater@2.9.0': - resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} - '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -7950,6 +7907,9 @@ packages: '@types/bun@1.3.14': resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} @@ -8046,6 +8006,9 @@ packages: '@types/filewriter@0.0.33': resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/google-protobuf@3.15.12': resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==} @@ -8094,6 +8057,9 @@ packages: '@types/keygrip@1.0.6': resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/koa-compose@3.2.9': resolution: {integrity: sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==} @@ -8136,6 +8102,9 @@ packages: '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/prismjs@1.26.6': resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} @@ -8171,6 +8140,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -8935,6 +8907,13 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + app-builder-lib@26.15.3: + resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.15.3 + electron-builder-squirrel-windows: 26.15.3 + archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -9045,6 +9024,10 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -9065,6 +9048,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + autoprefixer@10.5.2: resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==} engines: {node: ^10 || ^12 || >=14} @@ -9090,6 +9077,9 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + aws4fetch@1.0.18: resolution: {integrity: sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ==} @@ -9097,6 +9087,9 @@ packages: resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -9336,6 +9329,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -9398,6 +9395,14 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builder-util-runtime@9.7.0: + resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} + engines: {node: '>=12.0.0'} + + builder-util@26.15.3: + resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} + engines: {node: '>=14.0.0'} + builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} @@ -9423,6 +9428,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -9439,6 +9448,10 @@ packages: resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} engines: {node: ^20.17.0 || >=22.9.0} + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -9447,6 +9460,10 @@ packages: resolution: {integrity: sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==} engines: {node: '>=18'} + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -9575,6 +9592,9 @@ packages: chromium-edge-launcher@0.2.0: resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -9582,6 +9602,14 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -9632,6 +9660,9 @@ packages: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -9699,6 +9730,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -9711,6 +9746,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + common-ancestor-path@2.0.0: resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} engines: {node: '>= 18'} @@ -9718,6 +9757,10 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + compatx@0.2.0: resolution: {integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==} @@ -9740,6 +9783,11 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + concurrently@9.2.3: resolution: {integrity: sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==} engines: {node: '>=18'} @@ -9832,6 +9880,9 @@ packages: resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} engines: {node: '>=18.0'} + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -10034,6 +10085,10 @@ packages: resolution: {integrity: sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==} engines: {node: '>=20'} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -10060,6 +10115,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -10112,6 +10171,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} @@ -10128,6 +10190,9 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -10135,6 +10200,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dmg-builder@26.15.3: + resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} + dns-packet@5.6.1: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} @@ -10345,9 +10413,32 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + electron-builder-squirrel-windows@26.15.3: + resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==} + + electron-builder@26.15.3: + resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@26.15.3: + resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} + electron-to-chromium@1.5.382: resolution: {integrity: sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==} + electron-updater@6.8.9: + resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} + + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + + electron@43.4.0: + resolution: {integrity: sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==} + engines: {node: '>= 22.12.0'} + hasBin: true + emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -10372,6 +10463,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} @@ -10408,10 +10502,17 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -10466,6 +10567,9 @@ packages: es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -11382,6 +11486,10 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.3.1: resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} @@ -11390,6 +11498,18 @@ packages: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + fs-minipass@3.0.3: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -11470,6 +11590,10 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -11534,6 +11658,10 @@ packages: resolution: {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 + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -11577,6 +11705,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + got@14.6.6: resolution: {integrity: sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==} engines: {node: '>=20'} @@ -11706,6 +11838,10 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -11762,6 +11898,10 @@ packages: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + http2-wrapper@2.2.1: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} @@ -12166,6 +12306,14 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -12284,6 +12432,10 @@ packages: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} + joi@18.2.3: + resolution: {integrity: sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==} + engines: {node: '>= 20'} + jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} @@ -12363,6 +12515,9 @@ packages: json-stringify-nice@1.1.4: resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -12372,6 +12527,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} @@ -12431,6 +12589,9 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -12570,6 +12731,13 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -12607,6 +12775,10 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + lowercase-keys@3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12694,6 +12866,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -12976,6 +13152,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -13002,6 +13183,14 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + mimic-response@4.0.0: resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -13081,6 +13270,10 @@ packages: resolution: {integrity: sha512-CRdXtgfQH2jARmtNmPR0Q7jL20fiESbaYk1b0KvLD0jCdUuemepREtsbd8nbiY6BHV9OGGddAZITNXklupUPUQ==} engines: {node: '>=20.0.0'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -13262,9 +13455,16 @@ packages: xml2js: optional: true + node-abi@4.33.0: + resolution: {integrity: sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==} + engines: {node: '>=22.12.0'} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + node-av@5.2.4: resolution: {integrity: sha512-L5r+6k+YGvH5MZX8o55JHQbbeRZ+iFieAb1bhKtpa8hrqBEGuSwdkXRwpE4vd414JqVJsq/EQq6s+3qKeviQTg==} @@ -13345,6 +13545,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + normalize-url@8.1.1: resolution: {integrity: sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==} engines: {node: '>=14.16'} @@ -13727,6 +13931,10 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -13768,6 +13976,10 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pkijs@3.4.0: + resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} + engines: {node: '>=16.0.0'} + playwright-core@1.61.1: resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} @@ -13778,6 +13990,10 @@ packages: engines: {node: '>=18'} hasBin: true + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + plist@3.1.1: resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} engines: {node: '>=10.4.0'} @@ -13859,6 +14075,11 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -13948,6 +14169,10 @@ packages: promise-call-limit@3.0.2: resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} @@ -13961,6 +14186,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -13975,6 +14203,13 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@1.3.2: resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} @@ -14314,6 +14549,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -14490,6 +14729,10 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} @@ -14524,6 +14767,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + responselike@4.0.2: resolution: {integrity: sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==} engines: {node: '>=20'} @@ -14536,15 +14782,28 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + rolldown-plugin-dts@0.17.8: resolution: {integrity: sha512-76EEBlhF00yeY6M7VpMkWKI4r9WjuoMiOGey7j4D6zf3m0BR+ZrrY9hvSXdueJ3ljxSLq4DJBKFpX/X9+L7EKw==} engines: {node: '>=20.19.0'} @@ -14652,6 +14911,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + sax@1.2.1: resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} @@ -14679,6 +14941,9 @@ packages: selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -14687,6 +14952,10 @@ packages: resolution: {integrity: sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==} engines: {node: '>=12'} + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -14723,6 +14992,10 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serialize-javascript@7.0.7: resolution: {integrity: sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==} engines: {node: '>=20.0.0'} @@ -14797,6 +15070,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + shell-quote@1.8.4: resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} @@ -14850,6 +15127,10 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -15022,6 +15303,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sql-escaper@1.3.3: resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} @@ -15100,6 +15384,10 @@ packages: standard-navigation@0.0.7: resolution: {integrity: sha512-NCGLCNyuXrFOkGHxdNZFnpsehGtiq1oXbPhKl7ZuxFO5J//H2evqqOchmD4YwEUJnkjO4kH9Xp4hQX6hdAYCKQ==} + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -15300,6 +15588,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + super-regex@1.1.0: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} @@ -15415,6 +15707,13 @@ packages: teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + terminal-link@2.1.1: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} @@ -15468,9 +15767,15 @@ packages: resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} engines: {node: '>=12'} + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -15523,6 +15828,9 @@ packages: resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==} hasBin: true + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} @@ -15578,6 +15886,9 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -15697,6 +16008,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -15810,6 +16125,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -15919,6 +16237,10 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -16159,6 +16481,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -16474,6 +16799,11 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + wait-on@9.0.1: + resolution: {integrity: sha512-noeCAI+XbqWMXY23sKril0BSURhuLYarkVXwJv1uUWwoojZJE7pmX3vJ7kh7SZaNgPGzfsCSQIZM/AGvu0Q9pA==} + engines: {node: '>=20.0.0'} + hasBin: true + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -16592,6 +16922,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + which@6.0.1: resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -18030,7 +18365,7 @@ snapshots: optionalDependencies: workerd: 1.20250718.0 - '@cloudflare/vitest-pool-workers@0.6.16(@cloudflare/workers-types@4.20260630.1)(@vitest/runner@3.2.6)(@vitest/snapshot@3.2.6)(vitest@2.1.9(@types/node@22.20.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0))': + '@cloudflare/vitest-pool-workers@0.6.16(@cloudflare/workers-types@4.20260630.1)(@vitest/runner@3.2.6)(@vitest/snapshot@3.2.6)(vitest@2.1.9(@types/node@24.13.3)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -18040,7 +18375,7 @@ snapshots: esbuild: 0.17.19 miniflare: 3.20250204.1 semver: 7.8.5 - vitest: 2.1.9(@types/node@22.20.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0) + vitest: 2.1.9(@types/node@24.13.3)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0) wrangler: 3.109.1(@cloudflare/workers-types@4.20260630.1) zod: 3.25.76 transitivePeerDependencies: @@ -18235,13 +18570,13 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@effect/sql-mysql2@0.47.1(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(@effect/sql@0.46.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4))(@types/node@22.20.0)(effect@3.21.4)': + '@effect/sql-mysql2@0.47.1(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(@effect/sql@0.46.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4))(@types/node@24.13.3)(effect@3.21.4)': dependencies: '@effect/experimental': 0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1) '@effect/platform': 0.92.1(effect@3.21.4) '@effect/sql': 0.46.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4)(ioredis@5.11.1))(@effect/platform@0.92.1(effect@3.21.4))(effect@3.21.4) effect: 3.21.4 - mysql2: 3.22.5(@types/node@22.20.0) + mysql2: 3.22.5(@types/node@24.13.3) transitivePeerDependencies: - '@types/node' @@ -18262,6 +18597,100 @@ snapshots: dependencies: '@types/hammerjs': 2.0.46 + '@electron-internal/extract-zip@1.0.5': {} + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + + '@electron/get@3.1.0': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/get@5.1.0': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.8.5 + sumchecker: 3.0.1 + optionalDependencies: + undici: 7.28.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@4.2.0': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + node-abi: 4.33.0 + node-api-version: 0.2.1 + node-gyp: 12.4.0 + read-binary-file-arch: 1.0.6 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.3': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + dir-compare: 4.2.0 + fs-extra: 11.3.6 + minimatch: 9.0.9 + plist: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 11.3.6 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -19248,6 +19677,22 @@ snapshots: protobufjs: 7.6.4 yargs: 17.7.3 + '@hapi/address@5.1.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/formula@3.0.2': {} + + '@hapi/hoek@11.0.7': {} + + '@hapi/pinpoint@2.0.1': {} + + '@hapi/tlds@1.1.7': {} + + '@hapi/topo@6.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + '@headlessui/react@1.7.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/react-virtual': 3.14.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -19680,6 +20125,19 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 9.1.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 @@ -19943,8 +20401,12 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/hashes@1.4.0': {} + '@noble/hashes@1.8.0': {} + '@noble/hashes@2.3.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -22412,6 +22874,8 @@ snapshots: '@sinclair/typebox@0.34.49': {} + '@sindresorhus/is@4.6.0': {} + '@sindresorhus/is@7.2.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -22733,11 +23197,11 @@ snapshots: dependencies: solid-js: 1.9.13 - '@solidjs/start@1.3.2(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@solidjs/start@1.3.2(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: - '@tanstack/server-functions-plugin': 1.121.21(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0)) - '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0)) + '@tanstack/server-functions-plugin': 1.121.21(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0)) cookie-es: 2.0.1 defu: 6.1.7 error-stack-parser: 2.1.4 @@ -22749,8 +23213,8 @@ snapshots: source-map-js: 1.2.1 terracotta: 1.1.0(solid-js@1.9.13) tinyglobby: 0.2.17 - vinxi: 0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) - vite-plugin-solid: 2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + vinxi: 0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) + vite-plugin-solid: 2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -22869,12 +23333,12 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/builder-vite@10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@storybook/builder-vite@10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: - '@storybook/csf-plugin': 10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@storybook/csf-plugin': 10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) storybook: 8.6.18(prettier@3.9.4) ts-dedent: 2.3.0 - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - esbuild - rollup @@ -22901,14 +23365,14 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@storybook/csf-plugin@10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: storybook: 8.6.18(prettier@3.9.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.25.12 rollup: 4.62.2 - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) '@storybook/csf-plugin@8.6.14(storybook@8.6.18(prettier@3.9.4))': dependencies: @@ -23110,6 +23574,10 @@ snapshots: '@swc/wasm@1.15.43': {} + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@t3-oss/env-core@0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)': optionalDependencies: typescript: 5.9.3 @@ -23240,7 +23708,7 @@ snapshots: - csstype - utf-8-validate - '@tanstack/directive-functions-plugin@1.121.21(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@tanstack/directive-functions-plugin@1.121.21(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.7 @@ -23249,7 +23717,7 @@ snapshots: '@tanstack/router-utils': 1.162.2 babel-dead-code-elimination: 1.0.12 tiny-invariant: 1.3.3 - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -23307,7 +23775,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.121.21(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@tanstack/server-functions-plugin@1.121.21(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.7 @@ -23316,7 +23784,7 @@ snapshots: '@babel/template': 7.29.7 '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 - '@tanstack/directive-functions-plugin': 1.121.21(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@tanstack/directive-functions-plugin': 1.121.21(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) babel-dead-code-elimination: 1.0.12 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -23334,73 +23802,39 @@ snapshots: '@tauri-apps/api@1.6.0': {} - '@tauri-apps/api@2.11.1': {} - - '@tauri-apps/api@2.8.0': {} - - '@tauri-apps/cli-darwin-arm64@1.6.3': + '@tauri-apps/api@2.11.1': optional: true - '@tauri-apps/cli-darwin-arm64@2.11.4': + '@tauri-apps/cli-darwin-arm64@1.6.3': optional: true '@tauri-apps/cli-darwin-x64@1.6.3': optional: true - '@tauri-apps/cli-darwin-x64@2.11.4': - optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@1.6.3': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - optional: true - '@tauri-apps/cli-linux-arm64-gnu@1.6.3': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - optional: true - '@tauri-apps/cli-linux-arm64-musl@1.6.3': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - optional: true - - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - optional: true - '@tauri-apps/cli-linux-x64-gnu@1.6.3': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - optional: true - '@tauri-apps/cli-linux-x64-musl@1.6.3': optional: true - '@tauri-apps/cli-linux-x64-musl@2.11.4': - optional: true - '@tauri-apps/cli-win32-arm64-msvc@1.6.3': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - optional: true - '@tauri-apps/cli-win32-ia32-msvc@1.6.3': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - optional: true - '@tauri-apps/cli-win32-x64-msvc@1.6.3': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - optional: true - '@tauri-apps/cli@1.6.3': dependencies: semver: 7.8.5 @@ -23416,67 +23850,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 1.6.3 '@tauri-apps/cli-win32-x64-msvc': 1.6.3 - '@tauri-apps/cli@2.11.4': - optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.4 - '@tauri-apps/cli-darwin-x64': 2.11.4 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 - '@tauri-apps/cli-linux-arm64-musl': 2.11.4 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-musl': 2.11.4 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 - '@tauri-apps/cli-win32-x64-msvc': 2.11.4 - - '@tauri-apps/plugin-clipboard-manager@2.3.2': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-deep-link@2.4.9': - dependencies: - '@tauri-apps/api': 2.11.1 - - '@tauri-apps/plugin-dialog@2.4.2': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-fs@2.4.2': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-http@2.5.2': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-notification@2.3.3': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-opener@2.5.4': - dependencies: - '@tauri-apps/api': 2.11.1 - - '@tauri-apps/plugin-os@2.3.2': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-process@2.3.0': - dependencies: - '@tauri-apps/api': 2.8.0 - - '@tauri-apps/plugin-shell@2.3.5': - dependencies: - '@tauri-apps/api': 2.11.1 - '@tauri-apps/plugin-store@2.4.3': dependencies: '@tauri-apps/api': 2.11.1 - - '@tauri-apps/plugin-updater@2.9.0': - dependencies: - '@tauri-apps/api': 2.8.0 + optional: true '@testing-library/dom@10.4.0': dependencies: @@ -23544,9 +23921,9 @@ snapshots: '@types/node': 20.19.43 zod: 3.25.76 - '@ts-rest/core@3.52.1(@types/node@22.20.0)(zod@3.25.76)': + '@ts-rest/core@3.52.1(@types/node@24.13.3)(zod@3.25.76)': optionalDependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 zod: 3.25.76 '@tsconfig/bun@1.0.7': {} @@ -23621,6 +23998,13 @@ snapshots: dependencies: bun-types: 1.3.14 + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 20.19.43 + '@types/responselike': 1.0.3 + '@types/canvas-confetti@1.9.0': {} '@types/chai@5.2.3': @@ -23724,6 +24108,10 @@ snapshots: '@types/filewriter@0.0.33': {} + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 20.19.43 + '@types/google-protobuf@3.15.12': {} '@types/graceful-fs@4.1.9': @@ -23768,6 +24156,10 @@ snapshots: '@types/keygrip@1.0.6': {} + '@types/keyv@3.1.4': + dependencies: + '@types/node': 20.19.43 + '@types/koa-compose@3.2.9': dependencies: '@types/koa': 2.15.2 @@ -23820,6 +24212,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + '@types/prismjs@1.26.6': {} '@types/qs@6.15.1': {} @@ -23855,6 +24251,10 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/responselike@1.0.3': + dependencies: + '@types/node': 20.19.43 + '@types/semver@7.7.1': {} '@types/send@0.17.6': @@ -24257,7 +24657,7 @@ snapshots: untun: 0.1.3 uqr: 0.1.3 - '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))': + '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))': dependencies: '@babel/parser': 7.29.7 acorn: 8.17.0 @@ -24268,18 +24668,18 @@ snapshots: magicast: 0.2.11 recast: 0.23.12 tslib: 2.8.1 - vinxi: 0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) + vinxi: 0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) - '@vinxi/server-components@0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))': + '@vinxi/server-components@0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0))': dependencies: - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0)) acorn: 8.17.0 acorn-loose: 8.5.2 acorn-typescript: 1.4.13(acorn@8.17.0) astring: 1.9.0 magicast: 0.2.11 recast: 0.23.12 - vinxi: 0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) + vinxi: 0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0) '@virtual-grid/core@2.0.1': {} @@ -24308,14 +24708,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -24360,13 +24760,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0))': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 5.4.21(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0) '@vitest/mocker@3.2.6(vite@6.3.5(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: @@ -24376,21 +24776,21 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - '@vitest/mocker@3.2.6(vite@6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitest/mocker@3.2.6(vite@6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - '@vitest/mocker@3.2.6(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitest/mocker@3.2.6(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) '@vitest/pretty-format@2.0.5': dependencies: @@ -24448,7 +24848,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vitest: 3.2.6(@types/debug@4.1.13)(@types/node@24.13.3)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) '@vitest/utils@2.0.5': dependencies: @@ -24962,6 +25362,54 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3): + dependencies: + '@electron/asar': 3.4.1 + '@electron/fuses': 1.8.0 + '@electron/get': 3.1.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.3 + '@electron/rebuild': 4.2.0 + '@electron/universal': 2.0.3 + '@malept/flatpak-bundler': 0.4.0 + '@noble/hashes': 2.3.0 + '@peculiar/webcrypto': 1.7.1 + '@types/fs-extra': 9.0.13 + ajv: 8.20.0 + asn1js: 3.0.10 + async-exit-hook: 2.0.1 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chromium-pickle-js: 0.2.0 + ci-info: 4.3.1 + debug: 4.4.3(supports-color@8.1.1) + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.15.3) + electron-publish: 26.15.3 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + isbinaryfile: 5.0.7 + jiti: 2.7.0 + js-yaml: 4.3.0 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.5 + pkijs: 3.4.0 + plist: 3.1.0 + proper-lockfile: 4.1.2 + resedit: 1.7.2 + semver: 7.7.4 + tar: 7.5.19 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + unzipper: 0.12.5 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -25121,6 +25569,8 @@ snapshots: astring@1.9.0: {} + async-exit-hook@2.0.1: {} + async-function@1.0.0: {} async-listen@3.0.0: {} @@ -25135,6 +25585,8 @@ snapshots: asynckit@0.4.0: {} + at-least-node@1.0.0: {} + autoprefixer@10.5.2(postcss@8.5.16): dependencies: browserslist: 4.28.4 @@ -25176,10 +25628,22 @@ snapshots: aws-ssl-profiles@1.1.2: {} + aws4@1.13.2: {} + aws4fetch@1.0.18: {} axe-core@4.12.1: {} + axios@1.19.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + axobject-query@4.1.0: {} b4a@1.8.1: {} @@ -25488,6 +25952,9 @@ snapshots: boolbase@1.0.0: {} + boolean@3.2.0: + optional: true + bottleneck@2.19.5: {} bowser@2.14.1: {} @@ -25566,6 +26033,32 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + builder-util-runtime@9.7.0: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + sax: 1.6.0 + transitivePeerDependencies: + - supports-color + + builder-util@26.15.3: + dependencies: + '@types/debug': 4.1.13 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + js-yaml: 4.3.0 + sanitize-filename: 1.6.4 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + builtin-modules@5.0.0: {} bun-types@1.3.14: @@ -25585,6 +26078,8 @@ snapshots: bytes@3.1.2: {} + bytestreamjs@2.0.1: {} + c12@3.3.4(magicast@0.5.3): dependencies: chokidar: 5.0.0 @@ -25617,6 +26112,8 @@ snapshots: p-map: 7.0.5 ssri: 13.0.1 + cacheable-lookup@5.0.4: {} + cacheable-lookup@7.0.0: {} cacheable-request@13.0.19: @@ -25629,6 +26126,16 @@ snapshots: normalize-url: 8.1.1 responselike: 4.0.2 + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -25764,10 +26271,16 @@ snapshots: transitivePeerDependencies: - supports-color + chromium-pickle-js@0.2.0: {} + ci-info@2.0.0: {} ci-info@3.9.0: {} + ci-info@4.3.1: {} + + ci-info@4.4.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 @@ -25818,6 +26331,10 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + clone@1.0.4: {} clsx@2.1.1: {} @@ -25872,16 +26389,23 @@ snapshots: commander@4.1.1: {} + commander@5.1.0: {} + commander@6.2.1: {} commander@7.2.0: {} commander@8.3.0: {} + commander@9.5.0: + optional: true + common-ancestor-path@2.0.0: {} commondir@1.0.1: {} + compare-version@0.1.2: {} + compatx@0.2.0: {} compress-commons@6.0.2: @@ -25917,6 +26441,15 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + concurrently@9.2.3: dependencies: chalk: 4.1.2 @@ -25992,6 +26525,9 @@ snapshots: croner@10.0.1: {} + cross-dirname@0.1.0: + optional: true + cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 @@ -26120,10 +26656,10 @@ snapshots: '@deno/shim-deno': 0.19.2 undici-types: 5.28.4 - db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)): + db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)): optionalDependencies: - drizzle-orm: 0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)) - mysql2: 3.22.5(@types/node@22.20.0) + drizzle-orm: 0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)) + mysql2: 3.22.5(@types/node@24.13.3) debounce@2.2.0: {} @@ -26159,6 +26695,10 @@ snapshots: dependencies: mimic-response: 4.0.0 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-eql@5.0.2: {} deep-equal@2.2.3: @@ -26197,6 +26737,8 @@ snapshots: dependencies: clone: 1.0.4 + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -26233,6 +26775,9 @@ snapshots: detect-node-es@1.1.0: {} + detect-node@2.1.0: + optional: true + devalue@4.3.3: {} devalue@5.8.1: {} @@ -26245,12 +26790,27 @@ snapshots: diff@8.0.4: {} + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 dlv@1.1.3: {} + dmg-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + fs-extra: 10.1.0 + js-yaml: 4.3.0 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + dns-packet@5.6.1: dependencies: '@leichtgewicht/ip-codec': 2.0.5 @@ -26340,13 +26900,13 @@ snapshots: bun-types: 1.3.14 mysql2: 3.22.5(@types/node@20.19.43) - drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)): + drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)): optionalDependencies: '@cloudflare/workers-types': 4.20260630.1 '@opentelemetry/api': 1.9.1 '@planetscale/database': 1.20.1 bun-types: 1.3.14 - mysql2: 3.22.5(@types/node@22.20.0) + mysql2: 3.22.5(@types/node@24.13.3) dts-resolver@2.1.3: {} @@ -26387,8 +26947,80 @@ snapshots: dependencies: jake: 10.9.4 + electron-builder-squirrel-windows@26.15.3(dmg-builder@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + ci-info: 4.4.0 + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + fs-extra: 10.1.0 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.3 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-publish@26.15.3: + dependencies: + '@types/fs-extra': 9.0.13 + aws4: 1.13.2 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + form-data: 4.0.6 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + electron-to-chromium@1.5.382: {} + electron-updater@6.8.9: + dependencies: + builder-util-runtime: 9.7.0 + fs-extra: 10.1.0 + js-yaml: 4.3.0 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.7.4 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 7.0.1 + lodash: 4.18.1 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2 + transitivePeerDependencies: + - supports-color + + electron@43.4.0: + dependencies: + '@electron-internal/extract-zip': 1.0.5 + '@electron/get': 5.1.0 + '@types/node': 24.13.3 + transitivePeerDependencies: + - supports-color + emoji-regex-xs@1.0.0: {} emoji-regex@10.6.0: {} @@ -26403,6 +27035,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + engine.io-parser@5.2.3: {} engine.io@6.6.9: @@ -26448,8 +27084,12 @@ snapshots: env-paths@2.2.1: {} + env-paths@3.0.0: {} + environment@1.1.0: {} + err-code@2.0.3: {} + error-stack-parser-es@1.0.5: {} error-stack-parser@2.1.4: @@ -26585,6 +27225,9 @@ snapshots: es-toolkit@1.49.0: {} + es6-error@4.1.1: + optional: true + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -28002,6 +28645,12 @@ snapshots: fresh@2.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@11.3.1: dependencies: graceful-fs: 4.2.11 @@ -28014,6 +28663,25 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-minipass@3.0.3: dependencies: minipass: 7.1.3 @@ -28091,6 +28759,10 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -28168,6 +28840,16 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.8.5 + serialize-error: 7.0.1 + optional: true + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -28211,6 +28893,20 @@ snapshots: gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + got@14.6.6: dependencies: '@sindresorhus/is': 7.2.0 @@ -28422,6 +29118,10 @@ snapshots: hookable@5.5.3: {} + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -28490,6 +29190,11 @@ snapshots: http-shutdown@1.2.2: {} + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + http2-wrapper@2.2.1: dependencies: quick-lru: 5.1.1 @@ -28851,6 +29556,10 @@ snapshots: isarray@2.0.5: {} + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + isexe@2.0.0: {} isexe@3.1.5: {} @@ -29013,6 +29722,16 @@ snapshots: jmespath@0.16.0: {} + joi@18.2.3: + dependencies: + '@hapi/address': 5.1.1 + '@hapi/formula': 3.0.2 + '@hapi/hoek': 11.0.7 + '@hapi/pinpoint': 2.0.1 + '@hapi/tlds': 1.1.7 + '@hapi/topo': 6.0.2 + '@standard-schema/spec': 1.1.0 + jose@4.15.9: {} jose@5.10.0: {} @@ -29087,12 +29806,19 @@ snapshots: json-stringify-nice@1.1.4: {} + json-stringify-safe@5.0.1: + optional: true + json5@1.0.2: dependencies: minimist: 1.2.8 json5@2.2.3: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.2.1: dependencies: universalify: 2.0.1 @@ -29147,6 +29873,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lazy-val@1.0.5: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -29278,6 +30006,10 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.escaperegexp@4.1.2: {} + + lodash.isequal@4.5.0: {} + lodash.merge@4.6.2: {} lodash.throttle@4.1.1: {} @@ -29310,6 +30042,8 @@ snapshots: loupe@3.2.1: {} + lowercase-keys@2.0.0: {} + lowercase-keys@3.0.0: {} lru-cache@10.4.3: {} @@ -29405,6 +30139,11 @@ snapshots: marky@1.3.0: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + math-intrinsics@1.1.0: {} md-to-react-email@5.0.6(react@19.2.4): @@ -30065,6 +30804,8 @@ snapshots: mime@1.6.0: {} + mime@2.6.0: {} + mime@3.0.0: {} mime@4.1.0: {} @@ -30077,6 +30818,10 @@ snapshots: mimic-function@5.0.1: {} + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + mimic-response@4.0.0: {} min-indent@1.0.1: {} @@ -30173,6 +30918,10 @@ snapshots: mixpart@0.0.6: {} + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mkdirp@1.0.4: {} mlly@1.8.2: @@ -30253,9 +31002,9 @@ snapshots: named-placeholders: 1.1.6 sql-escaper: 1.3.3 - mysql2@3.22.5(@types/node@22.20.0): + mysql2@3.22.5(@types/node@24.13.3): dependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 @@ -30354,7 +31103,7 @@ snapshots: - '@types/node' - babel-plugin-macros - next@16.3.0(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(@types/node@22.20.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.3.0(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.3.0 '@swc/helpers': 0.5.15 @@ -30376,13 +31125,13 @@ snapshots: '@opentelemetry/api': 1.9.1 '@playwright/test': 1.61.1 babel-plugin-react-compiler: 1.0.0 - sharp: 0.35.3(@types/node@22.20.0) + sharp: 0.35.3(@types/node@24.13.3) transitivePeerDependencies: - '@babel/core' - '@types/node' - babel-plugin-macros - nitropack@2.13.4(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0))(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(xml2js@0.6.2): + nitropack@2.13.4(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(xml2js@0.6.2): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.62.2) @@ -30403,7 +31152,7 @@ snapshots: cookie-es: 2.0.1 croner: 10.0.1 crossws: 0.3.5 - db0: 0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)) + db0: 0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)) defu: 6.1.7 destr: 2.0.5 dot-prop: 10.1.0 @@ -30447,9 +31196,9 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + unimport: 6.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) unplugin-utils: 0.3.2 - unstorage: 1.17.5(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1) + unstorage: 1.17.5(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1) untyped: 2.0.0 unwasm: 0.5.3 youch: 4.1.1 @@ -30494,8 +31243,16 @@ snapshots: - vite - webpack + node-abi@4.33.0: + dependencies: + semver: 7.8.5 + node-addon-api@7.1.1: {} + node-api-version@0.2.1: + dependencies: + semver: 7.8.5 + node-av@5.2.4: dependencies: unzipper: 0.12.5 @@ -30594,6 +31351,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-url@6.1.0: {} + normalize-url@8.1.1: {} npm-bundled@5.0.0: @@ -31051,6 +31810,8 @@ snapshots: pathval@2.0.1: {} + pe-library@0.4.1: {} + peberminta@0.9.0: {} pend@1.2.0: {} @@ -31085,6 +31846,15 @@ snapshots: exsolve: 1.1.0 pathe: 2.0.3 + pkijs@3.4.0: + dependencies: + '@noble/hashes': 1.4.0 + asn1js: 3.0.10 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + playwright-core@1.61.1: {} playwright@1.61.1: @@ -31093,6 +31863,12 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + plist@3.1.1: dependencies: '@xmldom/xmldom': 0.9.10 @@ -31173,6 +31949,11 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + powershell-utils@0.1.0: {} preact-render-to-string@5.2.6(preact@10.29.3): @@ -31243,6 +32024,11 @@ snapshots: promise-call-limit@3.0.2: {} + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + promise@7.3.1: dependencies: asap: 2.0.6 @@ -31262,6 +32048,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@6.5.0: {} property-information@7.2.0: {} @@ -31285,6 +32077,13 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@2.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@1.3.2: {} punycode@2.3.1: {} @@ -31715,6 +32514,12 @@ snapshots: react@19.2.4: {} + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -31980,6 +32785,10 @@ snapshots: requires-port@1.0.0: {} + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + reselect@5.2.0: {} resend@4.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -32015,6 +32824,10 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + responselike@4.0.2: dependencies: lowercase-keys: 3.0.0 @@ -32029,12 +32842,28 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.1.0: {} + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + rimraf@3.0.2: dependencies: glob: 7.2.3 + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + rolldown-plugin-dts@0.17.8(rolldown@1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1))(typescript@5.9.3): dependencies: '@babel/generator': 7.29.7 @@ -32221,6 +33050,10 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + sax@1.2.1: {} sax@1.6.0: {} @@ -32243,12 +33076,17 @@ snapshots: dependencies: parseley: 0.12.1 + semver-compare@1.0.0: + optional: true + semver-regex@4.0.5: {} semver-truncate@3.0.0: dependencies: semver: 7.8.5 + semver@5.7.2: {} + semver@6.3.1: {} semver@7.6.3: {} @@ -32295,6 +33133,11 @@ snapshots: serialize-error@2.1.0: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + serialize-javascript@7.0.7: {} seroval-plugins@1.5.4(seroval@1.5.4): @@ -32418,7 +33261,7 @@ snapshots: '@types/node': 20.19.43 optional: true - sharp@0.35.3(@types/node@22.20.0): + sharp@0.35.3(@types/node@24.13.3): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 @@ -32449,7 +33292,7 @@ snapshots: '@img/sharp-win32-arm64': 0.35.3 '@img/sharp-win32-ia32': 0.35.3 '@img/sharp-win32-x64': 0.35.3 - '@types/node': 22.20.0 + '@types/node': 24.13.3 optional: true shebang-command@2.0.0: @@ -32458,6 +33301,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + shell-quote@1.8.4: {} shell-quote@1.9.0: {} @@ -32541,6 +33386,10 @@ snapshots: dependencies: is-arrayish: 0.3.4 + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -32608,7 +33457,7 @@ snapshots: ip-address: 10.2.0 smart-buffer: 4.2.0 - solid-devtools@0.34.5(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + solid-devtools@0.34.5(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7 '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) @@ -32617,7 +33466,7 @@ snapshots: '@solid-devtools/shared': 0.20.0(solid-js@1.9.13) solid-js: 1.9.13 optionalDependencies: - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -32738,6 +33587,9 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.3: + optional: true + sql-escaper@1.3.3: {} ssri@13.0.1: @@ -32810,6 +33662,8 @@ snapshots: standard-navigation@0.0.7: {} + stat-mode@1.0.0: {} + statuses@1.5.0: {} statuses@2.0.2: {} @@ -32827,16 +33681,16 @@ snapshots: stoppable@1.1.0: {} - storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.14(storybook@8.6.18(prettier@3.9.4)))(esbuild@0.25.12)(rollup@4.62.2)(solid-js@1.9.13)(storybook@8.6.18(prettier@3.9.4))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.14(storybook@8.6.18(prettier@3.9.4)))(esbuild@0.25.12)(rollup@4.62.2)(solid-js@1.9.13)(storybook@8.6.18(prettier@3.9.4))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - '@storybook/builder-vite': 10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@storybook/builder-vite': 10.6.0-alpha.5(esbuild@0.25.12)(rollup@4.62.2)(storybook@8.6.18(prettier@3.9.4))(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) '@storybook/types': 9.0.0-alpha.1(storybook@8.6.18(prettier@3.9.4)) magic-string: 0.30.21 solid-js: 1.9.13 storybook: 8.6.18(prettier@3.9.4) storybook-solidjs: 1.0.0-beta.7(@storybook/test@8.6.14(storybook@8.6.18(prettier@3.9.4)))(solid-js@1.9.13)(storybook@8.6.18(prettier@3.9.4)) - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-solid: 2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-solid: 2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: - '@storybook/test' - esbuild @@ -33043,6 +33897,12 @@ snapshots: tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 + sumchecker@3.0.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + super-regex@1.1.0: dependencies: function-timeout: 1.0.2 @@ -33196,6 +34056,16 @@ snapshots: - bare-abort-controller - react-native-b4a + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + terminal-link@2.1.1: dependencies: ansi-escapes: 4.3.2 @@ -33256,8 +34126,14 @@ snapshots: dependencies: convert-hrtime: 5.0.0 + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + tiny-invariant@1.3.3: {} + tiny-typed-emitter@2.1.0: {} + tinybench@2.9.0: {} tinyclip@0.1.15: {} @@ -33293,6 +34169,10 @@ snapshots: dependencies: tldts-core: 7.4.5 + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.7 + tmp@0.2.7: {} tmpl@1.0.5: {} @@ -33333,6 +34213,10 @@ snapshots: trough@2.2.0: {} + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -33458,6 +34342,9 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.13.1: + optional: true + type-fest@0.20.2: {} type-fest@0.21.3: {} @@ -33584,6 +34471,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.18.2: {} + undici@5.29.0: dependencies: '@fastify/busboy': 2.1.1 @@ -33666,7 +34555,7 @@ snapshots: transitivePeerDependencies: - rollup - unimport@6.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + unimport@6.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: acorn: 8.17.0 escape-string-regexp: 5.0.0 @@ -33680,7 +34569,7 @@ snapshots: scule: 1.3.0 strip-literal: 3.1.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + unplugin: 3.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) unplugin-utils: 0.3.2 transitivePeerDependencies: - '@farmfe/core' @@ -33746,6 +34635,8 @@ snapshots: universal-user-agent@7.0.3: {} + universalify@0.1.2: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -33763,11 +34654,11 @@ snapshots: transitivePeerDependencies: - rollup - unplugin-fonts@1.4.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + unplugin-fonts@1.4.0(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: fast-glob: 3.3.3 unplugin: 2.3.5 - vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) unplugin-icons@0.19.3: dependencies: @@ -33804,7 +34695,7 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + unplugin@3.3.0(esbuild@0.28.1)(rollup@4.62.2)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.4 @@ -33812,7 +34703,7 @@ snapshots: optionalDependencies: esbuild: 0.28.1 rollup: 4.62.2 - vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) unrs-resolver@1.12.2: dependencies: @@ -33841,7 +34732,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - unstorage@1.17.5(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1): + unstorage@1.17.5(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -33854,7 +34745,7 @@ snapshots: optionalDependencies: '@planetscale/database': 1.20.1 '@vercel/functions': 3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0) - db0: 0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)) + db0: 0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)) ioredis: 5.11.1 untun@0.1.3: @@ -33962,6 +34853,8 @@ snapshots: dependencies: react: 19.2.4 + utf8-byte-length@1.0.5: {} + util-deprecate@1.0.2: {} util@0.12.5: @@ -34049,7 +34942,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@22.20.0)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@22.20.0))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0): + vinxi@0.5.11(@planetscale/database@1.20.1)(@types/node@24.13.3)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.32.0)(mysql2@3.22.5(@types/node@24.13.3))(terser@5.48.0)(xml2js@0.6.2)(yaml@2.9.0): dependencies: '@babel/core': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) @@ -34070,7 +34963,7 @@ snapshots: hookable: 5.5.3 http-proxy: 1.18.1 micromatch: 4.0.8 - nitropack: 2.13.4(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0))(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(xml2js@0.6.2) + nitropack: 2.13.4(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(xml2js@0.6.2) node-fetch-native: 1.6.7 path-to-regexp: 6.3.0 pathe: 1.1.2 @@ -34082,8 +34975,8 @@ snapshots: ufo: 1.6.4 unctx: 2.5.0 unenv: 1.10.0 - unstorage: 1.17.5(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)))(mysql2@3.22.5(@types/node@22.20.0)))(ioredis@5.11.1) - vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + unstorage: 1.17.5(@planetscale/database@1.20.1)(@vercel/functions@3.7.4(@aws-sdk/credential-provider-web-identity@3.972.57)(ws@8.21.0))(db0@0.3.4(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@24.13.3)))(mysql2@3.22.5(@types/node@24.13.3)))(ioredis@5.11.1) + vite: 6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) zod: 4.4.3 transitivePeerDependencies: - '@azure/app-configuration' @@ -34137,13 +35030,13 @@ snapshots: - xml2js - yaml - vite-node@2.1.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0): + vite-node@2.1.9(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 5.4.21(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0) transitivePeerDependencies: - '@types/node' - less @@ -34176,13 +35069,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vite-node@3.2.4(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -34197,13 +35090,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -34218,7 +35111,7 @@ snapshots: - tsx - yaml - vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.5.0)(solid-js@1.9.13)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7 '@types/babel__core': 7.20.5 @@ -34226,27 +35119,27 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.13 solid-refresh: 0.6.3(solid-js@1.9.13) - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) optionalDependencies: '@testing-library/jest-dom': 6.5.0 transitivePeerDependencies: - supports-color - vite-plugin-top-level-await@1.6.0(@swc/helpers@0.5.23)(rollup@4.62.2)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + vite-plugin-top-level-await@1.6.0(@swc/helpers@0.5.23)(rollup@4.62.2)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.62.2) '@swc/core': 1.15.43(@swc/helpers@0.5.23) '@swc/wasm': 1.15.43 uuid: 10.0.0 - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' - rollup - vite-plugin-wasm@3.6.0(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + vite-plugin-wasm@3.6.0(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@6.3.5(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: @@ -34259,24 +35152,24 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0): + vite@5.4.21(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0): dependencies: esbuild: 0.21.5 postcss: 8.5.16 rollup: 4.62.2 optionalDependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 fsevents: 2.3.3 lightningcss: 1.32.0 terser: 5.48.0 @@ -34297,7 +35190,7 @@ snapshots: terser: 5.48.0 yaml: 2.9.0 - vite@6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vite@6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -34306,14 +35199,14 @@ snapshots: rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 fsevents: 2.3.3 jiti: 1.21.7 lightningcss: 1.32.0 terser: 5.48.0 yaml: 2.9.0 - vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -34322,14 +35215,14 @@ snapshots: rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 terser: 5.48.0 yaml: 2.9.0 - vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -34338,21 +35231,21 @@ snapshots: rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - vitest@2.1.9(@types/node@22.20.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0): + vitest@2.1.9(@types/node@24.13.3)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -34368,11 +35261,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) - vite-node: 2.1.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 5.4.21(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0) + vite-node: 2.1.9(@types/node@24.13.3)(lightningcss@1.32.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.20.0 + '@types/node': 24.13.3 jsdom: 26.1.0 transitivePeerDependencies: - less @@ -34429,11 +35322,11 @@ snapshots: - tsx - yaml - vitest@3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vitest@3.2.6(@types/debug@4.1.13)(@types/node@24.13.3)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/mocker': 3.2.6(vite@6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -34451,12 +35344,12 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 - '@types/node': 22.20.0 + '@types/node': 24.13.3 '@vitest/ui': 3.2.6(vitest@3.2.6) jsdom: 26.1.0 transitivePeerDependencies: @@ -34473,11 +35366,11 @@ snapshots: - tsx - yaml - vitest@3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): + vitest@3.2.6(@types/debug@4.1.13)(@types/node@24.13.3)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/mocker': 3.2.6(vite@6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -34495,12 +35388,12 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite: 6.3.5(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 - '@types/node': 22.20.0 + '@types/node': 24.13.3 '@vitest/ui': 3.2.6(vitest@3.2.6) jsdom: 26.1.0 transitivePeerDependencies: @@ -34523,6 +35416,17 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + wait-on@9.0.1: + dependencies: + axios: 1.19.0 + joi: 18.2.3 + lodash: 4.18.1 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + - supports-color + walk-up-path@4.0.0: {} walker@1.0.8: @@ -34702,6 +35606,10 @@ snapshots: dependencies: isexe: 3.1.5 + which@5.0.0: + dependencies: + isexe: 3.1.5 + which@6.0.1: dependencies: isexe: 4.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4e721379fd7..591f70404e0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,6 @@ packages: - "apps/*" - "packages/*" - - "crates/tauri-plugin-*" - "infra" - "scripts/orgIdBackfill" diff --git a/scripts/build-desktop-binaries.mjs b/scripts/build-desktop-binaries.mjs index 66af8651a33..d2714aa5103 100644 --- a/scripts/build-desktop-binaries.mjs +++ b/scripts/build-desktop-binaries.mjs @@ -9,7 +9,7 @@ const binariesDir = path.join( repoRoot, "apps", "desktop", - "src-tauri", + "src-backend", "binaries", ); @@ -69,6 +69,17 @@ async function main() { await fs.mkdir(binariesDir, { recursive: true }); for (const sidecar of [ + { + packageName: "cap-desktop", + sourceBinary: "cap-desktop", + destBinaries: ["cap-desktop"], + watchPaths: [ + path.join(repoRoot, "apps", "desktop", "src-backend"), + path.join(repoRoot, "crates", "desktop-runtime"), + path.join(repoRoot, "crates", "desktop-runtime-macros"), + path.join(repoRoot, "Cargo.lock"), + ], + }, { packageName: "cap-muxer", sourceBinary: "cap-muxer", @@ -99,6 +110,16 @@ async function main() { ]) { await buildSidecar(sidecar, target, ext); } + + if (target.includes("windows")) await stageWindowsRuntimeLibraries(); +} + +async function stageWindowsRuntimeLibraries() { + const releaseDir = path.join(repoRoot, "target", "release"); + for (const name of await fs.readdir(releaseDir)) { + if (!name.toLowerCase().endsWith(".dll")) continue; + await fs.copyFile(path.join(releaseDir, name), path.join(binariesDir, name)); + } } async function buildSidecar(sidecar, target, ext) { diff --git a/scripts/build-desktop-binaries.sh b/scripts/build-desktop-binaries.sh index ee08300f9db..a767b94cfff 100755 --- a/scripts/build-desktop-binaries.sh +++ b/scripts/build-desktop-binaries.sh @@ -7,7 +7,7 @@ if [[ -z "$TARGET" ]]; then fi REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -BINARIES_DIR="$REPO_ROOT/apps/desktop/src-tauri/binaries" +BINARIES_DIR="$REPO_ROOT/apps/desktop/src-backend/binaries" EXT="" case "$TARGET" in @@ -39,5 +39,10 @@ build_sidecar() { done } +build_sidecar "cap-desktop" "cap-desktop" "cap-desktop" build_sidecar "cap-muxer" "cap-muxer" "cap-muxer" build_sidecar "cap" "cap" "cap-cli" "cap-exporter" + +if [[ "$TARGET" == *windows* ]]; then + find "$REPO_ROOT/target/release" -maxdepth 1 -type f -name '*.dll' -exec cp {} "$BINARIES_DIR/" \; +fi diff --git a/scripts/check-tauri-plugin-versions.js b/scripts/check-tauri-plugin-versions.js deleted file mode 100755 index 80fae557904..00000000000 --- a/scripts/check-tauri-plugin-versions.js +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env node - -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Check for debug flag -const DEBUG = process.argv.includes("--debug") || process.env.DEBUG === "true"; - -function debug(message) { - if (DEBUG) { - console.log(`[DEBUG] ${message}`); - } -} - -// Helper function to parse semantic version -function parseVersion(version) { - const match = version.match(/^(\d+)\.(\d+)\.(\d+)/); - if (!match) { - throw new Error(`Invalid version format: ${version}`); - } - return { - major: parseInt(match[1], 10), - minor: parseInt(match[2], 10), - patch: parseInt(match[3], 10), - full: version, - }; -} - -// Parse pnpm-lock.yaml to extract Tauri plugin versions -function parsePnpmLock(lockfilePath) { - const content = fs.readFileSync(lockfilePath, "utf8"); - const plugins = {}; - - const lines = content.split("\n"); - let currentPlugin = null; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Look for @tauri-apps/plugin- packages (with or without quotes) - const pluginMatch = line.match( - /^\s*['"]?(@tauri-apps\/plugin-[^'":\s]+)['"]?:\s*$/, - ); - if (pluginMatch) { - currentPlugin = pluginMatch[1]; - debug(`Found JS plugin: ${currentPlugin} at line ${i + 1}`); - continue; - } - - // Look for version when we're tracking a plugin - if (currentPlugin) { - const versionMatch = line.match(/^\s*version:\s*['"]?([^'"]+)['"]?\s*$/); - if (versionMatch) { - plugins[currentPlugin] = versionMatch[1]; - debug(` Version: ${versionMatch[1]}`); - currentPlugin = null; - } - - // Reset if we hit another top-level key - if ( - line.match(/^\s*[^'":\s]+:\s*$/) && - !line.match(/^\s*(specifier|version):/) - ) { - currentPlugin = null; - } - } - - // Also check for inline version specifications in dependency lists - const inlineMatch = line.match(/@tauri-apps\/plugin-([^@]+)@([^)]+)/); - if (inlineMatch) { - const pluginName = inlineMatch[1]; - const version = inlineMatch[2]; - plugins[`@tauri-apps/plugin-${pluginName}`] = version; - debug( - `Found inline JS plugin: @tauri-apps/plugin-${pluginName}@${version}`, - ); - } - } - - return plugins; -} - -// Parse Cargo.lock to extract Tauri plugin versions -function parseCargoLock(lockfilePath) { - const content = fs.readFileSync(lockfilePath, "utf8"); - const plugins = {}; - - const lines = content.split("\n"); - let currentPackage = null; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Look for package declarations - const packageMatch = line.match(/^name = "tauri-plugin-(.+)"$/); - if (packageMatch) { - currentPackage = `tauri-plugin-${packageMatch[1]}`; - debug(`Found Rust plugin: ${currentPackage} at line ${i + 1}`); - continue; - } - - // Look for version when we're in a tauri-plugin package - if (currentPackage && line.match(/^version = "(.+)"$/)) { - const versionMatch = line.match(/^version = "(.+)"$/); - if (versionMatch) { - plugins[currentPackage] = versionMatch[1]; - debug(` Version: ${versionMatch[1]}`); - currentPackage = null; - } - } - - // Reset when we hit a new [[package]] block - if (line.match(/^\[\[package\]\]$/)) { - currentPackage = null; - } - } - - return plugins; -} - -// Map plugin names between JS and Rust -function mapPluginNames(jsName, rustPlugins) { - // Convert @tauri-apps/plugin-clipboard-manager to tauri-plugin-clipboard-manager - const rustName = jsName.replace("@tauri-apps/plugin-", "tauri-plugin-"); - return rustPlugins[rustName] ? rustName : null; -} - -// Compare versions and report mismatches -function compareVersions(jsPlugins, rustPlugins) { - const results = { - matching: [], - mismatched: [], - jsOnly: [], - rustOnly: [], - }; - - // Check JS plugins against Rust plugins - for (const [jsName, jsVersion] of Object.entries(jsPlugins)) { - const rustName = mapPluginNames(jsName, rustPlugins); - - if (!rustName) { - results.jsOnly.push({ name: jsName, version: jsVersion }); - continue; - } - - const rustVersion = rustPlugins[rustName]; - - try { - const jsParsed = parseVersion(jsVersion); - const rustParsed = parseVersion(rustVersion); - - if ( - jsParsed.major === rustParsed.major && - jsParsed.minor === rustParsed.minor - ) { - results.matching.push({ - jsName, - rustName, - jsVersion, - rustVersion, - majorMinor: `${jsParsed.major}.${jsParsed.minor}`, - }); - } else { - results.mismatched.push({ - jsName, - rustName, - jsVersion, - rustVersion, - jsMajorMinor: `${jsParsed.major}.${jsParsed.minor}`, - rustMajorMinor: `${rustParsed.major}.${rustParsed.minor}`, - }); - } - } catch (error) { - console.warn( - `Warning: Could not parse versions for ${jsName} (${jsVersion}) / ${rustName} (${rustVersion}): ${error.message}`, - ); - } - } - - // Check for Rust-only plugins - for (const [rustName, rustVersion] of Object.entries(rustPlugins)) { - const jsName = `@tauri-apps/plugin-${rustName.replace("tauri-plugin-", "")}`; - if (!jsPlugins[jsName]) { - results.rustOnly.push({ name: rustName, version: rustVersion }); - } - } - - return results; -} - -// Main function -function main() { - const rootDir = path.resolve(__dirname, ".."); - const pnpmLockPath = path.join(rootDir, "pnpm-lock.yaml"); - const cargoLockPath = path.join(rootDir, "Cargo.lock"); - - // Check if files exist - if (!fs.existsSync(pnpmLockPath)) { - console.error(`❌ Error: pnpm-lock.yaml not found at ${pnpmLockPath}`); - console.error("Please run this script from the project root directory."); - process.exit(1); - } - - if (!fs.existsSync(cargoLockPath)) { - console.error(`❌ Error: Cargo.lock not found at ${cargoLockPath}`); - console.error("Please run this script from the project root directory."); - process.exit(1); - } - - debug(`Reading pnpm lockfile: ${pnpmLockPath}`); - debug(`Reading Cargo lockfile: ${cargoLockPath}`); - - console.log("🔍 Checking Tauri plugin version consistency...\n"); - - try { - // Parse both lockfiles - const jsPlugins = parsePnpmLock(pnpmLockPath); - const rustPlugins = parseCargoLock(cargoLockPath); - - console.log(`Found ${Object.keys(jsPlugins).length} JS Tauri plugins`); - console.log( - `Found ${Object.keys(rustPlugins).length} Rust Tauri plugins\n`, - ); - - if (DEBUG) { - console.log("JS Plugins found:"); - Object.entries(jsPlugins).forEach(([name, version]) => { - console.log(` ${name}@${version}`); - }); - console.log("\nRust Plugins found:"); - Object.entries(rustPlugins).forEach(([name, version]) => { - console.log(` ${name}@${version}`); - }); - console.log(); - } - - // Compare versions - const results = compareVersions(jsPlugins, rustPlugins); - - // Report results - if (results.matching.length > 0) { - console.log("✅ Matching versions (major.minor):"); - results.matching.forEach( - ({ jsName, rustName, jsVersion, rustVersion, majorMinor }) => { - console.log( - ` ${majorMinor}: ${jsName}@${jsVersion} ↔ ${rustName}@${rustVersion}`, - ); - }, - ); - console.log(); - } - - let hasErrors = false; - - if (results.mismatched.length > 0) { - hasErrors = true; - console.log("❌ Version mismatches (major.minor):"); - results.mismatched.forEach( - ({ - jsName, - rustName, - jsVersion, - rustVersion, - jsMajorMinor, - rustMajorMinor, - }) => { - console.log( - ` ${jsName}@${jsVersion} (${jsMajorMinor}) ↔ ${rustName}@${rustVersion} (${rustMajorMinor})`, - ); - }, - ); - console.log(); - } - - if (results.jsOnly.length > 0) { - console.log("⚠️ JS-only plugins (no Rust equivalent found):"); - results.jsOnly.forEach(({ name, version }) => { - console.log(` ${name}@${version}`); - }); - console.log(); - } - - if (results.rustOnly.length > 0) { - console.log("⚠️ Rust-only plugins (no JS equivalent found):"); - results.rustOnly.forEach(({ name, version }) => { - console.log(` ${name}@${version}`); - }); - console.log(); - } - - // Summary - if (hasErrors) { - console.log("💥 Version consistency check failed!"); - console.log( - "Please ensure that Tauri plugins have matching major.minor versions between package.json and Cargo.toml", - ); - process.exit(1); - } else { - console.log("🎉 All Tauri plugin versions are consistent!"); - } - } catch (error) { - console.error(`❌ Error: ${error.message}`); - if (DEBUG) { - console.error("Stack trace:", error.stack); - } - process.exit(1); - } -} - -// Show usage if help flag is passed -if (process.argv.includes("--help") || process.argv.includes("-h")) { - console.log(` -Usage: node check-tauri-plugin-versions.js [options] - -Options: - --debug Enable debug logging - --help Show this help message - -Description: - Checks that all @tauri-apps/plugin-* packages have matching major.minor - versions with their corresponding tauri-plugin-* crates. - - The script reads pnpm-lock.yaml and Cargo.lock from the project root - and compares versions to ensure compatibility. - -Exit codes: - 0 - All versions match - 1 - Version mismatches found or error occurred -`); - process.exit(0); -} - -main(); diff --git a/scripts/setup.js b/scripts/setup.js index 9b42839ea77..feec22ca063 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -279,7 +279,6 @@ async function main() { console.log( `Staged ${sonameLibs.length} FFmpeg shared libraries for Linux bundling`, ); - await writeLinuxTauriConfig(sonameLibs); cargoConfigContents += `\n[target.${triple}]\nrustflags = ["-C", "link-arg=-Wl,-rpath,$ORIGIN", "-C", "link-arg=-Wl,-rpath,$ORIGIN/../lib/cap"]\n`; } @@ -512,30 +511,6 @@ async function fileExists(path) { .catch(() => false); } -async function writeLinuxTauriConfig(sonameLibs) { - const configPath = path.join( - __root, - "apps", - "desktop", - "src-tauri", - "tauri.linux.conf.json", - ); - const files = {}; - - for (const name of sonameLibs.toSorted()) { - files[`/usr/lib/cap/${name}`] = - `../../../target/native-deps/cap-deb-libs/${name}`; - } - - await writeFileIfChanged( - configPath, - `${JSON.stringify({ bundle: { linux: { deb: { files } } } }, null, "\t")}\n`, - ); - console.log( - `Generated Linux Tauri deb config with ${sonameLibs.length} shared libraries`, - ); -} - async function missingFiles(dir, names) { if (!(await fileExists(dir))) return names; diff --git a/scripts/verify-recording-pipeline.mjs b/scripts/verify-recording-pipeline.mjs index e261f42abf9..9d2e4b7af5a 100644 --- a/scripts/verify-recording-pipeline.mjs +++ b/scripts/verify-recording-pipeline.mjs @@ -41,7 +41,7 @@ const desktopBundleId = const signDesktopApp = process.env.CAP_VERIFY_SIGN_DESKTOP !== "0"; const desktopEntitlementsPath = process.env.CAP_VERIFY_DESKTOP_ENTITLEMENTS ? path.resolve(process.env.CAP_VERIFY_DESKTOP_ENTITLEMENTS) - : path.join(root, "apps", "desktop", "src-tauri", "Entitlements.plist"); + : path.join(root, "apps", "desktop", "src-backend", "Entitlements.plist"); const desktopLogDirs = ["so.cap.desktop.dev", "so.cap.desktop"].map((name) => path.join(os.homedir(), "Library", "Logs", name), ); diff --git a/turbo.json b/turbo.json index bf4056ab19c..1085a2f729a 100644 --- a/turbo.json +++ b/turbo.json @@ -4,7 +4,7 @@ "globalEnv": ["*"], "tasks": { "build": { - "inputs": ["**/*.ts", "**/*.tsx", "!src-tauri/**", "!node_modules/**"], + "inputs": ["**/*.ts", "**/*.tsx", "!src-backend/**", "!node_modules/**"], "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, @@ -32,12 +32,12 @@ "outputs": ["dist/**"] }, "build:web:docker": { - "inputs": ["**/*.ts", "**/*.tsx", "!src-tauri/**", "!node_modules/**"], + "inputs": ["**/*.ts", "**/*.tsx", "!src-backend/**", "!node_modules/**"], "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "build:web": { - "inputs": ["**/*.ts", "**/*.tsx", "!src-tauri/**", "!node_modules/**"], + "inputs": ["**/*.ts", "**/*.tsx", "!src-backend/**", "!node_modules/**"], "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, From 3a5711784bc77c733b113c855c762c5a5531ba23 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:36:59 +0100 Subject: [PATCH 03/31] ci(desktop): build and publish Electron releases --- .../actions/install-desktop-deps/action.yml | 1 - .github/workflows/ci.yml | 37 +- .github/workflows/desktop-release.yml | 602 +++++------------- .github/workflows/promote-nightly.yml | 2 +- .github/workflows/publish-nightly.yml | 25 +- .github/workflows/publish.yml | 8 +- 6 files changed, 186 insertions(+), 489 deletions(-) diff --git a/.github/actions/install-desktop-deps/action.yml b/.github/actions/install-desktop-deps/action.yml index 6da52f04fd0..4b86762bce1 100644 --- a/.github/actions/install-desktop-deps/action.yml +++ b/.github/actions/install-desktop-deps/action.yml @@ -19,7 +19,6 @@ runs: run: | sudo apt update sudo apt install -y --no-install-recommends \ - libwebkit2gtk-4.1-dev \ build-essential \ pkg-config \ curl \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c42007a69ad..ec0d3aa4d95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest outputs: rust: ${{ steps.filter.outputs.rust }} - tauri-plugins: ${{ steps.filter.outputs.tauri-plugins }} + desktop: ${{ steps.filter.outputs.desktop }} branch: ${{ steps.branch.outputs.branch }} steps: - name: Checkout @@ -37,12 +37,9 @@ jobs: - '.github/**' - 'apps/cli/**' - 'crates/**' - - 'apps/desktop/src-tauri/**' + - 'apps/desktop/src-backend/**' - 'Cargo.toml' - 'Cargo.lock' - tauri-plugins: - - 'Cargo.lock' - - 'pnpm-lock.yaml' - name: Output branch name id: branch @@ -242,27 +239,19 @@ jobs: - name: Build app working-directory: apps/desktop - run: pnpm tauri build --debug --target ${{ matrix.settings.target }} --no-bundle + shell: bash + run: | + pnpm run preparescript + pnpm build + pnpm build:electron-main + if [[ "$RUNNER_OS" == "macOS" ]]; then + pnpm exec electron-builder --dir --mac --arm64 + else + pnpm exec electron-builder --dir --win --x64 + fi env: RUST_TARGET_TRIPLE: ${{ matrix.settings.target }} - - tauri-plugins: - name: Verify Tauri plugin versions - runs-on: ubuntu-latest - needs: changes - if: needs.changes.outputs.tauri-plugins == 'true' - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: 20 - - - name: Run verify - shell: bash - run: node scripts/check-tauri-plugin-versions.js + CSC_IDENTITY_AUTO_DISCOVERY: false rust-cache: name: Rust cache diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index e22099e885f..86f565dc2dc 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -1,44 +1,27 @@ -name: "desktop-release" - -# Reusable core of the desktop release pipeline. Callers: -# - publish.yml -> channel: stable, version from Cargo.toml, manual CN publish -# - publish-nightly.yml -> channel: nightly, generated prerelease version, auto CN publish -# - promote-nightly.yml -> channel: stable, rebuilds a nightly's commit as a plain x.y.z -# -# Stable releases get a cap-v{version} git tag + draft GitHub release and are -# published on CrabNebula manually (Discord bot / dashboard), exactly as before. -# Nightly releases skip the GitHub side entirely and go to the CrabNebula -# "nightly" channel, which is invisible to the default update endpoint and to -# the website's `latest` download URLs. +name: desktop-release on: workflow_call: inputs: version: - description: "Version to stamp into the build and the CrabNebula release" required: true type: string channel: - description: "Release channel: stable or nightly" required: true type: string sha: - description: "Commit SHA to build (and to tag, for stable releases)" required: true type: string auto_publish: - description: "Publish the CrabNebula release automatically after upload" required: false type: boolean default: false interactionId: - description: "Discord Interaction ID" required: false type: string default: "" env: - CN_APPLICATION: cap/cap SENTRY_ORG: cap-s2 SENTRY_PROJECT: cap-desktop @@ -46,237 +29,125 @@ jobs: draft: runs-on: ubuntu-latest outputs: - tag_name: ${{ steps.create_tag.outputs.tag_name }} - needs_release: ${{ steps.create_tag.outputs.release_published != 'true' }} - cn_release_stdout: ${{ steps.create_cn_release.outputs.stdout }} - gh_release_url: ${{ steps.create_gh_release.outputs.url || steps.create_tag.outputs.release_url }} + tag_name: ${{ steps.release.outputs.tag_name }} + release_url: ${{ steps.release.outputs.release_url }} permissions: contents: write - id-token: write - env: - CN_CHANNEL_FLAG: ${{ inputs.channel == 'nightly' && '--channel nightly' || '' }} steps: - - name: Prepare release - id: create_tag - if: ${{ inputs.channel == 'stable' }} + - name: Create or update release tag and draft + id: release uses: actions/github-script@v7 with: script: | - const tag = "cap-v${{ inputs.version }}"; - const buildSha = "${{ inputs.sha }}"; - const tagRef = `tags/${tag}`; - - const TAG_EXISTED = "tag_existed"; - const TAG_NAME = "tag_name"; - const RELEASE_PUBLISHED = "release_published"; - const RELEASE_URL = "release_url"; - - core.setOutput(TAG_NAME, tag); - - async function main() { - let tagExisted = true; - let tagSha = ""; - let releasePublished = false; - let releaseUrl = ""; - - try { - const tagRefResponse = await github.rest.git.getRef({ - ref: tagRef, - owner: context.repo.owner, - repo: context.repo.repo, - }); - - tagExisted = true; - tagSha = tagRefResponse.data.object.sha; - core.notice(`Tag '${tag}' already exists. Reusing it for this release run.`); - } catch (error) { - if ("status" in error && error.status === 404) tagExisted = false; - else throw error; - } - - if (!tagExisted) - await github.rest.git.createRef({ - ref: `refs/${tagRef}`, - owner: context.repo.owner, - repo: context.repo.repo, - sha: buildSha, - }); + const tag = "${{ inputs.channel }}" === "nightly" + ? `cap-nightly-v${{ inputs.version }}` + : `cap-v${{ inputs.version }}`; + const sha = "${{ inputs.sha }}"; + try { + await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `tags/${tag}`, sha, force: true }); + } catch (error) { + if (error.status !== 422 && error.status !== 404) throw error; + await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/tags/${tag}`, sha }); + } - const releases = await github.paginate(github.rest.repos.listReleases, { + let release; + try { + release = (await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag })).data; + if (!release.draft) throw new Error(`Release '${tag}' is already published: ${release.html_url}`); + release = (await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, - per_page: 100, - }); - - const release = releases.find((release) => release.tag_name === tag); - - if (release) { - releaseUrl = release.html_url; - releasePublished = !release.draft; - - if (releasePublished) - throw new Error(`Release '${tag}' is already published: ${releaseUrl}. Bump apps/desktop/src-tauri/Cargo.toml before publishing a new build.`); - else - core.notice(`Draft release '${tag}' already exists. Continuing build and upload.`); - } - - if (tagExisted && !releasePublished && tagSha !== buildSha) { - await github.rest.git.updateRef({ - ref: tagRef, - owner: context.repo.owner, - repo: context.repo.repo, - sha: buildSha, - force: true, - }); - - core.notice(`Updated unpublished tag '${tag}' to ${buildSha}.`); - } - - core.setOutput(TAG_EXISTED, tagExisted); - core.setOutput(RELEASE_PUBLISHED, releasePublished); - core.setOutput(RELEASE_URL, releaseUrl); - } - - await main(); - - - name: Create draft CN release - id: create_cn_release - if: ${{ steps.create_tag.outputs.release_published != 'true' }} - uses: crabnebula-dev/cloud-release@v0 - with: - command: release draft ${{ env.CN_APPLICATION }} ${{ inputs.version }} --framework tauri ${{ env.CN_CHANNEL_FLAG }} - api-key: ${{ secrets.CN_API_KEY }} - - - name: Create draft GH release - id: create_gh_release - if: ${{ inputs.channel == 'stable' && steps.create_tag.outputs.release_published != 'true' }} - # TODO: Change to stable version when available - uses: softprops/action-gh-release@v2 - with: - name: ${{ inputs.version }} - tag_name: ${{ steps.create_tag.outputs.tag_name }} - draft: true - generate_release_notes: true - - - name: Update Discord interaction - if: ${{ inputs.channel == 'stable' && inputs.interactionId != '' && steps.create_tag.outputs.release_published != 'true' }} - uses: actions/github-script@v7 - with: - script: | - async function main() { - const token = await core.getIDToken("cap-discord-bot"); - const cnReleaseId = JSON.parse(`${{ steps.create_cn_release.outputs.stdout }}`).id; - - const resp = await fetch("https://cap-discord-bot.brendonovich.workers.dev/github-workflow", { - method: "POST", - body: JSON.stringify({ - type: "release-ready", - tag: "${{ steps.create_tag.outputs.tag_name }}", - version: "${{ inputs.version }}", - releaseUrl: "${{ steps.create_gh_release.outputs.url }}", - interactionId: "${{ inputs.interactionId }}", - cnReleaseId - }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - } - }); - - if(resp.status !== 200) throw new Error(await resp.text()); + release_id: release.id, + target_commitish: sha, + name: "${{ inputs.version }}", + prerelease: "${{ inputs.channel }}" === "nightly", + })).data; + } catch (error) { + if (error.status !== 404) throw error; + release = (await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + target_commitish: sha, + name: "${{ inputs.version }}", + draft: true, + prerelease: "${{ inputs.channel }}" === "nightly", + generate_release_notes: true, + })).data; } - - main(); + core.setOutput("tag_name", tag); + core.setOutput("release_url", release.html_url); build: needs: draft - if: ${{ needs.draft.outputs.needs_release == 'true' }} permissions: contents: write actions: read - id-token: write strategy: fail-fast: false matrix: settings: - target: x86_64-apple-darwin runner: macos-latest-xlarge + builder_args: --mac --x64 - target: aarch64-apple-darwin runner: macos-latest-xlarge + builder_args: --mac --arm64 - target: x86_64-pc-windows-msvc runner: windows-2022 + builder_args: --win --x64 - target: x86_64-unknown-linux-gnu runner: ubuntu-24.04 + builder_args: --linux deb --x64 + runs-on: ${{ matrix.settings.runner }} env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - CN_CHANNEL_FLAG: ${{ inputs.channel == 'nightly' && '--channel nightly' || '' }} - runs-on: ${{ matrix.settings.runner }} steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: ref: ${{ inputs.sha }} - # Nightly and promoted builds carry a version that differs from the - # committed Cargo.toml; Tauri reads the app version from package.version. - - name: Stamp build version + - name: Stamp Electron version shell: bash env: BUILD_VERSION: ${{ inputs.version }} run: | node -e ' const fs = require("fs"); - const path = "apps/desktop/src-tauri/Cargo.toml"; - const version = process.env.BUILD_VERSION; - let toml = fs.readFileSync(path, "utf8"); - const re = /^version = ".*"$/m; - if (!re.test(toml)) { - console.error("package.version line not found in " + path); - process.exit(1); - } - toml = toml.replace(re, `version = "${version}"`); - fs.writeFileSync(path, toml); - console.log("Stamped " + path + " with " + version); + const path = "apps/desktop/package.json"; + const json = JSON.parse(fs.readFileSync(path, "utf8")); + json.version = process.env.BUILD_VERSION; + fs.writeFileSync(path, JSON.stringify(json, null, "\t") + "\n"); ' - name: Install Linux desktop dependencies if: ${{ runner.os == 'Linux' }} uses: ./.github/actions/install-desktop-deps - - name: Create API Key File - if: ${{ runner.os == 'macOS' }} - run: echo "${{ secrets.APPLE_API_KEY_FILE }}" > api.p8 - - - uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2 + - name: Import macOS signing certificate if: ${{ runner.os == 'macOS' }} + uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 with: p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - name: Verify certificate + - name: Write Apple notarization key if: ${{ runner.os == 'macOS' }} - run: security find-identity -v -p codesigning ${{ runner.temp }}/build.keychain + shell: bash + run: echo "${{ secrets.APPLE_API_KEY_FILE }}" > "$RUNNER_TEMP/AuthKey.p8" - - name: Rust setup - uses: dtolnay/rust-toolchain@1.88.0 + - uses: dtolnay/rust-toolchain@1.88.0 with: targets: ${{ matrix.settings.target }} - - name: Verify Rust toolchain - shell: bash - run: | - rustc -Vv - cargo -V - rustup show active-toolchain - - uses: ./.github/actions/setup-rust-cache with: target: ${{ matrix.settings.target }} - uses: ./.github/actions/setup-js - - name: Create .env file in root + - name: Create build environment shell: bash run: | { @@ -291,186 +162,71 @@ jobs: echo 'RUST_TARGET_TRIPLE=${{ matrix.settings.target }}' } >> .env - - name: Run cap-setup + - name: Prepare native dependencies shell: bash run: pnpm -w cap-setup env: RUST_TARGET_TRIPLE: ${{ matrix.settings.target }} APPLE_SIGNING_IDENTITY: ${{ runner.os == 'macOS' && secrets.APPLE_SIGNING_IDENTITY || '' }} - APPLE_KEYCHAIN: ${{ runner.os == 'macOS' && format('{0}/build.keychain', runner.temp) || '' }} - - name: Build desktop binaries - shell: bash - run: | - if [[ "$RUNNER_OS" == "Linux" ]]; then - export CARGO_PROFILE_RELEASE_DEBUG=0 - fi - ./scripts/build-desktop-binaries.sh ${{ matrix.settings.target }} - - # The release profile uses fat LTO + full debuginfo, so the final link - # peaks well above the 16GB stock Linux runner and gets OOM-killed - # ("runner received a shutdown signal"). The runner already ships an - # active /swapfile (resizing it fails with "Text file busy"), so add a - # large swapfile on the roomy /mnt disk on top of the existing swap. - # macOS runs on xlarge runners and is unaffected. - - name: Increase swap space (Linux) + - name: Increase Linux swap if: ${{ runner.os == 'Linux' }} shell: bash run: | - sudo swapoff /mnt/swapfile 2>/dev/null || true - sudo rm -f /mnt/swapfile - sudo fallocate -l 16G /mnt/swapfile - sudo chmod 600 /mnt/swapfile - sudo mkswap /mnt/swapfile - sudo swapon /mnt/swapfile - swapon --show - free -h + sudo fallocate -l 16G /mnt/electron-swapfile + sudo chmod 600 /mnt/electron-swapfile + sudo mkswap /mnt/electron-swapfile + sudo swapon /mnt/electron-swapfile - # The same fat-LTO link exhausts commit on the 16GB Windows runner - # ("rustc-LLVM ERROR: out of memory" while linking cap-desktop), so - # raise the pagefile on the roomy D: disk — the Windows analog of the - # Linux swapfile above. - - name: Increase pagefile size (Windows) + - name: Increase Windows pagefile if: ${{ runner.os == 'Windows' }} - uses: al-cheb/configure-pagefile-action@a3b6ebd6b634da88790d9c58d4b37a7f4a7b8708 # v1.4 + uses: al-cheb/configure-pagefile-action@a3b6ebd6b634da88790d9c58d4b37a7f4a7b8708 with: minimum-size: 16GB maximum-size: 24GB disk-root: "D:" - - name: Build app + - name: Build Rust backend and sidecars + shell: bash + run: ./scripts/build-desktop-binaries.sh ${{ matrix.settings.target }} + env: + CARGO_PROFILE_RELEASE_DEBUG: ${{ runner.os == 'Linux' && '0' || '' }} + + - name: Build renderer and Electron distributable working-directory: apps/desktop - run: ${{ runner.os == 'Linux' && 'CARGO_PROFILE_RELEASE_DEBUG=0 ' || '' }}pnpm build:tauri --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json ${{ runner.os == 'Windows' && '--bundles nsis' || runner.os == 'Linux' && '--bundles deb --verbose' || '' }} + shell: bash + run: | + pnpm run preparescript + pnpm build + pnpm build:electron-main + pnpm exec electron-builder ${{ matrix.settings.builder_args }} --publish never env: - # https://github.com/tauri-apps/tauri-action/issues/740 CI: false - LD_LIBRARY_PATH: ${{ runner.os == 'Linux' && format('{0}/target/{1}/release:{0}/target/native-deps/lib', github.workspace, matrix.settings.target) || '' }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # codesigning - APPLE_CERTIFICATE: ${{ runner.os == 'macOS' && secrets.APPLE_CERTIFICATE || '' }} - APPLE_CERTIFICATE_PASSWORD: ${{ runner.os == 'macOS' && secrets.APPLE_CERTIFICATE_PASSWORD || '' }} - APPLE_SIGNING_IDENTITY: ${{ runner.os == 'macOS' && secrets.APPLE_SIGNING_IDENTITY || '' }} - # notarization - APPLE_API_ISSUER: ${{ runner.os == 'macOS' && secrets.APPLE_API_ISSUER || '' }} - APPLE_API_KEY: ${{ runner.os == 'macOS' && secrets.APPLE_API_KEY || '' }} - APPLE_API_KEY_PATH: ${{ runner.os == 'macOS' && format('{0}/api.p8', github.workspace) || '' }} - APPLE_KEYCHAIN: ${{ runner.os == 'macOS' && format('{0}/build.keychain', runner.temp) || '' }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RUST_TARGET_TRIPLE: ${{ matrix.settings.target }} + CSC_NAME: ${{ runner.os == 'macOS' && secrets.APPLE_SIGNING_IDENTITY || '' }} + APPLE_API_KEY: ${{ runner.os == 'macOS' && format('{0}/AuthKey.p8', runner.temp) || '' }} + APPLE_API_KEY_ID: ${{ runner.os == 'macOS' && secrets.APPLE_API_KEY || '' }} + APPLE_API_ISSUER: ${{ runner.os == 'macOS' && secrets.APPLE_API_ISSUER || '' }} - # Guards against the Linux packaging regressions: - # 1. Frontend missing: vinxi must produce apps/desktop/.output/public, - # which Tauri EMBEDS into the binary (it is NOT shipped as a loose - # /usr/lib/Cap/index.html file). A blank webview ships if turbo's cache - # restores an empty .output/public (see @cap/desktop#build outputs in - # turbo.json), so we assert the build output exists and is non-empty. - # 2. FFmpeg soname mismatch: every libav*/libsw* the main binary lists as - # NEEDED must be bundled under /usr/lib/cap/ with the exact same soname. - # The bundled libs come from native-deps (FFmpeg 7); if the binary - # linked system FFmpeg 6 the loader fails at launch. - # 3. Undeclared runtime dependency: a system lib the binary NEEDs that is - # neither bundled nor part of the declared GTK/webkit stack must appear - # in the .deb Depends, or the loader aborts at launch. This is the - # libpipewire-0.3.so.0 crash that shipped in 0.5.2. - - name: Verify Linux deb contents - if: ${{ runner.os == 'Linux' }} + - name: Verify packaged application shell: bash run: | - set -euo pipefail - - # (1) frontend was actually built. Tauri embeds .output/public into the - # binary at build time, so an empty/missing dir => blank webview. - FE="apps/desktop/.output/public/index.html" - if [[ ! -s "$FE" ]]; then - echo "::error::Frontend build output missing/empty ($FE) - webview would be blank." - ls -la apps/desktop/.output/public 2>/dev/null || echo "(no .output/public dir)" - exit 1 - fi - echo "Frontend build output OK ($FE, $(wc -c < "$FE") bytes)" - - UNSUPPORTED_ARTIFACT="$(find target/${{ matrix.settings.target }}/release/bundle -type f \( -name '*.AppImage' -o -name '*.rpm' \) -print -quit)" - if [[ -n "$UNSUPPORTED_ARTIFACT" ]]; then - echo "::error::Unexpected Linux non-deb artifact produced: $UNSUPPORTED_ARTIFACT" - exit 1 - fi - - DEB="$(find target/${{ matrix.settings.target }}/release/bundle/deb -name '*.deb' | head -n1)" - if [[ -z "$DEB" ]]; then echo "::error::No .deb produced"; exit 1; fi - if [[ ! -s "$DEB.sig" ]]; then echo "::error::No .deb updater signature produced next to $DEB"; exit 1; fi - echo "Inspecting $DEB" - WORK="$(mktemp -d)" - dpkg-deb -x "$DEB" "$WORK" - - # Best-effort confirmation that the SPA actually got embedded into the - # binary (Tauri stores asset path keys as plaintext). Informational only. - if strings "$WORK/usr/bin/Cap" 2>/dev/null | grep -q "index.html"; then - echo "Binary appears to embed the frontend (index.html key present)" - else - echo "::warning::Could not confirm embedded frontend key in binary (asset format may differ)" - fi - - # (2) every FFmpeg soname the binary NEEDs is bundled - BIN="$WORK/usr/bin/Cap" - NEEDED="$(objdump -p "$BIN" | awk '/NEEDED/{print $2}' | grep -E '^lib(av|sw|postproc)' || true)" - echo "Binary NEEDs FFmpeg sonames:"; echo "${NEEDED:-(none)}" - echo "Bundled FFmpeg libs:"; ls -1 "$WORK/usr/lib/cap" 2>/dev/null || true - MISSING=0 - for so in $NEEDED; do - if [[ ! -f "$WORK/usr/lib/cap/$so" ]]; then - echo "::error::Binary NEEDs $so but it is not bundled under /usr/lib/cap/" - MISSING=1 - fi - done - if [[ "$MISSING" -ne 0 ]]; then - echo "::error::FFmpeg soname mismatch between binary and bundled libs." - exit 1 - fi - echo "FFmpeg soname bundle OK" - - # (3) runtime libs that are NOT bundled and NOT pulled in by the - # declared GTK/webkit stack must be declared in the .deb Depends, or - # the dynamic loader aborts at launch (the libpipewire-0.3.so.0 crash - # in 0.5.2). Assert the curated set of such sonames the binary links. - DEPENDS="$(dpkg-deb -f "$DEB" Depends || true)" - echo "Declared Depends: ${DEPENDS:-(none)}" - NEEDED_ALL="$(objdump -p "$BIN" | awk '/NEEDED/{print $2}')" - # soname -> a Depends token that satisfies it. The alsa token is a - # substring so it matches both libasound2 and libasound2t64. - declare -A SONAME_DEP=( - [libpipewire-0.3.so.0]="libpipewire-0.3-0" - [libasound.so.2]="libasound2" - ) - DEP_MISSING=0 - for so in "${!SONAME_DEP[@]}"; do - if echo "$NEEDED_ALL" | grep -qx "$so"; then - tok="${SONAME_DEP[$so]}" - if ! echo "$DEPENDS" | grep -q "$tok"; then - echo "::error::Binary NEEDs $so but '$tok' is not declared in .deb Depends (add it to tauri.conf.json bundle.linux.deb.depends)." - DEP_MISSING=1 - else - echo "Runtime dep OK: $so -> $tok" - fi - fi - done - if [[ "$DEP_MISSING" -ne 0 ]]; then - echo "::error::Undeclared runtime dependency in .deb Depends." - exit 1 - fi - echo "Runtime dependency declarations OK" + test -s apps/desktop/.output/public/index.html + test -n "$(find apps/desktop/dist-electron -type f \( -name '*.dmg' -o -name '*.zip' -o -name '*.exe' -o -name '*.deb' \) -print -quit)" + test -n "$(find apps/desktop/dist-electron -type f -name 'cap-desktop-*' -print -quit)" - name: Upload unsigned Windows installer if: ${{ runner.os == 'Windows' }} - id: upload_unsigned_windows_installer + id: unsigned_windows uses: actions/upload-artifact@v4 with: name: unsigned-windows-installer - path: target/${{ matrix.settings.target }}/release/bundle/nsis/*.exe + path: apps/desktop/dist-electron/*.exe if-no-files-found: error - - name: Submit SignPath signing request + - name: Sign Windows installer with SignPath if: ${{ runner.os == 'Windows' }} - id: submit_signpath_signing_request uses: signpath/github-action-submit-signing-request@v1 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} @@ -478,20 +234,8 @@ jobs: project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }} artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }} - github-artifact-id: ${{ steps.upload_unsigned_windows_installer.outputs.artifact-id }} + github-artifact-id: ${{ steps.unsigned_windows.outputs.artifact-id }} wait-for-completion: true - # SignPath Foundation requires a human to approve each signing - # request, so this step blocks until someone clicks approve. Raising - # wait-for-completion-timeout-in-seconds alone did NOT extend the - # window: while a request sits "WaitingForApproval" the action's - # per-call retry budget (service-unavailable-timeout-in-seconds, - # default 600s) is what expires and fails the job at ~10 min, so both - # must be raised. GitHub-hosted jobs are hard-capped at 6h total and - # the Windows build already consumes part of that, so ~4h is the - # practical ceiling for a blocking wait; the action times out cleanly - # here rather than letting GitHub kill the runner mid-build. For a - # genuinely longer (12h+) window the signing has to be decoupled from - # the build job (submit without waiting, finalise in a later job). wait-for-completion-timeout-in-seconds: 14400 service-unavailable-timeout-in-seconds: 14400 output-artifact-directory: signed-windows-installer @@ -500,76 +244,68 @@ jobs: if: ${{ runner.os == 'Windows' }} shell: pwsh run: | - $signedDir = "signed-windows-installer" - $bundleDir = "target/${{ matrix.settings.target }}/release/bundle/nsis" - - if (-not (Test-Path $signedDir)) { - throw "Signed artifact directory '$signedDir' not found." - } - - $executables = Get-ChildItem -Path $signedDir -Filter *.exe -Recurse - if (-not $executables) { - throw "No signed executables found in '$signedDir'." - } - - Write-Host "Copying signed executables to: $bundleDir" - Copy-Item -Path (Join-Path $signedDir '*.exe') -Destination $bundleDir -Force - - Write-Host "Files in bundle directory after signing:" - Get-ChildItem -Path $bundleDir -Filter *.exe | ForEach-Object { Write-Host " - $($_.Name)" } - - - name: Re-sign Windows installer for Tauri updater + $signed = Get-ChildItem signed-windows-installer -Filter *.exe -Recurse | Select-Object -First 1 + if (-not $signed) { throw "No signed installer returned by SignPath" } + $destination = Get-ChildItem apps/desktop/dist-electron -Filter *.exe | Select-Object -First 1 + Copy-Item $signed.FullName $destination.FullName -Force + + # SignPath mutates the installer after electron-builder has produced the + # updater manifest. Recalculate the full-download metadata and remove the + # now-stale differential blockmap before publishing the signed artifact. + - name: Refresh Windows updater metadata if: ${{ runner.os == 'Windows' }} - shell: bash - working-directory: apps/desktop + shell: pwsh run: | - BUNDLE_DIR="../../target/${{ matrix.settings.target }}/release/bundle/nsis" - for exe in "$BUNDLE_DIR"/*.exe; do - echo "Re-signing $(basename "$exe") for Tauri updater..." - rm -f "${exe}.sig" - pnpm tauri signer sign -k "$TAURI_SIGNING_PRIVATE_KEY" -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$exe" - done - echo "Signature files after re-signing:" - ls -la "$BUNDLE_DIR"/*.sig - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + $dist = Resolve-Path "apps/desktop/dist-electron" + $installer = Get-ChildItem $dist -Filter *.exe | Select-Object -First 1 + if (-not $installer) { throw "No signed installer found in $dist" } + + $sha512 = [Convert]::ToBase64String( + [Security.Cryptography.SHA512]::HashData( + [IO.File]::ReadAllBytes($installer.FullName) + ) + ) + $size = $installer.Length + $manifests = Get-ChildItem $dist -Filter *.yml + if (-not $manifests) { throw "No Electron updater manifest found in $dist" } + + foreach ($manifest in $manifests) { + $contents = Get-Content $manifest.FullName -Raw + $contents = $contents -replace '(?m)^(\s*)sha512:.*$', "`$1sha512: $sha512" + $contents = $contents -replace '(?m)^(\s*)size:.*$', "`$1size: $size" + $contents = $contents -replace '(?m)^\s*blockMapSize:.*\r?\n?', '' + [IO.File]::WriteAllText($manifest.FullName, $contents) + } - - name: Upload assets - if: ${{ runner.os != 'Linux' }} - uses: crabnebula-dev/cloud-release@v0 - with: - working-directory: apps/desktop - command: release upload ${{ env.CN_APPLICATION }} "${{ inputs.version }}" --framework tauri ${{ env.CN_CHANNEL_FLAG }} - api-key: ${{ secrets.CN_API_KEY }} - env: - TAURI_BUNDLE_PATH: ../.. + Get-ChildItem $dist -Filter *.blockmap | Remove-Item -Force - - name: Upload Linux deb asset - if: ${{ runner.os == 'Linux' }} - uses: crabnebula-dev/cloud-release@v0 + - name: Upload release assets + uses: softprops/action-gh-release@v2 with: - working-directory: apps/desktop - command: release upload ${{ env.CN_APPLICATION }} "${{ inputs.version }}" --file "../../target/${{ matrix.settings.target }}/release/bundle/deb/Cap_${{ inputs.version }}_amd64.deb" --signature "../../target/${{ matrix.settings.target }}/release/bundle/deb/Cap_${{ inputs.version }}_amd64.deb.sig" --public-platform deb-x86_64 --update-platform linux-x86_64-deb ${{ env.CN_CHANNEL_FLAG }} - api-key: ${{ secrets.CN_API_KEY }} + tag_name: ${{ needs.draft.outputs.tag_name }} + draft: true + prerelease: ${{ inputs.channel == 'nightly' }} + files: | + apps/desktop/dist-electron/*.dmg + apps/desktop/dist-electron/*.zip + apps/desktop/dist-electron/*.exe + apps/desktop/dist-electron/*.deb + apps/desktop/dist-electron/*.yml - uses: matbour/setup-sentry-cli@8ef22a4ff03bcd1ebbcaa3a36a81482ca8e3872e - - name: Upload debug symbols to Sentry - if: ${{ runner.os == 'macOS' }} - shell: bash - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - run: | - sentry-cli debug-files upload -o ${{ env.SENTRY_ORG }} -p ${{ env.SENTRY_PROJECT }} target/Cap.dSYM - - - name: Upload debug symbols to Sentry - if: ${{ runner.os == 'Windows' }} + - name: Upload Rust debug symbols to Sentry + if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} shell: bash env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: | - sentry-cli debug-files upload -o ${{ env.SENTRY_ORG }} -p ${{ env.SENTRY_PROJECT }} target/${{ matrix.settings.target }}/release/cap_desktop.pdb + if [[ "$RUNNER_OS" == "macOS" ]]; then + dsymutil "target/${{ matrix.settings.target }}/release/cap-desktop" -o target/cap-desktop.dSYM + sentry-cli debug-files upload -o "$SENTRY_ORG" -p "$SENTRY_PROJECT" target/cap-desktop.dSYM + else + sentry-cli debug-files upload -o "$SENTRY_ORG" -p "$SENTRY_PROJECT" "target/${{ matrix.settings.target }}/release/cap_desktop.pdb" + fi done: needs: [draft, build] @@ -577,41 +313,35 @@ jobs: permissions: contents: write id-token: write - env: - CN_CHANNEL_FLAG: ${{ inputs.channel == 'nightly' && '--channel nightly' || '' }} steps: - - name: Publish CN release + - name: Publish nightly release if: ${{ inputs.auto_publish }} - uses: crabnebula-dev/cloud-release@v0 + uses: actions/github-script@v7 with: - command: release publish ${{ env.CN_APPLICATION }} "${{ inputs.version }}" ${{ env.CN_CHANNEL_FLAG }} - api-key: ${{ secrets.CN_API_KEY }} - - - name: Send Discord notification + script: | + const release = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag: "${{ needs.draft.outputs.tag_name }}" }); + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.data.id, + draft: false, + prerelease: "${{ inputs.channel }}" === "nightly", + }); + + - name: Notify Discord that the stable draft is ready if: ${{ inputs.channel == 'stable' && inputs.interactionId != '' }} uses: actions/github-script@v7 with: script: | - async function main() { - const token = await core.getIDToken("cap-discord-bot"); - const cnReleaseId = JSON.parse(`${{ needs.draft.outputs.cn_release_stdout }}`).id; - - const resp = await fetch("https://cap-discord-bot.brendonovich.workers.dev/github-workflow", { - method: "POST", - body: JSON.stringify({ - type: "release-done", - interactionId: "${{ inputs.interactionId }}", - version: "${{ inputs.version }}", - releaseUrl: "${{ needs.draft.outputs.gh_release_url }}", - cnReleaseId - }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - } - }); - - if(resp.status !== 200) throw new Error(await resp.text()); - } - - main(); + const token = await core.getIDToken("cap-discord-bot"); + const response = await fetch("https://cap-discord-bot.brendonovich.workers.dev/github-workflow", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ + type: "release-done", + interactionId: "${{ inputs.interactionId }}", + version: "${{ inputs.version }}", + releaseUrl: "${{ needs.draft.outputs.release_url }}", + }), + }); + if (!response.ok) throw new Error(await response.text()); diff --git a/.github/workflows/promote-nightly.yml b/.github/workflows/promote-nightly.yml index 99e1674eecb..118138180b7 100644 --- a/.github/workflows/promote-nightly.yml +++ b/.github/workflows/promote-nightly.yml @@ -7,7 +7,7 @@ name: "promote-nightly" # GitHub release, draft CrabNebula release on the default channel, published # manually as usual. # -# After promoting, bump apps/desktop/src-tauri/Cargo.toml on main past the +# After promoting, bump apps/desktop/package.json on main past the # promoted version so future nightlies and stable releases version correctly. on: diff --git a/.github/workflows/publish-nightly.yml b/.github/workflows/publish-nightly.yml index e877af3d027..1e627e59e16 100644 --- a/.github/workflows/publish-nightly.yml +++ b/.github/workflows/publish-nightly.yml @@ -55,11 +55,9 @@ jobs: - name: Read base version if: ${{ steps.guard.outputs.skip != 'true' }} - uses: SebRollen/toml-action@v1.0.2 id: read_version - with: - file: apps/desktop/src-tauri/Cargo.toml - field: "package.version" + shell: bash + run: echo "value=$(node -p "require('./apps/desktop/package.json').version")" >> "$GITHUB_OUTPUT" - name: Compute nightly version if: ${{ steps.guard.outputs.skip != 'true' }} @@ -67,7 +65,7 @@ jobs: shell: bash run: | BASE="${{ steps.read_version.outputs.value }}" - # If Cargo.toml still holds an already-tagged version, nightlies + # If package.json still holds an already-tagged version, nightlies # belong to the next patch release. if git rev-parse -q --verify "refs/tags/cap-v$BASE" >/dev/null; then BASE=$(echo "$BASE" | awk -F. '{printf "%d.%d.%d", $1, $2, $3 + 1}') @@ -90,20 +88,3 @@ jobs: sha: ${{ github.sha }} auto_publish: true secrets: inherit - - tag: - needs: [prepare, release] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Tag published nightly - uses: actions/github-script@v7 - with: - script: | - await github.rest.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "refs/tags/cap-nightly-v${{ needs.prepare.outputs.version }}", - sha: context.sha, - }); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 77054493473..7cfb182d164 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,6 @@ name: "publish" -# Stable release entrypoint. Reads the version from Cargo.toml (bump it before +# Stable release entrypoint. Reads the version from package.json (bump it before # dispatching) and hands off to the reusable desktop-release workflow. The # CrabNebula release stays a draft; publishing remains the manual step it has # always been (Discord bot / CN dashboard). @@ -22,11 +22,9 @@ jobs: - uses: actions/checkout@v4 - name: Read version number - uses: SebRollen/toml-action@v1.0.2 id: read_version - with: - file: apps/desktop/src-tauri/Cargo.toml - field: "package.version" + shell: bash + run: echo "value=$(node -p "require('./apps/desktop/package.json').version")" >> "$GITHUB_OUTPUT" release: needs: version From 1dec47e64cb02368858a62de99c6bad5118f53f4 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:37:03 +0100 Subject: [PATCH 04/31] docs: add Electron and Tauri benchmark --- .../assets/electron-vs-tauri-performance.png | Bin 0 -> 201697 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/assets/electron-vs-tauri-performance.png diff --git a/.github/assets/electron-vs-tauri-performance.png b/.github/assets/electron-vs-tauri-performance.png new file mode 100644 index 0000000000000000000000000000000000000000..5341c82f7c4ce5da8ccd62631eee8c4feea9498d GIT binary patch literal 201697 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%0AIuGqenW2mS?NLiX`!Shhg2lB- zb(VQVdA$Uf&jL4U7hKps%uv6`ZDN-c}Dz6%tHZ&_5uY1{?@Pr&h`mCFKcZaNU zYD%a83gb7feAY=RzCt_BwY*K^PC}+`)2!cE)F8$}E{*Tl(N?q#&V}2K(TReWBiPHp zM=V5P%QjDWlbF`-ALJ+gi-`m$P#YMkhCT;&hZ?zpA?|{IpZftt)d_i_EQ<~O7D&`9 zW#t?H{6i12Is)RIfpzsjTOFVo`0m(323X&gO-cJ)?7k;=VO#x#!n9V?Cm$MCEIe)Z z;uXCsIv8&|02Kjfu-$Uy~^4BawfW8r@3K(1Te<%*`Kx z633>Rf7WRQjNO)M=eOVETgv8Y(r>kz&))LI#R*+@epCvR$SR0Zu~XF@#A$kZ`JaXM z>RP>-0^1|c)GTi#3uc0N!ng*44>);2mOuRE;rYC@K4LOhId3Z`zR5RYs=6n`1rLff z3L%{2=d6W6S5R1h`BH*4I;eOc@sbN^oX^*4LZ=aaGR8Z_kbKlPTjoZHV09dY?`V)e z$|a7NRvtL7Mqy=apA3GNJG1ANkW*?B;fOL+3D3z=_>`&BQ&0New3932Yw~erG58=w zO2K*E(}xnXleB)Ydar{|*&JB?2ClZ|T*j3TE`R*q3d#QOoBlC6*ZM z_M|m9v+C$N5|J`r<4}zTgKP+2-+c(USXlcimddC|5dd<_oQ^yG>GIb#`;#9rg(_cZ znwuT=Ct96q3&OL+p$I*ad*4}(Tu4RJdB|fH7Q`)JqwqQ!!K=psqBW4awzy+v(G;@! z9U5WIs=zz8?#<$Ndwf|=bNk_;pMA#(OEQr^9r@G)fb zkR5q`ES}mijJm9?%69}4$HNJmycM1u{S<*ar9e?fxy#4*a-NCDux9em-y`?TAI*$v3{t@wo2~Ts`${zxPi%U6Qe)CmVFv z39>WNDe1Tf+q749DfFCYc&CPC_$GAvCp^GMk{M(K&M067n+5+Cc~_`fM0xPzY~WUe zW;Vb{f9O_OERA?mvI*=c8xGg=&e(76q&S?aot*GrD(91$T|q17q(|^to5aqI!5u2#81J07b)<5|oTOk*M1D&4t6{8ud!oUak z{NDct=yCP`{@do_lkO-s<9^XUY7>R)=War_fjH7I!;4kn$@~s@@p4wXS!>a`o73C( z>p(tkh(4WTQWH{fwQTf1VPjsC*Wk&_ICYF;* zT*X~S84^beRL0usn1jw%Z(e=7o@rsVnv;kl(r|?}&WTT z9T--3eiiS)(`dCyN35e7{lfheZRZ`Q+i{9&R6K(w(5Xo6n7dP6D7wX7hx<0gJ?&`n zladY9Cdy!sl2|2LXX1XJn-;;@qyieqH7=IsyQt07JL$On4kT>aVs`Qv<10KW_{zhtJXIFk2n1Ms$I@jX zb*OzdN3-cxBKN(=eI_+8t^LjfA<}q1Uab&dzGWH@Ui{o$X(2hW_&+zblvY_v9IK)#pvb1R`G*n=}>2IKxjg81+h_du>l5|>8BtE3Hm zUKibmv@~n>*q*<5;Ks<>h!XG(MQHWhY>$24KN2rqYxmtsnOepxO?t!!YA$|XBOG6q z9SD`vJ9@|QtiNGzaQ#^l3wIv-GN^GOw~s`qnfI5-Q-Y906`Kq2kOv=Dy2!0)xeKZVu2ycEiC|u4jwyyH`!7Zhw6;MdNDu3W2f$6AY{#+`NsQsPKt=G8pur zUQi-p=j|4&;z{}kF_lhrz1;YI3G!@z^a(Q!-s4Jiwyhdpg@sVN!j`ko70G)!z-Uf&e(5Oog0E+3Q5EX~esMUy0oD zO_?*mOX6}`ROFMFdVzr%!!|eHJL(;n^fZ`>*~9*bBj9A&fTCL2N$$tcRu1^UdN~fM z`N4T2U0s#N-o=Ak3t@zwU7GK_7;0u*mCJ18)=M>tLq*1AkLdxt#2=3Lh1bqZD23UD zFqpWU7f=>d@3fDpIe~ zw0`)U=1yeaKs-&LtZY{qzHW||yDb$b#S_etr{J`$VHkINixVQJn^SLq#sfi!YW-$! zqu@KVLdhH5gC>BmHT5fW4o6qFSKX|uFbq6~m<_csIk+^e?+!=liSx;#vm43PIih6$ z>C-X4j#`J_r*;s&j^LdV5J7bFBQqs6_7Q}9!G**Z*TFR zQbElWkh+NL7=c-z3YYd&`C`0Sa}(=wjq-s3_tbvFl8|YHad}brtwQI4Gyz0>Prcnn z?q8knDg+WV#U5r+GOJ7Umi5nERHc7uR0(zIWZGmy+~B6LDcK{tW=QE z-{?WEHr(iFKMZX3$+?ClyuBancl`oGc=yHkKv6gifV3{YkuVrcsYB-C?~Yix!uf?~ zVu8`WKWT8#KbX7qQ|w<_Rmb3#ry5luQ-RBUTYxgu6BLtde}w(4!1lkv@IUds->dM9 z8gEg{%rElBRSg7m4^Z%9jxul_1afFsV=+1`^FH7Do@gX;QSl{d$?}aI%7tu-Z~-}E zCVcnUuO2D|<9bcT`GBaK5J>588gDC|o`Ia~W^1v{&m58@J@bS2Eh5gytcS$ZeiLi* zw4(p{IjPtss}aZb<-oKN;Kk2Oz)TKdzVioczcN)5h18@U@|TTd**V?WMR65)&^s`=9y7gSw!V zCTjiyE`2%A`^A@A zU7txL!eX~e{uY>qF0{G9?5jUJXsYIFv&j3tO39MCs)03)INm_AOP?4YT#DfK*_!Rx z6+DL&e`=-}VCGAQ)9dZG24b_Wjgd-AR$mJeDwogLUId(+%lJ^fX|xw-l__6bSZk;= zAlf?q-8)e_Dzw90z9P6*&Ct6kZjKBoU3iuEJlL!un3Jhl<)ql>&q;Av)%Iv^{O;w8 z;Ff2Xt_D^wA@7u<&D)h|E-nbf=ox}%g!jSZr-}^bCK6bA*O3w-3AOCpZjY;EhEH3- zsB&vs${v+g&M0FTMfyHY1S$ZC+<+i24#-L#R1h5<%;^7ML9Q z53xMw@tBt}FlpWnC1}*XD*0(Um)&0AwoAV50-l|cqhAVRi>QtDUgRpkLhvzLj`@CF zck~;R%T46I2K((xkdc9pN%q~DmFA>M<=;7%o}1-QF4gf2u$%ujBZme_afvnobu=uz`v=IW@YRYk&BD8ndNxu_*1ue|IU<_m3KTbi!3ZDxb(_B47;K1O zBFc+0KbV$4NRyl@^9okfH3dcwgozVfNje~f4wnD z#Q9T5P!jWB&0gtMF7}2tElk55kg1D3xGPC%bUndLs7np{qUUnp?jx1<_WptsHphlT zua8jlCQH(!ff$P~?iuUUMW4{kspiN%Yb6JZVY3En`Q6_wf0uLr#rb*A55L^eIjTVZ zB^?vf)a0DLl_|lwQ>c?IK3q4l`~}F)`yL(wx3b*@!bE;&sOBM+l^>U8^6PO9!eL}y zNHG4SlK*)~$n_&0U43c()Uj` z@J}&ZkC~5qZ|(()8pFK(#Mn~^qaI1ISJ0jp-NKe-A3biC=BZsfO8Sq*WOx-at-m%w zYTA3v-F>9f;SkFNPT~^DIWwxVhYJeUe(zceY*s?e_3V#A7o%R>t!KHgtm`fwyf&aN zWU}s0+`X%#5W_O&fBq$J$nhH(a_q73$%cBLXtvb2*P7k_U?M_)Of!R{xusC69lbthO9ydxhZ@L1P1#Cq49mv1%T1=*;6Ua7+7LoYT z*hOjKXZ|&wyU4UAsLZg}-|@OA>k^&d{99!IXhi!NTx=3BzP&$kiq8jC zF36w?GIin#A?K6I>ZAXe&tr@$ucTJ_<;qmN(XwKO?7#LuqT7-OmaRR?;rE>-igmPyO>#; zllzvg2i9oBew1&F!NLcgz5?|bw|K9WSPNVHnqFy8N$8vS3e2a@fCT=eQ%ccm>++Hr!L(`diCpYRb(6Jn%Ta z{_|JLtD>&qwfdp(WWL_3qpZMlz*M*7qB^;|m_Tn1Xt}t`w6#h~w$o0%_88HTHt5nP zhBz(GG3y!O1ZAg@O!;=LCOshk`-q0dlQ8Fl8Adb9{yL@cv_F$XEb&8(<72R1YeTs* zNyGr(VJ2v-B#=!l9}sgKMg>fGzv9VTZ6ii=TZoSYZ5}=(oEADP)N)_BPBl(o>NjlA zL5vLEFk%#VPu2%~8POIE0SmYsZG?RpxaG-F9Dyl+BboUwfQDPUY`+KLk?jL0Jmzhl z$NHVBGo8zQ+g9*DVU|i)&mk9KkqLUlTMKLw?p0AVQIEf<(x1C$SmJTw!Dyh`)~ePw zt5!6v4MennUjtnO+$WzGh7OLp^)A|%dPWV8rQjACpcgPU(UUYj_`R=mA$8~=g7K7C zLKmefh3W)ec-=l{N&*Zhu&y)r1Py37aqy(BA|T<} z-2MGIKTA9lZQuZTO$a*g2KP$nltWCJ{;u#E+D_5h2?w^Lo-0Sv1vyAT9fi1~36350 znJRPRXvB{_l(|2}d*JMGC27%)MLHLBeLdmRuzT_H6JxVgslhsD4NY|x&D=jl zeAN&i5}McW)6@f>kS{2A+A>^n)XSeLRA9mnEY{1J1e6`Wcv+L;{+xo5N?6L2wK5fD z+-R1S1TU~`6ga+> zC1JZ)0^J0E-C@a3^}97wqE(+uZhM)X7amOlY=UL5rNkCl^&m*1O<%|G`KtiVR*rr9 zuA)NJ`PwQoG3lvd0KUY2wBbn4Cs{BunDEQl%Xon5_|M*V5|H>y9EW#&vFF4cMxh7N z|C@wxTK@39DjgFOR>K9$Vrti*j7e!_eVDkli$Q?1>pN0_Obw$daj zvJYJ3bMn=PKQU|k!-7_Vc!<`i_}$f$x&@P#Yc6ydtz@*p&G+uU~t1G`Kmz-vQE{Dv*bOzp6Vbs?GnttaWvJZvRC zCnXfc!p_)ZRz7UV!@P4OoIcIHNi1USJW2nC)pNUP)qM{d`}+6y5Z3xnVf-e6E{6jg zsvLS24mwe3*DvcvVi>K2ty*mF=jgcn2+Sc-;9KF{A8~Nyc4jV2)%D?l&^#`0N+$H$ z!<0k?%LMuP_SeMF<2-$s+@wr7vPxSQ!QU*!PcI$4KLugdaMI5=_c0hwFo1mgzdZsJ zGr4U!>d|F($0T%;7L&~cL|bKn z+(5z1ez|Gm=3*G{$><57E8d8>=0iNC&!ewic<%la&Yt{X;K5nrsmCIW{BBK)ezA6= z@}2|!s92AA`1L=DgJphFp<5Baww87%OWq7`j}B=X&2D#{9x>;rq#mG4i3QCR10jkj z-V&YF*&-#}^6O?#{{i|p?e1ykoX{%Q9wqZ@5s!2D+}nyM0^!g@cw`GiZUKH&D9J18 zaeoq*uHYT7di_a-tD=C+$yzzVd7k-FSSgPkkKh}DC(ebWIBDr3?er(_miJnFlv68A z_aM+K`=*Ps65Or8fbHFsx+W*8r93F{ocWG@n!nMd6Y*R%Zb|6thlb-cKC>FPdRcS- z%Z2R}&GM{!b)AA1geKT}O#hm$);JfC(hwCkqd;hy2}Pi+y`9fiK%qn9O{LZ3J&{N2 zgX(-XbeNIqS2q2iy1997u|#=8_Oqcx5?#>IUiy1-c2MC|CDK3PnLh4~2~E|OvdrGs z_q@V8EPI}8+SD&aZqv$ZW$ zzwfs;D}7$7ft3y7n(vjcyDhV@byTp+Mx!)fqJ7%4@$@OMbu6<>$QGmp;f-6904tQ% zNZH7yaplrevVCY$8n5=ZODfn{x5^C+&gV(`WP3#YXVV*( zXY_7?K3I`eew%ksesg9+?Ct{sK9;#DLgl9Y(f+EWkUg)6Zu5tuI!z(&_4#_QdXJ)< z^rRub{$ChLYdMz-m3gf)>%GdV8(gEq-=3m8q7>f58?+eRtyct={OFM2K*rM7Xu-Vi znnLZ2C*TR5*aqpXY2?Jm>I+P+(|!e?Kq=^bq?nOAG{*RYc=|;jX6Ah^YyEq(v zIYA8Q=uLzz2SWZ2d+!<6RNJkKV&%1g6%;9o^eWP&SwV_KdIymvy@LUgsEDXE=~AQ8 z3B5`Q0qF_7*HA;Rp@$@#$M^ldz5eWToxb+E&b8(rMF`H!%sHNC+~Y0=JcthGnorDD zEpGx1^57vMEwfSD>RBOKX&>lx_!P~0vB9Y!==+j7C=Q#Z%cHFd7FXGSye02E5&4z2 zsa)06#I;d$5oX(TWsQDW@AJVeZjr+Iw^^jbRaIdw8J#9@s+aiL&;C0o0cn#M zM}9_!roX-$rbTF6FpHF|b_Ytt6WG$gYO5K-!@bbw99Kj-%-DZE#gK1TWY$bsES-BK z#4f+u^XP(DPX0&Bx~2>Im2aqFOM3nIhlzW5#J5@h?8uWzC#yuD=7YQ8=#&<9o_9rK zr=>gx`eZB%A5|Fny4E!!#>UdW@CZO*;QV?JHn@!SF*#UL2DQKUPjdd&X zjk~}06{h1=JbEdG8%OX790>oB%ACG!Vu-efuFj7e8iYROpQvH0S>bW^jR zD1xKouf6KO%N_R#9&rW4KF7tuT1Ty=<{k`_SoL zD_ZQpMop0Lz$T|@zw;M?F|#qcfn=qks0c55$e|jMdz%&taIEqq`E~rgIcGr=!{+=$ z&wFJd(e3=0b48Na*hjECvnY;H>DH?PV`9{BxG|jFL!fLh#l9pgS`H z%JDjfop}Fl?aJV-OQZ723+W&4N6D##Dxf4+qGa?zT6@M>1;0@(Pv+ZoYsEI{DO%%9 z1VX8uH1S#_hX0U<0q0tZx@JNHXuOy`n?s31=Q9H%W7Uvmb zH0XAsRSFwo8;15U(S48<)57?}tWzSuqU*st*mD&wEz}1ypP=1Y&D=dZe4O+{lZq<) z*mlPkUgDo{SY5w)tQK*s&MGP*<5PMSyD|qq;zi-(Dv4!XVWSEMIKEn!ulQM1h570i zAet#{i=HJ!HXYTxsZ*P|C1^Ml4HbL5IntHlYx7y9--Tb%(NIzW>VOy@an@N?ixG%p z@GUco9-Av}JFmBPZo?Q~NR;KP@1!H3sHaT!c~C^UTa3-d6VQTW@A`OIlRR4x6sN`G0= zS--gY=^u(NID2xXjw&u%(D9J)H0bI*YInSNc3k_5>{%mogCs+70Y>7}2|Q8RK!Pu} z)1&)+9CBw#;?>a0)&U*N&&qEOY8579dt1pxzWR=5@M|-=fxD}j8ggsAkm)Dss+Vv~ z?+L1y{*tm+H@0HR_|Dv;L-TP*xiMwjzY$D+{2Az=4AWJs=sBnC^bh^Doe`U*8;V9| z@ZCaV7pDCF{(C?cq1JOuP~Fn%i7lh0>ik^!j?vw|EBkdy8KyZ#nS0hD#j*}?{MU8WuV|zGIMf1P|QqTzk0wtDu%if z>qk(z#;3uNPm1a^tMronsZ?gzk2Tb_2+!>oGF+A_Jo zcql})078qakD~TXQcbiMvLb_wT0dKgl4ya&7*%$*xDc~_sepd|38nSJn~HyxjL=`rs6)m(1`fd&?%$kvGRnbqxQ2qvVsyhiiLNbEuNx%g z&d#=*ALN9v$j{B5{^%;eB1$~R&el*X&yuUV8H@A>9y8nd8CGbYJ9X$;!6Ka^I%tv^DpTmb(yj$9fPX%{LP3S_#S~l1{;2UnqA?#qg2B>L>GYL z^Gz^Q>uehvQ}w|&02C0hXkP@|TSG5PI+18kwtSAs$Ou##5ODjQM!C69A|33L zPMtoTt`dFEJ-w~Mjc|8VrHJ#fY4u&zJ?c|1ZSVpD6f*^kO((L-x{U{FRas6O2m|hI z^?_l`EG*M6==J3HfMNXM!C_TJ#kCgX9`dNm4DI8&6%!T~wvP7QOG@nVZ3#_oB)2J- zz4c3Pjcn#TLc2RT!wxj9@FUYS&O^hBK5Q@czVWS8@>QZBtoEogTd^N61aW{j z=uN*Yr6W-kX7)7)<6xd~e(~P@jPjA6c0FwbAFZN~o_zf|m%ZpG2{=8#{=nC4)$%@$ z5AL|}Dw*BDhe8TLd|&kU@=y!cP(SKzg4rHDFS>%S@IKm{nFj?hV%27#!KiFw>+8je zw`!I)EU8cU=gj)OxA$AMj%urx?sCR9=n8+7bXNGT=o9Yzm7uc#W zma|~gc;T<=J3!!i*4o1ryAUB?*bY~qPSH~;{21hqd9TTT&Amz@mHH6~RmgGvWrZ@W zR^i4*e#x~7`hi2fXvc(Z&c|graTY|w;qcjuEZ0r_1xhVcGPW1n?Aj=*rPC~5SZXO_}5v0}^zN&aVt+3h=^0%2bdsxGD?4dWo#zfw(H6BnC z;-9vedtRQXE{uPQ4W9x6VK*t2WLewvV09wp%M{(#Mz;m#?ROb(&iu&7X2-$6nSc_k z@XJ}FGp$-=ye|LK`}fH+#>PE3XgYIK>Cq)=SrY9Sa1%1a$CE=_(p%!j?>tTH*gaNz z86*hQjuGtTF(C>30U8r~2M8Cu?G59muU0N+$h68{y995G?Q}$I1lJ-^8D&TCW`u)| z&8%$6nPSko1K%&p0QnF!4OdRjAEWsu<1?K|mIQR1F+tkzlHW`!?SdFYIGm;%KKG=0 zy`ho=hQC>`7&aCvodd6_<$JG6^R~ibieecpcnvm4rJXJ^TYu_!w z>hhw2=XNMO2M7>+`{p4?J$Qoy;s=oT9CQ8WiyG#UqjRBjygSL`MZc;VmI8#-(}kuI zMahVT>vV4z!JLmTQDHv*dDU#w$HJ5!fJyY_YMV~JVgOM1+~55RoW#Y)+pcH8uh#$T z4AgaxS`V76jOQ~glbqmtZxDa|I~idp=>rq^mp{kQ4RG539A1Hw|F-HM{~!MQFTMZc zRV@GcKk7z#vG{K+fQIIO@yY!E?F5u_>VB{K*8S^N*nqgIuV+p<_We70;idQGYM*P_ z;8rC|_EF-h%4jYk2X+y;$7nX-E^BoE+%&ao7mbhq^Vb5Ild=E0l~(BfiShtnRy_uH z`QOL?&X20U*UNt-4sP0`kB5+7_UC9ec}=nh!AY&t&aeI7_Pf$o2P$nu(wB@rf8p?jVb@@9f`LdzTaLH%?$Kj z$9JDozRazh#1(Ry=55{bV7@rb`jx-$O{zkb+rg^?KnRdOS`BpG{tfgi?!MB?g!t0s zPLp3MAmgl7;nUw|QeY2g3ivXs6jLFsvY);&IZQNU9042cyE5czeFc(qU)r{Z18tmd zN--?8`_qVM^R_1gEsov(#`;bXNCG;2tLD<*Pl?wmsgz%?vZNn(0&@euhjSLA*^kA= zi`$nwjkj^-8I|jvinYSUlqS~UpSei8Og^|!>-+cf^Y-X01d`8+gqd4>d~)WBEqJa# z#1u*gMZFIrZKtWm9zT9uPEO8kt}R*^tcv3HoGO-Z4h3-ihKp^|Zb8JI#-t32EUiTa zjDOt~6VpD6$K%1PbbBS8#$4t*Oa!YX2()Z|w*qQwYXN~@4#BFKrO7ENDG80P)Qq088}Yo!^*p|> z36%`MSUW<5Xa;1g+EsA&z+HHqIe8c~C^-?rM@EdJASxNUzflL&Tm4avMv_*oOIfij zz#1;Q_kpnUfp0x}L!@hbtSCy5O;?bIhsWgzZ*u!~Cr&Rjk+(R(wJ<}Sd9(l?O|~_g zFk0-fYFU~xDAKN25?|(0cYu3dWMZoHTRe_7PJR7(McVDu{-L0OnBIUa4^M4lqd27I zxS(Na=eKU16K@d>Muv_&b0{)=5;rFa~QG(seoZX09VJQ!F*`Vv|4aPUN>USB>Gh3v z7vcOBQa#&81Z-|wc?QQB6opv&0qJr_x6dquPp}QK>C!)=zRuRBGoxR~Rk&KO7`%i> z<(t;PSC=y6MsCJlG*8XhYU#eRJmBn*NFowc`_Xd{OsJgK_E(?RMNbalEw#Y%hLPXN z?2a|fK4P)q^A+nXt3{x?*UhQ6KrntKipGr8t%IOje*yY9Z~nYrrar;l>BP{>#=~djECRd#BrPs!_}t3VvhJG zoJ=!#yTG9#3liSAyM%Wl#5*$4R7(Q(6?o`le5Am0&dA8PbJwWMVFID>6L#oDst0^# zoosD`AO({uFBVCs>eG|Rf7Q&h2s0$hD78$)zu~>K+uXp%iExYy3L> zY;*_IL1)_CI63zs>ISMtPQp?b5+r z*NTzMweQMo&79!2of7A?*W*u7=+ApRSYrmivdUCO{iT)BAtxt`)=XDP3rxUJ@2RnC zly>ym2f0-V$I=OWTnp6(jBZcd*PG~HPlRz0y1cIQtm zsYNsn2;K}SJtye7-tRs1NyE^CK{$9td3D~bIjc`IRV|JfXhxMPK^*6^3Bf*=Iqkp-;v{tnXx3U#n zu8*%C_Mk(S^_Hz{Y+}>fN58dy6)-3&Tb0+Kmvq!mVWb+?JbS*f)i@ZFTdiO>g01V; zh>7F(#w&%f+PU^uEN572aeUGaY1kqN>*p%66yKO#hwnlWJ1<^o;To~BG4&U8@(i`2 zJCkH`kJTRTff6R|K3C#6%H=+%W>EBNFwexd_ANDtL6Eq)sj*cJrx*t5@oJ`a(U4~{ z|8yiY4e#^U1!wAS^m6HUxGzk~>(sbXvQ4Gjx!n9VR6d>!Q&AjL8}wtfT^B$FLQ8Lz z(i!UacQl>3%u(Q{({ERbxb9^40ltIf#NBY4E$I1#Z*0_wM#@5Sj@A0^4w~`%?yWht z@f;&7DE_k<`0_C%0VcU^b5m3*T|M{dyw{7DmIq!hj^J{Roov?^gpYuBMZ=R7)Kk2D zI}0v=_SC7&qAFiqRK8pLN;=khDyM@p1x0AkrlSLkz1CcN!eHKba}%$SkPv9^c`m0H z1P0o?JAU9OJk_2c$@CUM|F4BnO6tD#&PE@;y*C5$O8f}z>rKFPCd=z+X=W&+D8<2^ zw^^8T1B*jHF4om;zSz7%Gd&o@DD1QIrCP7A(4tuwL*`QoWwyft1C*a8pWtc|XV0GP zp&9yT-Mz)oW8|M(vglSH%K zzFg(4+F7|0smH1I?QU^G6*50dtE`shC^&C!>Lt+WZrnbhndau=;-VHS1WWKrDt+^c zY^0a+>3Im{B{tiR=B16(cx4urm6g>xi;yJaP1;$u-w7}^4VOFWh!d@{Xbx?%!5qOq zoUSz$$(91Q$HjAN21WN}sul?KfxEQ(hXx(R&Yc~$T1{XX^#&%ql6hvm+%f!S%I$ZZ zJH&uY_kMwQ&%Ow7fPxQAQqRqI_T_vYm8t**Q;QIA&RWiIqt-2XSd<&(Ryds|PE#N{ zTm;P`Cd>JjIUE?VTn5X4PT{LlI&50Zay|`%gOXrNbnd2+Kt{ZfJKaqQZKtcs!xIx2 z$59E83h*BnhY6HfO#+Z(!TNGaeRyuC47WvSm)V&**D|7gJZwkPpZagjt1tK7gbf+D zZ_Te`j4@vQD-bey=H*U-sehQZv_TJ`*G;7}_)5DIT4OQaP2tSUvUamB#Z$by>!Xyx zFOA^{^pp}4%}|_ig`4#K``Llw_5&W?-uznW9&>GRe0q5vo6|=9tuQ}7S=pjwDc46> zu$6nBAfW@fa8p76=+VJK+#b;gt9yTniAF|I%N1N*vL3U7&q6F);81dgV-@6HKv;f~ zlnqYAg*zpdUneUZRFHG?_4lO~Lcs^=h(M{zRphU0N)6Fh!8(?svWQe2%X3 z=Qk!wTtgai>^e?|#9QO$a*rwDDo6i~1-wlPuO|ZIAb>39rk@PZqQ_8nM{^A1#^Pu^KZv!Vb!jTuFDKPSIbU?gU1dcsWh9 zEur9SGVQ%x?Drm@=C(NcW=6knEJwJqEerPAhUfi;QVH47ssnkZV1WP|IC61fdtLWt zhFP;6%?9SHEYxjOR6F0)0v97V0oxk~cV}=fy1M!_9dqB0$AKU(P&Enwu3sJp0xvlr zB$D@BDrtLpo`;~5YgozzHqUt`RXNlO?JQ=ogf`Of$&2g%KsC$qi@R}!d_OhHlVQmU zh~4@PXNPJ({Lg-{po^>XzQ}AT!cd(C(>E<1=dy{S56{2uFH0G2FjIm!^yeh+IyFI( z525+${4s*TcBett0U~X)S!It+vgxL&^n+gAv2&}AHX5`R(B*;L1W9Mo;i0sY)UPU= zR`A8|59TIQt%$=wU$B#Z_Uw}^%5-6Etp;pXKZ}Fa<8Zb$@^?UxWk;d`!VY{yx<><* zp34sTYQVqor-?bu`uo4*J2#HN1#B(-wbYe$xS)`piN75J*21p&pnSc&VO_Mf3C?EJ zd&O(Np`WQV=Sc`i@fmV2avy{UBEevyXCO%lbAy;d&?4#$IE`olDP*Q*<7?EZ&r!Tu z>hThY-%%B(&M}JnsxXWqy1Lp&>E~PGMx~OlPiR)OVgwAZ(fos{%DteM1Bq8=5Wx@* zO|kTtFCZE43DR?^PY2L@ePiAxfCh0AGef`7B*E_oaTYwvlx!>kL9la?_qKO{b~GX zUIC7Qmo~ss1_YP)Eop(pMLAqHZZ$UWL-iY0;-j;4bi7wM$r=8OpyVQDVO#;AbaJZ3 zKPgsN_dWd6qW1JBP>*@9k5#de_p`J}B{xj&S$e1=c~i6^I5gnntnxk%o<2XHoB;#j zJ_n!8X+rV*ZOyN+J6Y|HR3ZVD(U}}5ED`=V`zpX%KgBiI)s2=r)<9a#8K zvBZUodWZky{2BkJ`v4GKh*u$Kv7SZqMKf3cL}<9&dmf-E&g0cE){va+&razU{?aY82d1=ZKUHpFpMmknw3j(TjdCj&P} zND@`*coc9>NgR{&Hb1{JU~4{-6Os&&K^q&Jo9F1yoH;{B7xM_LEj_8qDaSrtd9PaK zP9WuqTw!>!r0%pIULIA2+U<<5vK+6wW0Trd zpq>`5qhh~1PjN&7VO{yUJaKRLgpY^4;;S*P3#e#81D=P+`SV$%9CAW9!N6rVf>r~>?`J$2JU z0LiCQIrcU`Fg9O*n3#n`>^qT>DJ2UY%fmMN^cY)_Td|aLa~)sBD_}A$D_Na~7f2<0 zdRe(9m-EdI^}_NT*#gZZmKDij=~}U-Zs&U4<|dWGu(2KU^UHS#xiVo4JlaoAyxqS` zDmM!}PZiP?miC&~?ntZv=%ap(!@}3PSRoU!gtdhQsf}glO0QK`l#I}213IQ5#9HET z3`8f(a23!>3xb6K&H)g_4A7Ql@$M{1UKgDUQ;QQd5_-bkA(3m8XH=z#HG_-4Z0Ry* zdMouKYai3LJm~B+QGr-mNkbx#;_wkoT)Yoxll!RNj~-ED5prDk-d*U%-Tgze3}7l; z)!(OiE=+u0&_H2!URgsYuLQlJs^_GR+>1@k1M}55xx?wkbD&~Y(}LAB)37iTynuNK zli^C?132NUgQ(8j1VeH-YT}(4e5Bl)5!a^~d}HTvq+)-_VyY8?_>`^O&QgN1d<=Oc z_S?sAV|5U=(?<-VPJ|mC|D@FU+X#jZDyV;Xl+Qt1q9*v%#bx9BF^Wvdk(8L2m^H3f z9Vu^;u6+iTl{QpKSTC|tVU=~MmD*exF22|O&G3sjO0V@vMWa>4SE&2^LY7r1vovN& z;lYWEdM9I#UIDHbtBh6nlQ<@7#_{)SbQCBNm|FE;waM@RZe3ZLg>Qb4rPFfKRTpHP zmZqf;mJiF+1EDkTf*6${sy>1ft4(2t|wR!*$(-PLw7}4XF&)>~uh6)GqdP znpgo)nRPxIPWi%c0`QA6&62_uE+k3Ae){NFIs`xzj$6T<*TBT1cjww~npAlV;Cjv*2htird0SsZM1#a4KrVWL-aZX4Ql5b==stP=9@%MY6{zU zxtn32NU|PcAKU}Gxi_81i!`vHq?#7nK><#-ouSUE@KdkTfvgdv>v;d!Z41Rgc>try zbL0$khB_votBX(yx81$gdlZGk5~O5ods3Nb8jxTYYJ-hx-Ie85XGV&^;!QhZ+WFnB zC3pIw`C|_eI{`l5?kIJbDN+qf^j*2Rxj}VQ2;PSewA~v)w4Lso#^Y@(Fw?=JKDG`}M%1$68STaYCA`cavc zizGg=5|hyZO?{9PRz7h-_goagV|}D?yS(P0On-Bl>8RU zWw@Tl{XKjI_cG>zjnPsaKeWky?P+LNol?# z5_0108=WF;-UAzpk047>JzmOG&bZPqz~A5B=htiV*}pDE^XVkZAbv1?X&#ruu^~bu zySDZVEl5!ZTXS%df%8qo@>G3bu}!-+{k*V#Ux9|jPJvfaB_fn%qB?Go>uXN`+jsu6 zFMU^+bYOqFjby_wz=AlC0+8z(pBgs6yn)Njy&DkK~Lwg$hg*pNz;)%to zVIKw7^GLAztSRtJ4y6-ei69PdeXVcqIH1MTov)0&_3$__5d9;)ncq~8yGQhp-QHQA z;TEojUm5sW@e;q%8exFhWWD%i50HtkMsTQew8j(8NA#}Z5o6853$492i@j+bD@y|5 zsMT5_E|d)~$7~}|t-{F1*T-Prcr+dj1}~bJBz|FRO^`KM$v)rGBG-bcUMCHbU; zy#$uQu*x>pR6Ryaxl{dl3e%ub*_4fm;dvn3pl);(x|h-R6!w)tP2_%EE%!Td1~fl! z5KhLJbf(YrEF##YxAbh{j^d6)eRR@($0`Cf`Iue}Ai4i&1bQZfi`c_Pt4_s6yz4-U znh5XhZ=dUQFtrx_GlQFyoRG|Bj8Vs)Ws`F&u*@nl)E7;<2>X~=!LGEcMW8uqX$lKs zXfv(y3Al>Y$?OAIC_wyXPSfo#ja9h&KsO_-D%_P=f0<(7a_&v%cU;H~%GM`7sTxbU z_ViV_TU)h!t!r%s3WwtoM^R;7?RGkAWMxJRyDXF*rVcz%U~(XH#>PgOqqJam@*SzT zQ8h~2UBzK(VIec@Cj5vBgMHWp2;g?eYi)Tb)?=yQ#Cqr?aRPhC-qM2e%nEUK`^zq; zGroHGm(>KIegwk=lF=8v)_Gi~pR5}73me`om)~6Mo$sBaro|e;wNa1ae(N&+aydv3 z(sO!NCs!O~YTf%OZi9=Q9)@I(yms^%G2r7?Xjx6_n~Yflu5L}>vfK!mgk^yo)q;RPYi z>+N6VL-2TVdOjv688B$@VQ=5IyoTs#i{0^-MCs-`o)z+TSUe<1k~|O!X5x^Ma^V7t z#kffyBIXwK-`8u%WQkVOs7XI4<35uvz8cw$hH{Y2&aL#EhU2118WEsRt z_TmtsNL$YmSDH8ZF~UaoJ+YSPvGt(*r2&+)pVvBGF594dHkg$)IYyP|#rN7y3^}{8 zEpfP5y8vseEDhd#G+nsZLa zf?f53=qID zZQNo>Ggo`j&7jCfKZ)CQy5YG|3Cqzu5AQm@u+$q_U3!Q7jrD%(JRv+k*yq0LV z!|?ABX)NcSjhxJaR+D<-wSMBUNy_Hl)^ax*W!e#bXTJ8~oWH-INs)fEQQ6t$7xy?g zW@kTN0Z2BSw5^=p<^*+{=QU3K#2^Z7?GzFlCQSa#T?YZ;JuB=ksige$ zE%`s6)A;tLp4DKmdUpqAzV^(ub1K*vIirF12kxm}&patH69Y-xC%p$e=cMA3puWuO{gFDN5db!5;Y{}o~`SdU@)D<=h_y9835M4(0UP@Kq z9Ef@}P6Av@vObtik7a7y(MfCEyuu9@%HIwjRT=N_XFbavD)YnvGl#hLI~7yT7Sv4r z%@cz62lLFd2=7mw%QZk}J0m*<0~wD}RR@uX195=e*?oBuCvG<|QsU~*AV0D{IN`Uq zzd0j}SwJa_=0MLKtY>Lu)KHqPR;$O~)A59!nPk~CA?f+-@OgCn_VfWTIY*vQx3~8% zbDXIT>-_?wj~=$$Zop#)s3`BR#I|1M#l9+El{YR|)c4JNbl}dZp7<>N8Fb=Dq-S=l zsL^+|zH?AFYt^{lD$S4x4ljq^7p3blF2!n}--G;{-cem){jwi#!mG0{c{}JqPAb5Q z*k7IO1bjNu_UqNlBCO@r**c9s`# zQM=C#%ignJA0}Jf)RuN$Tk!ou9(1*4en1`@)@uA>#HKsFUs#{bOkU7f5;Nz%Th9I- z2byA>0#5JXfaEujQV+V^`yNYq!ybEiyw^RwT3=g~omWqni7&sy_k|0ROa_C<*8r4_ z!rT~YmfDhh4EJ#Fbfb>lfK~dZ%zzoaIL&_Zcs11G_r9x7g~=fuAdeI$HV;Y`ZG~{L ziOa*5q6o6{9Z4vYVp8RW1o{2%5!PBSUVi7xH!Ghj*SM40Az?-2IH+=;7rbj7iwPy= zzI@}ti^zMv9AoEG=3t5hL%?Uxp3SST?(kZ`kF+jKo|qhO2$Ok+Nvx@)?=9-Q`W@L4~Tfnr>L3-HY3!zrv$&ts%Cm<8=9f=YRgZ9*~H*UKqeQ#I+|ZtCUhV|od= zp71O4u{mQovAyZ8D}yP|i+D~C75?GLqQ5qYr2F4#9N7BG2%dCiAUe*zxU>@0vC@8= z=IuTZcvNxt_&^&99ySU2M1et*u+cp;IX?=i)N}*w+3U~&25VkxJt)vd+0n3V>P)b1 z&3AM;XbKJ_C7L+2rN37dtcGWrW||hSnc0{7`Y7U8qP)etrgQMH(|>G?Y$Ua=;XZ54 zNsk&!(NuHaK^z!FWA=&ss4e5ls|GV50Vy;zG$81ztA`&-<0VikXp3%So;>x`>%`BG zCwhL@H#1ASG0hv7d$l(^8sWM%pN9(&8?r#~HS_ZmKd{pne#Flh<&;kjD# z5I~j+WQyHg275huf5jUanXWIR&M)?g*}ixl6l&jeUI9L+s#yjs#jQGH`rSUR3>P^J zUm9SSxwQ})uV1JZ4x_El)=^oSUuK();U%W-wz&3c$eaH@35*Czj9&txYqqX{xitDs zxGdOB#Pok9ZfY^QTkQpJEwOxAFwKy~9Y8+%)O zd(RVu(1A*2AEx9+gQ=bktusn#U8EUxar;2eB?{=_aEzU1RbHIM3BGy8P!_FGk~_`D zTAp!*ENny@J(j-OY-);+*~*j=QK23`)kLP+#ND&n36$}2I7-!0>K~{wh@{V-*nXZY zi&Yi1aLGCZtrw+cU$gT8o1BlBM~n9r?%+YBVX;qt>gOx+6oDcXc4gSzf#kdFPfHK> zzCwXo>np=MGpkdN1L5Q;UqM+776tR4vYh2rPWWoqDKfMoBbwjaiJrdm!1EG zQcGx^H941`z#Vppw_m*))J0t*9h)te;` z5unGkI+{|-AkK!VRp1it!xi1&EEO{fe4RD04M~sl92Z}kQyn^URU!39AiAj zN$yZ6QBupbTFU5o3PR<(V_OoyO4l2r%LNcgGX-8G-VTQ(1-d-^Si6%ZcX6P za~iF|y?H^I^j9$|UvG)7_mI2J%v=p0A8BHPr#`KlnCRSh!Is>}HGFD%H(fo+r_b3U zf=ijy%x1C8+ncT~ayquM~YV6$L zEK)o2y0Y`l4fDs4R&9uVL zwNJRdeJ)G0bZ$V{saw17|tXLZksfqX%8Q$Or4`I|$9v5;m2}u+iHJhkm@Ub~H&v zI|ouz0e@oyumcFVm?eTNlsF4CD!_Auz8R8e3H{{m-FeS)q2RVZrkCvxyr$@4yRw&} z(r?$)r+H($yQAUmSve9@!}hNVt3>+^ag1qUv6N_1>O zIzJbNw-^DUEKXDS#Kji#dzq$8Qe8Qo!@{u|wU4J%of71r@NYN%5(wH{+08YTq7A z_g&wuuq{0lOwosJ)6jhPYsAXzCa?4q1|+k_$6?;If1Xj=p6nTxE*fOA2qI=)6*f@( zvo(b8t{7-@i{7~RB)Ia*)z=obb}n15eNkk13k^D&lz~L(0IUz-za&Jr7Q&9WeM%>L z;67|=pPMCqqK0L=`5V#*G1iKc2|P`+xw;U3C%fNki$O3YUJw9GGxgJrs`^Q}mRT)p z{xCl$TMfWdEixv_kiCNAk&4d-)Po03< zGwI7Jg(hM`4&(sm#0)kJS*N-^ILc=bFwQyliSxbcVKi7U04QN78&~`$mudXzIkuoN zQIyeuzzw+~OYG7d-@B2plw1#bBvM z)(IZ@Hx>}(KQ$J1ox8d2wiVbzuq&L|+2&Y-_nlaOqRKE$Sj@vbR94@bZ)erHYOf;^ z*5*U#1>T3xc9~Z7XO%*c379@Hsr}B!@}3@d*?PEB+&>PLorExTa~!RWAC}Em`*!w) z3Opq)2xkJqaZ5seZ5}0KZ*{n0XZ?btvngVG!MyvGTZpkKb@|)U&9v?A(2tdC)m zAgppbo7~d19FQhp!0wOZ&=7cj?&fBjax_vNdb!^L6wRf+A86BLDO(jqy+KK5yuddF z2=bmc^%2@}|Myc>;{1i1C0_kz{6M1hRG9gJ!)DR}WMhY3v+yO51{q0Y5t0vbZxZba zSX8fJf$tdl4tGbXqlTo^epWtaF%Lc*5%AL|dg*P?h*r9vU};KyRZem$oWB0%>(PB9 zkY%6(ViF!>XAQ=T1^M5p*bSE}CFHE-fnB6VzLc~psb?LtcxlTx|4F!?%g|_Z%Q_%X`oD)d$i8>+K&(?{p6i53}72>p9EgiU?|m?V;|+ zdikciM68o^nDJ@hZ#I<}qe9In3kf_1k#7@Q<^%mbTS1zMplpH+lr;VBPR7n6NMbY=RWz`n z#yG$HUMjl@Jm+kLjr(!Q2BK)(64Y9p!u1E%v*TX`$}R968OILn1h?S%zlts|!3NR- zVl#}u&acw*uBSh71=gjp7c#yvr*`BW#%~1Cn`fe9k#^$*= zMEp2qIMO!df4Kd=xHu@}m`f0Ya2ggNXlCh|Y$8*h?dpk*LS66wc^ZC@CEwNv+8==z zE!rZYJS`p}t4Via<9lVQ76mx()|!El*4*+;iuYMwz+$qix^g57-O46{xrAYoO?!L` zzm$cAiz?us{g@(JsUj81-446}FI3L5&kAcPcV-;4sq%c4K|#2DOx)=$Fk1UL6Iwpg zVR@Z7bxlC~ zvpjHDM@R~`Mg+DXrPu9WjA~z-?niY2Hwx5k|F#bws;`3#tjCc|(YV)}sZTU|ohvwS zV9XI_R|Xv5vo=Fju35+eT*x6DHS8mfi{+V3YM0w7sG|tl{I#)nNGWIAaVws4)H%^~ zISrc)ZK6_x`UA|+Zpv(fMq&9I%Gx3(&ZGuo{I0gT$^jOoLHp^|`c z>aTu6j&aTF;fMoG9-T_#P6*~jujtbfeFcCv#J^b!UM#_mj}Po9&@_bPJ6o1U1D?-N za>W+6iOxX~97aB$-|u+fyEP_V++S~4y0j1gMr7?sNxh?T?{!w#D?q&2$qt_eAxaps zgZxPY&~~mve6zq_e4yCJc@dBH{d-On$pv|i#B00a#Ur!qLS5yM@-QFrkO(@F*PZLa z1A$A%RE+15xn*Y<(9SV=fo*HnM|>s!pr0b-TL$aL>Nf{`eST8wXRXu}xaWmTwN{H% z8N;~9_dR!eQsZzT{{HzR1@6lokA0+rj1+a&g`H5b3eH~5pG@z~3z(KWdRtCFe*DNE zbi@}6vm*q6KAPZI-Ik}`6bNt!Ex9jXOhw&IdQA$_mgmvac7vc-W*Ei%X8W$#2*vBr z$bq!o?^82CsM*g30_Fr9nP>%*QZ}oKc81Ywtp{baVPe2bRBuxF~(WL@y=gOzltAZKI|{H zVRs(DMkK)Wx7uub&vbrwR^lj;_>h7@0(3_WK?pk|8ZwTu3VQ8ZV>9R_zFA8hx9&Xt zK=$nU^DhW1(mxlUl|OiQoX_q&9i5P2>g4-VTNDa;_M0EzrcTv=l*lyGYd6k$v&sH6 zXo&!Jbd*$gCCWrjT=)#<`RjL|MI)$-fdjdQiBfPfkgV*jPfS#Ls#e_N1eN=T_5N(6 zknZ48Pz`eV!jKH2Crz>4(5|w$xHu&xg+Ui7_j;lRO=?I{x+ARJ{_*DVi&wT?500LX zl!SI3|IJ5v9#_gc_l5 zgEKob10+Aic&M$v(0u+`nK6W0-Hhzll@Hb|GYlLyzjngHu>r(vWVwy$3cLP%k~Av8 zBxhbD4{OD?+e@spEhY16{QNNJw4aTM2CXmP2iSHif4%jvobLJQBeBh$Wl5s*@9ne+ zzitEPJ8Zd-5pk;Rb{pu?ZKrsrQg7h2I3ROtF(U6`9^jw8>@sOJ>Tip<3Gcf_+W8#4 zo4W+ttA9etZ(%sx4Lq|5+KyNu&*>+h0Sm;rP2>uy@gvx z*}C~j^01GOX?K@SxG%$5#q&~s#H8)p&JF?T!9w92pdHL2tMA(Db^G_qi@o@HeZ&z2#*S>Z++@v|?JY|hvFz3>%E?TQ|K0ZFzc(iAmLhQN2lq8h1 zOv=pxi(PP{5fA}6O8Z75!k9!`b7I?mKmCI1nO)5>te6e!R>1irOT(t;*-kHaVjo|i znKpU}0I3Z1NUo6sPCMg@R~4o)D@E?jyBh7%wnS~g`=`z_kT>eXVua1o6{0v%0SUH) zHU1(|+PVT}Wv{!z(CZFvU5Wg9*I_hlG=z;&;=Cb>p}mIU^l1!2GH=+kd-wS=XAe!o zWpSY+46lY%(Nsa0Sy#x0NIqv>&hA5H@y_SSx1I@m4&zY$uv47L%Y((we~{Cu!5nX^ zu85FJUJcxp9r2n5vWu_5YG%NfhU|QB8dyXNtuo>GvI0Q|z>GJLDfzrf zU)j`{oX6`L0gl4dIp~tFwPqR=JH!2wLAuRTTx%q!xpTMaN_F#;QRaST&dz z#VJ5`Ays9c%vqV-FSGA0b{n!AiW2~~rk8inGH^FN=8tB1yws|)B=pbcs}5+Yx_QuL z%a^#h9aV;Q@${HGaHifuclu(+N^7jFZ=D%kXlN5-$2SRcP_di(+yDV|hsiIq@j*Tw z*i}F+-8~K7^&W6piJ#4gnG4G+6SMb`IG>rJWR2+no0_GnNI+Q?BMR|S|}2hk}?BahPP&0QH`~; zS-mn|OAfxQXC#%LZX4TPy?(I^osh*ew;fl^%+j8pdm2W`t;v4)@F8+{sk>N75=O%o z=e|&|4i;S{7hCVk-1l_+`~5LP*#@O*D6UeD?p`)7<-{k#VtINM0J4a=WC})6Ck{O} zI_9Y6XzY?s3^tlCimNosvv}4CU2gA()=VtFED0HCo ze!j*ZLze3~riE~rP;56cVjfc#KvVxpS_5wl+Hs$Q^;Oyv$5U=(7`Z-+euQ@(wf6jJ z7qA-BS^*r8+#b>ze6TKUuWMaN1sz=~^m&`Xxrc#x98I)0ieUYHroA z<_2*FJcxY-(q9@r|Me_@6m;dWNT}bIaE++!}_vKnwN^%%Iy&=`&rrj*-Btba3@uk?7p(k z+iR_V;qewD&s|TDVZX(oFPBHha$icSn(ojB2?LwvQ@9&FX{z2E8--xAdYxP4ybrm- z)@GiEmWTG`{_vzJ}t=NGkXqO?1$Ju%(}=*ectiR0_CXW{G+ ztA7j)Hjgq{YY>x3z;n%M0(y}+{j|tZxi`aeyCq)wapl6zhYjb3C-w61m9V`!u!g$9 zB1S}_9S)fBte%!!P&X)$ivBrj51+W4 zF-i`s22|!VSGX4NKR)-ri?>nDAbQrUie-Q{W(LCHBhLy}2p=Kl~tA{!oqI9)0D1I97VS3e%r~ZpzLE zfW#YHVD+IiiO6(5CcsKlVL- z!jEkXvmDnsj()aRW@F@>{tLzT_Z2N>rVsXG@hqlqLd?_y04wB zO$K{h5cLr>m_MRnP-uT+Yiu39hd&JuWEh_YE`v_L!*5bW@l3y+kjgUpaJE^ zYGs7VzQtV<=izyFPDU!@8B!h{e`G6#BY=6!t*27FuWJ$Xb!p=jZj^y7irjh9QQIVdy#A|9$TJIqRHtUOw-hdB<9_ zV7TVG_Wtfq5>N@y2K^^TNJXbg3_H6EmfwZo?#RHr2&~zkg#8lH+3RBmLCl$~nd2L$ zwWYnsi>IP4sC=aA`NOA_+_oPNRfsdacY~@JAkRiEuzMwOiZ#}YS=}3pf=wMKk4&#o zbm<;;xl{_e+ExUdE*k{Z556qqmpv5HgnCeJONJ}IzuW_ zh*SjuOE!P`uw_yo#26_^?f=S4Mn$2!JzrqQgB^~L1j@(H1zvy)~ml5a=JBI-q z+Z!2Ww_rkQkSQB$aC18AfJ;dABu0}`z&Ts5p%S$mLd0;4>?7SR0{%iMTXLRvz5n$E z3L%es05nu2%xX#;tst^wJ@K{Cd+d$jw3hiureq)R^E7*fj^^wh>X3UX<~yBo9ABW5 zLE&J!pwHIEqD6Rmt=iC17}%@}uJ^xwz2d-nf4p+2BLri%M_vSVp)#+JT0zdwJk$G- zMO~dof3^3s98J~MYHilmOs3x3UBuWkw{nEVu2iDKIJ!^;Oq0mL)IRVh0|Enu+)z{HM(xk2egVhxaG|=~ZYrBz{R{dk zHiKrZPzP|1yFMNg15XZcGR6G-`7i#N!;AVWQV@fGG{qG93O)|j^_v(f(fY-rV-S;Z zcREepd~@+~b$D2bP0+<644S|(Gu?H~Yr9y=W=<4IJ-wLcgE!0Z=-~Lg`dE6Tv=?0N zi_`DHc(!KUac;F1Q!QTMr)lBTBEHKhGGsWH?gvY46D7I@!Qg)ZopKn=V&vIMC`X}s zQE4vrsE-367nKzhxVX5|-CAmHq%zSGr@W^il?)hdx8w_f=^6^|h3oZXhYNrN{CX$O zGYdd(y1vlfKTRI{?r4Kn#=ag2 z-;QU9ytYDAHe9IUp4uBWIll{xoSD*^nO5y?!LDi&)OQfR0+U+-7KSfS6^n zvVtBGcBO!3dd_j9BiN(?qvti(YH+O$w}82N|+Qpe0gR&Uy$eXCqEOB$OCXVEMz)+}o|oCf!bdVvaX&!G6@3q|-dpXjLOosvNHR^x0CK%eFT;2L_(j zG;1-!jfXygYnMfNv5hER;z!jT%WThB!_TW2z$D&;Uh5?yJ_x5cFVeRD3qx}n{psMf z(~&zL&JYJahwsb3j5OGEYtR0kzNuwWrYQ%`}Qz z^PB=rX3o9o-9n&@(K>B-vMe&a`QSoci}obItBOI+{5p^E#laq1>szK*GAa3vU-aU* zgaMQdg7HNC-vO#$E}h-YT+<>%5BQ9HyAf4SpBx!um&ZUcIEbA47|WPp({)nHt^mw1 z@@cH@Vwsh1B2ycGQIT+@FwA_JWj8!-z%D6+yCaks&PJ%1(%`u=-YJBkg3r1R?WkmY zd@PMSb_oIn2imeM*!3FyL8_Un6-8Bj14(~+duQ)Nc-!w;3<0GGEf&hgL|qZ;BB)I_ z{UXlg{(J)jlubT8TI*l`$=u1d_#1|+c=zstTr7W7ksPozR30Mwfl5f&`O0>97Z*hE z4WiQCXc7zohS#?Eo6|8MW`9pJm4JG^70IJlwG;TYE$^ZkK)q zhwO^!{KeDNZmOnL-OIFj`Qs|Ri9!%W5`Gd$$5eVhZwoZDT=dH>&B-`(-ctPuxDxHCKCIR@*4vZQAuwV3_LpA#txN=h3(ua{Zw~}ZZ((l% zJB$XEHti58oiaRTjAbUi=g?_2&d-K-m(xAe>-7E97t#NZ)H!Le=l>~nrti(pe(@mn zKSJjM(2SlWuh-xXOd2_4lOj& zw6Mu2`MPeB$2y8T8KWNrQmj6;r==#VH`r&z6&6M)qpG9C`04_$01LhDvVBoP9yOn? z{+&ioE`C7K@eMiCow@r7PwH3OH%5edXD-8}piX+`i_KI!Y0U_x%0Yu=X2M^9Eyt5P8 zw10m#P1>QbcztqZ4e+DUI;c=GG5_}#`ra_AM9B2ZwpG6>HpQgKj09%K$U)njdph|OZ!J*M)b@L}@UR|)k0t}mg z$dTdC$&1U>V>&i<=}x1KE6A}wuDzqWQS2L!P&K9fnAD&b(3fvOhZpNBADXC*HwhkM zmD*FbZJUahx6B-(a*}Nr~xkkt4_t%_;jB+5w) zp8IbFY+Yh(ea<;Ah-wQ~A<9;^%(&fACL46s6D=<_Oa;7`#}CCdISk8FmRHWlNC(rn zlLh=!1>7W{Ro>p9#dv^(O(&nUxw$#)TA>RL@}khneJpDEzfk(j?ZFVp`4JS%5Q9m? zW|$_hriglty6!M5y=X;vnfGp6B*(q>Jow41UTD4lcEKS_DjX4=m1H+ap?c5P%jRz? z*$VWkerK*hm1CnnQT)!$oqVTr2e&bXO893RsXWV#MVF+t2{xUe-kEF?ue$#zvmZr#zV5q1 zxzeAGW&*v2Vp-!j28;48eY)Slk}_@zk8<#Q%-!Tt7)Nr7_}q87*zES(Vy1=XSXu5T zX*akx$(~f24a~vXrAK(5@X_8%zp4ZMeSn4v{?;PuxHcX~X=KA*fINs&MdPdFc{kA9 za{*`E1JH#O{R+jQSM93`8XUtRb~AHOZ(Xb9|8kiQ4qENc6E1ihdSwilUDbTQI*vzSW48>tX$ZKgzi~DWs1G!0zL`9dCA7i-Pc) z*SiQEOg@g0Cf3W9MJi>wN)F^|t3q0R4~w1do%{*kj;edewy@sz7-XWxqa`U&Y;WQT!TWYAPQ-XC?q6%DlZLtGg50u4of_(wbON&t6yGT zHsWo+zQm}jo}BIqg^}}Vgo-)ev0eIoi@6k0WOp0852jrf8D?&ajIA#J%4``u{mj;Z zn*Setd$e8ef9u;fUVu_7*%baLDyv-~SCuDxRrmHn^WVfXSk%Tc6}!G=Qr7I|#aF+y zf56ul%gFD2)%=sx2!Rj!(Uh7$7$Ck2)fO=T##y_fOHJmrymhdsh)&TB4N@JX$x`t; z|7f{MuHCJp)U%)UlUN_=OVmph-JVdP;bl8**+c+arkcZ|h>Q9;&wL|SF$bGS&Cbk? zxXdGY2whf8mN@)ej_<=c;HbXT?H3Xrf(~uX(nNqKsI_Q~eM6{y)oJjp0A|h2ku^ZE zfrX8j%@k=ZU-OW(QvQ`I6(7$|o$BHd&=J|6M-}Yq0;a68u?cu#ojcN77$M3Wa?Xv+ znd`aJ>(FrR7T2gZL>t@59_v?#Rs!st_28w^HM2TP9JeTKdM=gTeKCLU-CV=kVh8<5 z*h72~rx`UO31=02pl@_sA0H_bwe#7PU8^revLMD+Q0G+^WAtHHtCKw>vFg_2ig9zU z4_z4;-^x>8&MFn%30Q;?aqny!D5l-@*fK+^V1Y!%uTk2af#J}2`bVvl|96ndom9l? ze0aq)FNW<(c>ML-H}*6kZINR2GOLupK9{`6f<1|v1Le*;SAMnjI%Z+sNIx&rzS3J~ z7_B<{l2_0n;CV@52LO^hHNWGhJcZP1{^y(>uB?>8o*zzkDn#l3T6nh0aOG3@l_6S< zu2vw6vCk`sj}J|{v$DczGf^zDulGfNyjWZAi8_q#>`xHhC5CTb*Q@gfaUrmAWYyn` zZ)x#wa^F=U1a)2@^xF{(cmO`hkNgGj3n+^V^*`7LVEE#Ef=Ow0)xRyIo>QTAcm=9P z#g-zhwOI?|e%y0=@WtV4+83Q^AyhDLCSJ=^IwwvgWbK=idV#9p*xcLxgkF)_zQ?Wy2{?r>_YYp3_7%4 zNepNd+U8KDLy9_MT%V`dOk7*8&Xg~EtO1{WWvgXKC2E=$*F!F|Fn*iYHi}oDLf6lF zS*8MoJEn4{x`s;(G}G60Md#?j-0o6A0+lf zMs9s(%2)&Ecs2`=h`K*rmCKS8ghy)DpP(E{2(VxWkaadyVdS({ZKc|JfnurJuFXn>Dl~@2Mx#%Roa@69IS28FFOYjc%t_~^l>W|P{&2#k|GC+R^jw+1&HXNr z2!F8Z0RzRw7i(Kpz`*8Y0RS6U{#eMWT!(PqX{_)%>CBtnYd$L^rH@j z!ChLYQ}Gpf0uEjubQp>>#q?|V2fvc(--FW0taN}vE%30+ zSvz_i9)(*yrnJwMP0WPsyVnMD)Mb1UJ=P7{8WohiUkI^|3ojs|`^adNvv6{Gs;_E) z$;_?hW~ZH}BhY6Z#+l)}MdLl{eY1k#bKtJhK3apDYwM5G%9+DPJV^L7`%o3yRiB%Q3^f-T1>S6y+vBNFgwPl%>;^5pzoMT74Bt zs1V*ofb+H=V4>EBbG@xMuYVUOx+fFBY^z6Vmr+POF@mismu+2r^E_wA6mB2+8P_QN zq`UXek_KJ~Si!nVP7VX?4O))|NK*-S=1avcx4+-y^l5@Dr}`uaK-|2`^CeyFtlWNO z@Y{@6r|R6Gh>yeCkSOHwqbk2*7+-1P9JOdYhUV2Pt4dP(96D7|*uG zJ>x-X!Vcy=0>^UXNRLVX2I+=!v}o0!ICwfoMw|wcY5?%Vnd}0qPiJ5tK^PE9;&9#qjsjH+MGHIVMSW7MGT9O}>qn{WCT}|RGEKO6{?Fc39 zwW(2e1NJ&_Sk=*w(Gw6pSs)CwUxdUX@EYw3skB~{q1hMAIxkMk(KgGgq-Wixs@sfag;6zsQn%e z>;q9PK>iUe7RfZ~2Yh|P<9*dcxHuhc1w_HEdy_KZTv;&`jt7LRWV}{IoudGq?23FY zYSfQ0A7jxd5HTL-jt_ItX8`$k&j14MG0fs)+7wfqwlGF&Z?91|o~W8@cnS@*C|FX_ z@a*~A)7FAbsN^yZ7ulq`};M66Z>e8-NS0TPk2z!l2PYkeigd0p8+U4z1Vp zFMeY&i`egoQ7{2m2QG$eDC*vAx1lYk)d-ndqQ77eY+Zx^t z8YMG^{^vO)WwNo1QCt=Ze%VL&@Ycx#`-1S~HRC}B#Qs!7lU_qzlJK{0-((dE>xkla zv9k`X0KfJ`6e7&CD(+x8z{qq7`WpW5n0)i*fp5S;?Qry(UK55=%;#_h1PEB~n3FTd zF;z1AK1u{NqkL9c`mL#_jAy;_)T+`7ajPNQ#krn>8{hMQSI<8l;d3hd%J_>_%~X>I zcDeYTjas{D2ZD$XllBJ*Emm`)iM+W3UGQf#ZyvD=HG*i)BWSpfe5M@laz$ccAKT2i-i-mmIBy5QVz9- zq0f(j!y%sEX{9U}yHKcn?|M1oPOjm3o?`mR-i+G7MAlH<6He$oTJn1#jC zwamdxvToSjpWPRaw}ZZt(ev(8UQ-Nv#)IHTf{aSSy~X(&yLReUf)=;cLJ}%N0zmZq z=~oA**R&<}*qH3j!v1MlOT@Y9?_Su~S)?J}jZ zA3xEEpb}{WNKPDDgzl3aqAj-iC#JypO$;cVSq~gQE1)#cPfwmDo%v+ zg2bO<)C0iCT}(ltP^zo;v*vq0EGs0ZCS1+myp#pB1;($JIV%`b@%`cs%H*C5$_M_P$2HmQBrai-eNRxDn;{5a$n+58) zXF(3oIv_n^xB$E#4wTcb2q)*`ww?L_zdYSl6>$JijI0l#O76#L@O^7@D z-1q$GnZ3-?VyNqHe>$0zDYlp@62)~^1D}JQ=V+x&nxSHQu#2AbL^Bm77p+B$MWh*> z7uGR%?~47Guj6(uC*4B{^v2#iEod^YmT#?aH;&itHd+`Zsa_U5!yX57`}qj3_M z9Cv4aNY^u;9T;Z8Xa63tWH#x$q|ewO?O->Lg7-H(Eknzmd;N2XsfnFtm|0zK54+S| zejI^BB~e0WPaoPW^<-xuDl@3;!u`In2o8TB_f>B4?(FDzW#%|^QM0v=gVPh_RppQP zz{a4EIGNM^ySb+F0saHB@u~)I>*-%kMHOP#NR4Jw5}$_n6G0ah9D#7s|KlS$X>U+60SYne!L#v_^E%}Yn zfepDEQ}>fE_=jNA$tK^)P{As)Q*kV1)zv`2J*dzm#R& zTn27h8ymj(c&QgE^s^%qzZBF3CWU+sy<5|Ejg42RL3-MmIYZ{4`4w zJ2Z2ig`F7_Mrm-RLLsHW7QD=i;&_?DHE5Nh2ExyNl{F%9`s5f^M zulJRxO{?l{bnIEK)7REc?+54ePS5DbH5i#%YK_%hg>Y$RS1+uPN;-Po9Lamd^c2@@ zoZrHE!4*H4e&OZYb!IlmZeK-e$E4d*8LPP%?yKqd4BDf0p+^G;CArJljvESpr-K zsE&Jorlry8hlf21##ixcCrSTV|7pv73oHG#A(x{otJ!gtocWznT7>^%9~ETmU^_c}VdD8>A=4T|-%jHzqI6C`UuZ$ZyJkUNpm z>Lcc(fe6vP8$YczmJozA8H!PhkVt{_QK%*x|~zJD)xI|Zgg?+hbT@9XnIj>N?Oq0K^~rw1>a?T>FT zxHh~Eixrr9mD3WvQ+0(hjhZ9Uvg~^nBZ|g~DJ^<=x#nZvQZFM|;+K@)b7^lcR>3?9 zo;Xj>4>aiaU4TTT$4HjReF`DB0{a21qJ!C?bgK^3xqCDZwNQKcy}p-sg5#R$$q34U zE53(oV`^Fld*-KLvC>y!>#!dI>yEu80}gL)n*R`bkm{1NG$?P}&|27Eiu|0MEP`Sb zlW{)t$^UM@WQe3xA^|aMoWW!2hQc04X^WvwmtyS%9C;V|^fp!s9Nk0&^=@p2rZB~W+-3}5xnu;E zP$)7Ik~Ftez+v^w_ku_g0c*7xeyCU9O>s#H?xSbrpn^f!0Pw1zp{QeXP$`miG&%xA1%Kp(caXOn>@GNw-g7W@Vn|JB>w%UlJ7DXb^j_6^0EZ z)QuG_slsRAH1xa*vg9-_y6;D;=+WwBGfYecG&REhvOFXf;f10zi?`^I@srRgMa<-P#)$#CNut)6)0Z=FxSWIfSaX!e(Z-K39o$!S~9HT!kK5jiJ^EMix- zC-n|u(UYxXMTqY7NIR^C1D^+BlsT11yil5@a4<9T9IL>wx7hyS-AH0Y+V_JUYZ<3+0J5X`U(8W zlPBXFw9%t^tfZK$wmVo5s9tIS)6rfmP-S>EDQiE{=`ZM9|#S1N}R+aflvHs180X9h#oM7_Dg^9qvNAFazAsJUK;qA72sM*jr`{ZLNo8}? zWw^g!HCEg2CV^AqYIANoUW{bJ-8%JZv>F-_Sr1MwKabi|btnBPj=V{vudF0l%E!WXvwp6N8C=e+ zjFovqVmrDG0oJy;c}|B$kCn-S5g%eJ@e{n85i0hUObYGT<<;QG0EBAa%R38C^0Hai z&rf818jAAFS6!O=pff*jSyxK4_X@H~~U)w8o3IkVefz1h2#mbGTG-#;*dQSR7f3s+Rt9Qk@V z_6zbPC{p13#H({KfF2F4N)u*lezLc(uawWcx!)9 zP|(nMJTql_{9pxzV2NHJniMtd`4`Ot=np^4rAhl>`$^*D0PrUSy7^TN!@5 zg*ZSnq1MWsX5XRhcQ~U;-R&o*{^(&035fI!(V>09F3T4KvlvOQp)aa#Hfn>$w=+{H zldrzj?D7Xi(dRoNsFI@!}Y+R}!c{%>qvc;KCljje)s=hnew? zCCax(?V@B`Z~Vm{IPiObsKtp=eG8!Gn_duK63hBI>iN(iAU1aV*Uy9U?>!wI79{Jr z?42nTI|kk`IR%-=txt04>1P7pvl}+89vtFjDm4UPhQH`LUwkhpz+Bx46#s)KH1(8i zCBcmo%=x55M~DBz-iaR)F8}%s53^2od415m`C*m3 zsXV91!1i?E5P82Fy|2*D6iSz=2p-)qnS-yC1}=syrqJe?Zm zD_O?kru$*HNLRCcp-D*>!`Qvutk-$E|BVGCYXWYJlatq-8MUH1QumhO_pO`A<`fmW zE7Fj7P6z~xOL(=vh=~jddBelQrqWTNk^`p^8q{|VY}xr~_d-kz{~fOGU<(W^!K z5k9_ND6w+-wN&6gm(|Cjk3|+S)p?r~MoLrXeyzr_`dfd2#R2g`%L35gR9dS=$?Qn(8BSKJfeZudmWf zhqSkT+sHM?dS$C7mou;=^V&VZ5KE^;$Fb;L#E2h92kw{e=2rlzI<@be?!;w%B@0QC=h z6!dF{1!^n$%exE=t{a=$Cie9t?~NST4-t^EAN(_|^BIYEaelYb2{IoYd`Zn5&bfU1 z0OtUHy1&S9gGQs-RCB}b;+z8wy|$AJ)01aUX&x#>5FndHn9n=~NNQs~YpRaU^RUv4 z{w*CFXt3RihJvJG+tQafI1m3!t4goE-zJ!Uh4Y(5$oH&VLd*&0-sfH~U}Yiy+UxZ~ z7;+;lAkd(c@&2PTezPDwDC~Snu}}p3C`$u0G;$^;CVy*ZF2bdNp#49ulm8X%4f~%? z!ngbX6JS65zv>JB&s+aDT>*uRwY8@YIPk*1NB!5A@);uc7}w?jM^@+GiqgL?n0V-4 z>F51Jyk%WooE}93g#z#t+zZ6~cNgUUrjPc2?vDRGS0Ezlu1HLDG}$G$!UjDNk%X2VKIeWey_-f&uKAO6{0>rSvo*DTx&KT|7C+Ksru?CCW*FMurP&HR7rR;W7V zyw%GPzyJJEfyZQbbZ{{cUM{v09W15se|Ru7Y6e$l*3WG|u~q?ih=jBVB#jx~)kUu{ z!TSW~>F0XaX$>xb3sm=?xN*|cn}b+U%>E_rebLI=+QX9*R=|RxQHY$Ye~F2qDJiTE zNw%_umDzz!dU#>M;PwFrP5ro#KZaT%85kZ*?%xl3C^RJ?Bv5OoTqkj8JVV@dbK-Fl zqow?e$M;uLa@EPcPNnxO5TQwCsLpnkBu&JfD7MX~yO5RvL7*57Dx}ifFc%@2{{*e1xw=S@xn#^LBVjR{(*>nYg&zeGetOx+T- z^+ss1G03?nEc_lHAKg|V^?J2fyyp6!c@x?TN_6^kb)-^lL{`S7fhy^rLk zz-4IQOtqtiluH%<2q(|fOEt4*YtD5EG*nC7hebHjY5}g;284tD+0Yd zrOrsWN}7bEBzkXN4qlXDgvcjbw@ zqTa8|eYjE!a*Q`7YDf&5`MhS{GvVf5pXi$xY2a z!PqMlT0shETUwuUTmA$%bH1`kgWFmZt#m9pRnV>3aXB+JH8s{}zg3JiI4^JP2-yc6 zADx+LRLxVMr>D2K2Wu~uJwWc-%-&qk?2iS?COSGzQbM%B>GEbmUwSR$6}1Sk^jv@1 za1W=u!b(Vp$bjmwzI{Ufrc6aEu*+_yr*}g#(sT$e*OF^fy}hCA@kvSECrhOcVh{#l z))KB?|1ie0C%jqNriLcbSj6)6wg4FDoBZrk7q?c|y|tC|*8SNUZNR%CGVNiD1GR~3 z^Fn;s3z5~O79JG-^_k6~gM{biA+1k$gty`J$?DV)Ny!WgvcJ=XA=df)M~k5^#w7@j zz$0chwd^4G$GL|*-JNwd0&J4qY;E6p1UL)=Jg7*oRIOkX)D6kSu@=~-f}8bVIsM1` zw{L%gLHDmsf+-$A;Z=a5-y%*C^3Y%hq^R-n`hb9~65)%*H$`XL;Pg4wV7c6dE|S06 zyCbA)6JB$Xk>G=aJb+1x z?QBbm?fM6f`JdmzG2I0X9&g?hM!s*b`@s)sLEW34oWuYo4jeEK+Hk4Ii!>F1BwbV< z4oYF!Vc473TL@wAr;McFYDrl?xS~G1w__voD&dZIZL+9OP^@T_$HyI2o1&CKv*+nQ zJ=d?$-EA3dFCVYSc}^Nj>V*oZGj5m}8Rd@4$jHMSx6G|C#;2gZvWZD3Dl1V&{wt$T z<&4kBpKBCoNj1GciJ78hV846!n{arjM|qu-6T3>CIUS2!sHU)pT}X%w!=T=Se;yIw zMe=fUJF=secGk3Dg^`I?>|H(aqYmrmeSFYPlsY`br6=+B4K1$ZCwrf9!js^y%j@u z@%kS;f^ZPvQ+TE+9Jr#{SMVKHPrPE9P0_blqG~D5(i^r@voKu{0z^ z8F{oo#^AUefrW1SXDllp?Hg2NN}|T|ElpCpnGA2?U~l<&*sH;5sBNcfL#2YiX)pZj zqC2nR4M9-o_w=<`Q?Ki9h|pL;aGc)(xEdPv@uNI|Z7Xvl1DiefPDYwM?7icdV|ul- zKM6|12CW$g%Pii(Si zxtYI53gD5j3VZC?M$t%8P=jAW4RjoQ9{PT zXDc$CDN|B6!{Y@V8!LC4jN0{yu->n+Zr`gN*sihFVF)(){DUY#z@g@!v$amL7EypN zn8NxF$Ko-R@Ux#k6HaW~M&;ovk&6&Z3mZzR$7oC@+PDHNyiU7)lY5>NPw6!`CmJMU zE)YlVyH)EYBvI*%XBuSa%N^pQp2$id`2}Ex=h-C;2AfM4k)y)&r5t>>G~3=fo}93o zo?I=73rC$>8%AA?8@9SBn!xx)hN4Twaou~3B+^@0`q~qgg4C8Qc9MS;)!ie+`J5#Yp&7g*qz>bDnc4Zsd zb^WSYre>z=tTCs6!bEnCF%XzP)=Lv{(_+#r(3pi~$GfN2Qu;8q<@D&Ps;c5wUag{c zH});VS_Q5*_Mt}@qMLu)%#Nxo41MPm4QK;KHb#&A9rz}uY}OrClWKQfTcO?y`QU~J zPc<5K4iU8sKciB!|E@DNV%7FzUVu83=r=^_(-Litt%F1QG38{5E(0kLLE$Ud{*NwT zvRKasSnP zJCK9Te;XIOom^Zlf%CGaX(%-Q#A+-zoLWr5s+gHoJM*GYX6|{Er!3C+!L1uN z^p%&?L`-`_Gnj-Lth!&Ke}0HcNudeI083#{=!6Ezf&tyy5lA|Aqms`yceZ94FUm=@ zr4shm5UPm<@omm~EBJt-d+0=D&Y{N;(AR(Z1Xr)tyeXC|RKr$TMJ~P4;Es>m&m;Bs z#w*R8gJ8XfSg4wCNmDgq&yy{UOpQU35F2p$vyAk5sHeQj1b`;2`Fn z+x=)!FfuOab|b2Pu5ne&YbKyyL51R=?ztRT4Cufaoc`=EE2f*E2YEkSx1qu3d%*W) zcRO9oM)iX}l7X2zo8y>G#$8JB0P4ktoTQYtQFx>Ie3xM4Vy+2cj_=L^6_X1PIjF)Z zn@$kg(qBJQ859oQrO|EnJ1&eZV^0yv0H347@7Y{cP*=%RrP29!LT@wP0%cFF4`qXB zV4iw)%d`FN-TVN#hR4QwdUF7?28)8A89WvwV1sVU_g_B?dg!(IChBc2i0pPAk7P+# znGam7ghuT*bDaU55g1z)K<-d11L&;10L}7iM+t9C0Fpva{5mSeIxkMeyJ!@w81^&O z$|l!e9}u-%9pQm1rw{&02YxJ0enuy98};tDNzRVm_?y`aX+;Ay;}>@V-{%O=v8XfJ znV5NDjed=qD}B{#fx_YXuH+LXG#k-`)c2&NI;Uo}Z72@rI>}`lzLlBxb>v$j@SG&m zN6lbzI@$?~XhMG9D}X_T_$i+2m1vjvrYS1cZY&jm2xcZ91bEi6n~Fe?-EPxOaAZ-0 z`_4FBhr)CE3)Cm>6R90$E4~yvcI|&?2`QAr=jLi+P%8!O?_-MWs#eIZ3syJ7SD-W~C*jO6WU%1#+KwYi z2UyTcuOzDNl*_DhIPc%TPs+YH(=3i|Fh(}&6T_h@t#B_nUJ0=fXrCm-F8O$=PWE?w z3DbU%7=7oq_?pS!xy5z<@|_%tzWAcrs+u^5otD5nn#{EC?~xA|a$s|Y6xuvKK6<`? ziqCCgUsb2n1?QWa*d6lo`wo4HalV7_A-pX)K3gGA3lJuHud^2>SKXD!qMi8$r@1<6 zD+VXn{buaIQ~`=I9f2EByrMhH7!zU-hYp=R!6?KDtj-HP-~R>rJ(9F*WYulvkDJgd z6jvZ!kkPEvKP#@{jpEgwo@QyjF_es#w@}72l*9#*U-mT*hyr-Fx&2RBx5g{(z;}Sj zCH?X`eq>SpOy3|Z)aT+H7Y6mi6B2|W{643`Oe}j=dHObbxAM+BEU~BE96bF+Uyjx7PPTMusUaQQ@+W-?VU|Z;! zn6^*|8zTC!__6+5cP>JunbdzY5aAF9^i8TbJHuCqXV)qVLMp0Mmu)?!D=-NvH_O{b zLA#*1UA;`F)JT8PNes)jLgh4dEj?ImlgLYKj(lhq7WN3)TN#<&@+z7m{LT-k^Scgl zl6Ap_>>u3Lj*AMi4u+5ICkm9UQ0a0+F{3lV05Z<ZW2gmKP(4WF!HD!n+h&#tSzSFqkc`>i*H)EyOs#B*h0Nt#WxwcNOrJe%}er#L@r@VT5J+;@f(-DLLm?8 zT(OfwPyu4q0S7kp+VbH~In5Bvx&RksRNL-6Q@btlPVSIFiD~?Z@?*G$G=l{bk^AX$ zhb|i0KiRBCU`4tKoRL0%c(S^>8jRnUz|6qQtHGq0c$CapSN{!+U7~&$VDAeO z=x1MDS@AyEWRI5&s|Td|<9D(R45D{?WwwE754m)-TerA3cV5EkZZ-U?4Gt~p2!w}T zgN&v5M)}2-!VF2!cfjq}h${L8HeDtQ&#SYLKSAn{dlJ`xdGhuuXzVI8Y`p=x}& z)N8&AyB}qC-7-n<3awUPpDZd7BC?&$Z0YI=+p`~pHPuG-(R^z3n-m+$W5@|dB)q{N z4BmGiOR(#zD4$;vr9?VgYGpe&Rj0x{P0;rd^$hN{6X}p)P181&w#~WH5GnuoIvY*R zk-X|?9e5H?{~1|%y^S(mO)R27TSj8kS%6hoMZ2t8nRukc+xmQEj*QqvgJ(y?{o+R~ z`8_AZjD()gO21hHYqUoAJIzj)3kC3uDEaS=%Ku&Q9%$mRLrWI}de&nVrYrrLcnPT| zXqlK~48ndq(&Q}&4k5F(-Q}cWVU_UvK#gzo@}2B-hGQ<50{>GDLiYBYtBG6_%us7$ zcE>IA^>s09MZIrl*kRdFnjj77?QyaqUaJKIzX)etq06>*Ddte2y3#0|W*LwzkzbJu zRD{(2XUo9F4ltdP;;YRpLQ;0Ud{!XbWj3R$v0Z5H1bZj;!sc&)LrIA`>>5P%iMnq7 z?7v91IlDT3JXu;`u%KD2rCn{QytT*)s*=D(LGMf-k12{l`%tQPdQ}hq0eJ(eDstK& zt8c*%qu)^MnuGgdWJM*tli`*B>A+JokE{)+q!I3HDP^+Uv=h6I=jdjnae9|Xcqj*Q zxl}(RR@$in;-vIJiu}trw?Zm0=qYi`hijlPQ%+aA*a>Z@l6;aal2H{a+8|r?P0&22 z*q~;v5WlI@>VuX1l|7V6opUJ9<07SyI*$K)a%hB@9L)#|QhRQyrw2YN%+B7{HYn9J zSR!ubXy)1J&PZU@tN>#iD6z-n7PtfGWKy;ZeEbV!?Upu-p zvwB<;;A`dt191p^ILI$9?bB!#b5ybG3FlnoYD}^=l4VOyxpIEBx z4HTOKO2VOv&&yaQ$D~e$Jq{a%-~9{7#EgYOW!{|_hlNYaSg86;_v%yvgDL~0$+Yc} z*BF4d;@MIJ9D6J>DKOkwWXZxLZaZbKGX_wjR&FXoM$lDZf-5hS(Z=oIfMEg)wR21l?Vn+>r8hQusudmly z3>l!)Mt~q3vD6mmvGXCh=o9b~Pzr(JrNCvvq7uMsAS1a48A1fHohr{barld51qTP8 zUMPxD{#{>`|9SA>as19^!oShewcvQ2uOO0re*6qwW*y$iu@k;ze&J8~?Mr`_Y4NU+5U#19i9L!p$rbK?Nq* z$mVy#iw}u-|2(?6-Y0g}o|B5wA#ueR?XGU@&LFI~d7#?b+OKl?tXx@(O^Q-W!YTCV z=?ar#B&01UgaO6BG~zPj6EaIjs-l`6+CMTQi`jT%_jfG_5a((fDIok^$S$nHgLtdu z8G9GUGv@1E823>7P%Cz5wo+R!$q$Ewa_hd*~ zR`$n*9-q1SdLn{#XEWP0{*7YhE~P7mBr?1=!E$hPR0r-LO6>NiOgqoNit+n3w zede6Mc``qkuN_*fl=d)NZ6aH_wV?Va+vD^a?8>aNeN#iRs{ALncJ)l&tZS86A4lwR z{+O8eZjSv*QDeO}fIh~%96oQNZmgEEoW4_`1@6%vl;X{b;IYuC47|%q=(v&v!pJ3= zUVTv=a~A@_W5x`+PE?4;-~0^w)YK^?EECBxv(zs*uw6B?9+2MC=bVtYOeFi*;q`k~ z)XLG;_P%C70O$D(Qz*lYSOO} zh+(`oozmHqufZqYLA7x>=Iei*BrEyoN!W4#EoY{D9ZPZx^VtC}A1_mrkN@>n=5i&^ zT;@-|+F}voLVSZG!vl1nTPa-_i$}Ul4RyWrFZmz8zcp}eM4=%P!ytvV#?I42w zIg$97@Y#FPsorH9N3O2nK|M#tIiGIhwv;>K1en=AUBNcJ7MT?NwJKNWVW7lOY8x*fvGO&Ij<U;g+% zom^erruk7R@E>15Md!Bwi*zdg@M}rbKV_w)2;O*y3H1lMN*0hRdUR#FO*ac|g6mg7 zR@33$_Dmui6X4nKS@LskEBaRQ)E{hRfq^86V_I6zLaG}B$c;+%AeR|-ALXjdBo=VA zJj$!=SO~93j9}5Z>_9Z$(HpA*(>H_!$!(9`(D!Jv6(Eq0nJUB93u?{?=BnL~&kklD z#lKi-tUZkC)J)2Gms7vel9n?F~9QN<_bV|8Y z(ggfkZJ$yJ+9+HMr4;P%XDg<>HN+#qk6zN}31*vbDMXkRMUM0mF6Du6!Mp9-<~AK~ z?(@MZ!Y-8U)vPg*o0I`@194oHAm?E#27_SOUmxZh*~CUZ3w9saf-SGE*87TRld_JX z+b1zI_oC!#%2B<)xOdR(`9~x_Kz^EXO^$qWm*VFMkCc_7qqiw z4pTlW6<^jtyA70yX~*}6sRX>Z5!i#M#&Mf1uyscwi+XyBoo>j_Ny*B{3ZJ>H{@fQD zsd3$|9*dQ~&;PCaz-}VH9n4N+-;du57}8_WYiYAr_k8j9zgwFYtJz2fuc0=F66M$( zW>!`?BWu!o_jn+|WKj5~?iymgF@x}QZR$nRe?PZA6m#0)5xL<5=WG{yi?#W!o?Ii0KL0RT zUl!wn3v{<^3>It4$c&~mWk1HUH&f{_FSvucgpK0L`czs{Sn3kqOZYr|l%L11iO*)f z2wZRBAJswPS`+(5jiT6muItFX&>eafax8Vu>DlhRA#|rDa>vR!JmWD{79O|yC?|_x z7!KmO77CrW9Ey00K&&kjfqvdso{1`NOOBrr6i(wzHN9=qL@nBSpcI1b=0-+tJKa1E zl8f<1x%~`InpWAHn+&$7WiE9NqZqC6azmS%S}L=_3YWuCP!cgFPT{O{b2;CBV#VJu zfAzZYN-Yti>fmov3%!xewd!f{FsiEux)n}_BkAG%TselVM09TR^+}EGBa&F-w~~^Q zRP(jI`ua*qxeP!$q%HgEH6a9M3jj^q?h`eh6GKDl3twTZrih8Q|JE_(Jww)qjpuBS zM|DlVbZK6;ebAMsyCUU4RArB)_F|ki|*Rrau{p2OIzS|Y= zu(@1(9X@x4b{f7%salP~6zj?WjDf1a?2E>J*OSBWC5Lr$dQkEt8E_&JM)50U>j^xz zbHlj8@ilmG>U*;LDRhLpKuSiUE8lj%qaF6#41RK=${QSHTrCoe;YpPJA3uMD0f&Nm zu6jmrSj95x8QB%>YU|;R(ytrKrMbH4VdR`nr_kv$n~X_2(#G5)CZZtFBfCacycNv3pBD= zv@)xsWce|Bo(BhkFbOYy+v;DD<4DEBa0zSt5jfYWjYC`UPB%J^+>^yVg6>h6@`=F@ z7KA76-7JODf$uO!?JJQi+D{bSn=<6}gzxo9s+)I}hZBV82sk3^mOW<^*Vr^34-XIJ z$m8xzO?E8TspIfN%gXuSU4uf+js4WiPiD%fl2fSr2L;ME%KmI*VUit(O4e`A&l5ka zeFvkc$^}R1)E})R?L9nlLtCV$R$yie!K!+?+}Cn%X&U00-RjV8HGcG z;-tH)DSChHzUU})P`Oo%Lf)5wFh4*4&6_Fpb9z}4u*WkLEd4GF4fCweVkFI>=H#^h zb)}z*EVu_#{On8d7(Bip5khnYPV1?Ke%Fn1lodJcJ^%dFz5M5L?VP7dsz%YwY^Et0 zDFIoM-A=^e*v%+Dv#g@1s2yhY9JD)V-*dlqHTT0jhEO?XT#kd*hr?^YY|83LYOJhYwi|mzh35L;eo+ z0nHdA;5_g9JJselv8)n}C-z70&Zy#NdnR#J^e<$BHO@ekK@`ifkNxF)3G1v*=-wF` zk-%y3i*Can?QrM@>TUDJ*ZafUDohhF2&X4<+4aaph$zY^ES?;~G)p=mW&Ufk#ISK^vyUCxF?Spw*i;l-f6}?S{ zXI$&O4@nc(Vo}RsLl2?~BvQ+)%CcBl1?DB~^hFNxEmru-av3`_S$QhU`z^kaJ^7(f z>_0xip7E9NhB+*qVdM7{wGK)Ws})N@p}V*(n3}k6Eg}(XOg~ z#H24fYqmQorJR3yU6PZ_hL-;U_V+P&26BPb1FPWO@(c;l(McF?i|%#G}TEIyW+aqC(4T?dU%{kb!N>n|HwnZ z>hLfJd$FCY-wHt zd9c?Ljk)O2ZXXiHvT!`{a~4AP z=&XNmaa3}+i)XXlacaiB!5B!}G1dD9jpP}UgPD_#RnFOqD<8eBe(zcN_wFpq%L*;` zrQ5{EkZ#busG}~>s!2)E;3KlRv!G}EC%b^$xq6w8lW}OsP)7BLhhFBy^l;4m0pMvL zxeKIb^dMDE)_wA(_n)6y$il{Le7#Es+_%9VF{PQ}6A#q~@u%iN?Cerg?;A4MjQMNJ zPB0o@yu-PJXEsHlPU!Hl9}YE2t9nYp=e!f$@l!{<>s(SJa| zBFt#p{pVo#*v7_2z04AgvTr=zzRzEJzB{N3U|7|_DK5*=g8|K$G4$tKqyYA`!X#(1 z{c4AlcXmbumHSRNiLTqjr7{rFTdy7xlv<2rr$!_sBmjU_!DNuvHb>~UsyD=|%}hu{1C_>;Lm^O=DnCsOUp{`|*c~bx7sGKt?e-IyUgwc@HKsDeUi=XB3wp z;Ub@imNGHV>_DdurlTXT;T>zahJKFR?^9=LluIxOI&^d;j!+>p6C&r_ED()t_+YBK z-tYN);8fM#67x_mG`AXsDnuRI){1S?Q&TGIvQa9;g_sbq4#L`~Er_GEqyT8sKICvSXQ9#>{;uNCOF-?({;PsE?ie!dK9d61gl~ z3=481H2#_+p(rMKNx8z|^oO5pP11ly%%2B2o0*d{uXVHT6K42m zg-@QyUH+CHq{CS#_FP(05*OzNp5GT488^c>mdl7X$>>gzh`11H4_X;nnG)=eTGb9s zfZz~GLSEfS`m1tw2VYTf;}HAWXKxaR?+lDvPxx0Z^P!`Dmw%i5PWM#4Aw5~{t7trh z|E=wile3yC42s0N>$`-_v%1j+V{NP^qJ(%`ghDx& z*0^GgQ+PZCj|s=a+Q5y`s-vGU6-%?RL&997)GnF=w~>Gp-(jUO{PUTI&y5?mzsgZ} zF)QYW1S?QB=}O7UqTOeF&rkVbWd2D^3=E}JRqk=VNNtIa|Ct34hi=94j7TM9|6OX* zdb^m|GNk9zI}eXeVCj%YuC3mzBw|~dR{8ayRd}C)f#DL~I;e}$aVKr{eQgy{v3@y~ z;iv#_?$}7hY;W#yK)0U2=X3-6>Qz5gE|SQ%bC^SVbX74iC8Y+gP5TJ+JH+tna^#$3PfBt!M1={-$yXBg5bFkdq3vdRAbvycW@Aj6Ut;Rl*9Je5 zK{?(_5?3OdZ~PNKICtNZa^6gFtUL_c#mqrBLf6eJkxuMOw^-k~J-7DZ4sx)*@y+WA zFJ4j6>!tK!l7ftMA=QDqwhqjyjf$85wPfQE)Y2A(1Rcl-qBx$=@0@+h|9BQ(jrsRK zI(@*`qGqyW6}|kS$1uPSX-B`Xrcm3|TKGNw3fNT#eUpL}@_qz>6IEoWSV-Ii z_xQM;d1n|xMPl83)L0HbXmtnv;dRn*FA~e^eVwc0zrP&I<-Y;^;}0)>?&9;!5pcWz z{_Em@_fGuZAN~LS3NVC$M`g+&3I8d0@-IGrtQe+WvrHM3{P%Zb3>6_*;}0$#$(vY* zEv*d7kQsdaXZ$}^694ym{QuPzcpCD}2rNG!nl6nh0cje7OPd8d*QEc;u|IDJd9TJT zjCXx%hJ3GzhDLf`#EuRaT?6>{U)Cl6`bBf$xg;uebYNYbT?FrD)KG711KX!^yy>De z=7t730oMp-oB@@HYuD6IaT@$8qjzNP*2id`sFQC%WjE7_$3B4b zU5N+B!*b&S$jP!tVztTD|#y=AxFiXuh6J3oax_pV1OWm|N-V+RF@0Zfk_yvLHAEkNDYg^81M{h&pG;oB^&jR6U$X@Y; zV{u=!1`c@9^HkbZPnd|RXrXt-{P*AB>FXJB^8MR(vFqV(=V*s-_Xew8ExSfwGilB{MTF|2M55$#@A1h*iXGYA8Fo(*BEq!@(3f4&&X~H~3}4jT-nNR|Lktuf3Kq9=ua3AMw>f<;A*<5$Bq z_ar4dsi_l3%iTR!@@clPz5)Bea@ga$ZDk`Ygw1HP$WHl#bwY!}?nibrp5NnJZ22Kq%mDw=bd;p0 zl)4Mq@hn*W!y0_Ub6P*%*HjiHtRX*ljlDf4Qk!E}@u`1QWC{epH%YZ$MW0Q3@v=W| z%}~m*oT>gG6oU0YmzIvsa~}*zL+$MrfU+=XHFSGUErbKub@QKIB@b&v?(!IQOc2tY z(Obnld6Rt5ax_Ve09h|t3qm^CVPny72MeE{w0^;-2@~Bcw_on7cI95vOx&nBmlKIL zTuQlCpjjrDe_?=a*0X{8@D{$5?=LXYf|{6SRpD{8l+l|Z@Atm><%CO)-<8S!J6hFp zFwLKRZAIfDvdU>Ou%+p7hH%*iZTQN~o9s)tD)rquClW5^Q(G)jgv|r(%Xa3y5-IPU zkSl}v(+a-~ro#@mV!~G(R&KGPqNDEdldv#*0FHoTFQXUaOet)>Kv~v)Qc1{aA$Nh&OVBxfZD;61BLFlCdC{jmWl0yEx@-pm|DS`MUo~; z&3GL0`(a?Bi^i^EGgunwWRJOi`b^J^&NzRj!Fql$;Tvrv4p=2x}odN;zJC8 z(>TjYN=sD??>yt=v;ezJf?%a6>dzl*1EEBAqYnT3v0UC?LPJA&sBZjZq5%&(?)r#L zA6|Ctv&)x1prymRGJ`@wT=u4SUC#nvyr2=)hije>QX!{%D>=E*ZYtg5$MHP2q7+Y< z1%|u2epVO{3k>$>9O*w*E<5m81t>+?38lBUD7?Mex@$|LY=ejUnKaa6%v5jR@Z0e` zP`k?*&SqiymU$2DdafYS2O+-{3{h3TnXfI6`&kUn;lY7OX9(+f+NhvZd31G@nAS_T z7t;`UNPcZ(mM2skP&5wl3N?i$*h9e7eyoz}&i(B^OwTWrTJSad>|r%csp!hJ{mJS6 zk!cQs(2)AH@a;Y567^4d_pThrhQ)+rT9I)itL(#zUsmJQY>};Dj9Kv-jF3XH-xz({ z|7i?y#3A&&c1m47HRbvL$grHcTx3|7q+z={+f%JgditpG{Fyk~Cb_NB{32H< znBUM`{xYrxQ49uB>hilECnsO?miznptxV|d`u+f_BR@a?f87{9#@ej7g+IcR3SSsz-i5JSW`1N1!bXAV9?74 zX+{nQ$RG+cm1YMPJM**9g$-DY4`wD{M2g-wpg+ssw_)=tl+@tNsYiJpD(3(<*aYxkj;`^l4Q%5Nd6P%>C%qBEG{ z-Ku+YAX|6rV=H$9t*1b0UujQl3-{q znmywp6md(5{bQW4=W|y26t%CEr)YuQF7VvNa+ok)YR0<~S@6{0n#IeMNSB>eNUgy3 zku?K!>5`F zO(XKo58|Da{PvQr=r1?v>py6qkQM%MOIg%J^$?WS#y|_>%T9 z>*zrgZ>rFt_5k~LC_0N5p{~#$tA*rT;nQvykv)#C+P}a6Az0_Mg+}Tqe;3=gyeCCg zfU*Xhs9>FU2Y*fuKcmANZq#%rB!(md2>`aSm^u|0&WW!7q@*N98c#S>ypM}c;CDCg zXiH4pnNOVYIMYPa;$xk}ilmk!=Wr^tVDlfhO8s6gE%f01)tk&L#a2J7J@Y5BaW}UUNHeAGoXx;F zVXHPnHufb)_ zTbOlltK3Wc!>}^77Tzsk+gYn9Woq&|i0k)TNk>(hmvtkKr+8GV?tzbxV&M3n_S%{T zr*ofTYj!xs#kE3HE$UGCmVnE2^62kvh|Pk?28-N_;}d`p1NNqbc^#po?;OE)6kag? zT~m^!A(|sovA=GNQ|RGl+7 zDC)vlSyoWvHdUfUxw#uSOa}@%8*llEVf|zjodV>(J!Ms8traRg6hHqG1u5Zli%?lN zVpI5ByKhMJve)@%SQTZWo=h!vr)*7wDKRn>E%x<9;K75x?*jG_O~GYV<*c|Ht(qE9 zW;FprYxJ}6v|q4ef>WFQ%7C&iFfH*aR1d)Flr9%0q-BB< zR`~sU(7#_S;-An$b~JT+yNtFUFGDPGRn(8+zWRe4d2B|SM=IcuS5MJ^d3@-H8qWx; z_&@QcuMeVrXRMU1Mz(@SC(*sDX-RbZWudBHBdY22nfPr<(dn!!kvAiqQ%dW#6%#^FCrRz>lIPHpV7YScg1 zF<0u`hoIukNQ;QA>@O*&Wi;1ftM1VjM-``phd1=}o2=c2%>V&mXc#;G(|ig$91G1p zvx)3?TKR}V8Q}B`n2*~2F=qYb=Rro25hneEBorsaDTcV<_v6S0Hm_nu*2X57$MFW= zp%S(lV5>Rj6I1UkF~Ck^b1-zUR8P4QuSa)kVzlu9{eE_KHY>CyxV1sr={6-7qEfEL zQAoWE`Oc{ef{cJ#WVm|Z@nV=~spj+mHA+)$|Jf2ia$#7Lx;qUdG@?4B_wTzy06&c= z6&mk8L4}-QMzevr=~_c zd->M~N-LBdV~8IfIrv(`ivm#LN>ARtoe`Pmcj?i^u>B2=_(xb+K?Qs3qkY-$%3Mqt zkad!ZB!=k7W>U{{`w~QrMTV;E4M%fz_fGkK%f--O;d|sOQRuGvdi}%36v3n03cBiP z8f$sj`4L-tybi{JqgG=*7?-v{c3P|hN9}62IaqtBJ(YT1;$I#|yB4?~-{u{1$Asz5 z{rZBR*eZy!rH1w%$Z|Q#d1!$Z3to@shYQTX9vAG`tf3$FObX)a92^_~l-~GE<&o)8 z0veMx+C^622>iTfhYM7N=@7Gr*Ki2djYA*e$Rj4s-NOQQVk3px(Y6x#eJ&kJ($o4* zovMcBAx&>g>>1`Ggv~Vl}^#PeB=-+Tmpp49Y$2X_a!f_OAiT&jmFzYu0lt=JxEY21O`!!GL@XY9HvqppVQYU@q5A32eG zNJmH80U`61bo;L>WTfnx)O7tq-y|NwBi~XScZ_|-2Rppy^Y>f0iUFDvg#qi9Z?*Zk zPHo4@7>p7cY3U7a+A1!E|LbO#K5Z>I&I2uEZMd)k9-^(5F@A2kfKwS#8pLBn&Ct1d zOX=ZkX#i7L+~$X=335_&55G132f5=V{)*MrY4Gv~wwVt99u5HQiCb7^yT*~9mbL-q zB&k_h61WfXTpD47y~&AvBLd!!sXO>jPAzog86pnhOU2_yy)d!8rO8p1Sb7h=_>ZjB_F9 zE%~h-eo9J8E-sT|5&@4BwRIL&X037wkB^=}l|q@R0`&ucr*x)_+lN@qf58f<*07aU zCG{h#;0BHgZmaRD%vYsj(RII+H?<^;y_da&bok83$jJN;z6Ew*Z|yNA*{o}vx-!^iEl3$!JQDrMh#d(j@vWHVO#fn*_2pdKg0_Jb zeUspB$5j)z_gN-FZnv6Uyk&!*;jQ-9HaEWlRii-0ZR^B#_&~k)v2m`|?~-Q- z3g7nst<8GZ!Oj%8g-iXbLxsq(W=^%Rtv{Qy`AIMkJlzExl@wVLo)>M;;> z*=c?|0m+%>tN5IJJhRE{TCZL;x?xejr{G1{#s$C(@$_H@ZB>1!5q6P4Jv(s@@K~9j zht>{N6=E2FoShwSv<7f{o}UJSZhHkA+m92Xi%QGD+F;e#k*`4V#;jp^ae)qNXpzR7 zZiVP!r!C>L-^)HYoDMysT+goUOVJha##fMs^k?{PZAzj*_)V$XYW3Z67}B|SMZX(r zAL@y%de=WHj{J*1-l`~E+Lkfo@XeaV1+f>|A`lv!AtS?aezU&baA&m?)jd|suPRvG zk4Heb-3LJl#(NRO^BtnVp;NDsSqiyW_VH?;zpMM~AnbUQd4!p!6{w*+*1TAtycL6~Og8ueTcBpJX8L!Xursv8&|6B4o|x1oX&5PpVl=J6zRd#6UV zwKw>$zpHz%Uug_V-KWXNZri6Mns0>W9eJr+rU1`@+yw=$rO3Gnvz@34;M?57ZN zE2q$sS`x$*Q(D_XT353)Rkp4Rs$pSS0Y5R9@he@uUsN_hkdpi2Hqh{qx67;Q0T^y! z#w{FhD&p#Hy1cy|Y1N@w^;|~_<*^SCx}eq@l9Gt84GkG+(~Q(~LxJQ9=iB?rloH+s z(fx&pk+7g3;|Oi(y2y@4+xYA4?Zu=gXGGC4`aAO~iG||vM{fK>MM|*>>-nCqeWr)X zTCd~!-MWoi_Jo_H1?AAIL_~yrC5XwZM*FpfbCUd~`)y z0)PqS&tYK*?#+H*j>ZsCJgJFSH+JXa4+V&IVVQh0Yjse zSxvxZ+FJnkD-7=-RG&IZuii5=j4F|PM{U%$T32x3k_9f=X@%4s$_aIf_RdbL=Ae?S zPT;IU-KE*<*H3KcWq48MLq*~AexIXwY<}gc7uUx;)f4NCW>>u&zb%BcbPqZqWCz78dZl4;zp^@(E+d z{3R+PW~4&*jBmzRMMG8`g|>VA!zD83cI-@O8@VfrIQZe4;0A5`TStcnqVk7v^h)%3 zpDP6{=Px<~`#L?oR@iIq>B>;8_jU{%HEnR7za}RqtsrKio(n-*)zPH*^70!hC`LQ9 zv~f_0&0fKp+c_IX&Ia)!)bDFZCs_Y1E#8J zqnS&Z9xz2swuY@%@atuY3B^rdJ>_tp%pxnEQ6hv23rj<=$#HMt^zzz-QZF{xZSP$j z$N~BAbRXCO~m=!T< z!M2k{{R~xl`<45nkEZIp+LkE~>Z6Q-s?457dTpcRr2V?*>HMJ@Y z@^aAAivAVMc!!3i0fPuSO1`n!+cZA`CUs#$h8$n6Z?zq+aBqyN68sDSkw*YI{l3HU z7j^uJQNw_a+#3fRuB$FN?bBxaWaCGC-NpN%}A~Gj!Rk zF4U+%znkQw1C3>ns$f6@x-LhCJOyH7`n1+)B9M%o7O(hSBA>(G497}4;W|TSf}~cD z0LV4#)->4^DCWtl#Cm>m{5I{c@@Hr10kVzPXCAYOb=x zcOi4O#YD9>YkB$N+4bn4AnWzGWj({gEuvJdtsN!QO(0517-z2g5P};b;}$}_syw!X z53=}VoXboMf+=xN_0p@J@$#xCq-DUld|CE6^w0#~MB+JORb23y6tBJa|EeGw1S$aL zYOtHdu*WOcNU#a{Tt}Yu1oWP3*etBSbT$Bgv6`$%G@aavacGV0giXb3ef>p+U&^QN zym2-6%r=lSduXq}`&W?dro5+q$rN0`Y&Mb4zGE{ofB$pZ`@tef3);I09QM^bq24=p zdG<`;$~A%B$2y0=7k5nyPWj>jxQAeMyL)%;RT=i`RII)>3bpTT*2>aUT5q}zljFv3bi9izGH!$&PJ!+w&sO*i-A>EJX0 zRux(BG|F;JgB1*WY^D=>c*n(np^ruX_d| z-o@N)(4Uy*(q&^!RPRhJ{d|`#`FI|hrpmu(qdp%XrG*ZFvySMWx8kv#_t$!jq`RToSs$l9DH? zh+jBDY4u;JneNUt#w_b5}A#f+aBV$0If{0n>Qe!$w{Qg%|=k=gZcRA9v~t za_k+`f*ouUGh$}tIbG_NT=l%FFn0LnbG2#|EzKBu(5b0L7aA+Kplig*;CmE68Kkeu z&-%l5T^IGHEKG=~r>B3UA+X0&3qj8IY~g6%{J8xfdJF$HRn48d1e*?@(;+V`b#?QK zkCNSSg1@A+tDQ-7>r>4l1PT#tkP|RB5C({L9Gn{~qosvMeJ`mMh_Vd&{JKSBxpIR8 z18-yfT$!8Sm0tGqlp{~HKZV$YYOa>HtGls;n4jo=h>kkNRAc{5jGf=F{FJi1T+SwQ zAK$in(oLYaF*K>L#kv~nYVbeCS2Xqho`wmlv1PKz_661P)+vt#_1M8d`306k)34H1 z3Rs*PrRFFX`{7qaumZ1)qmO}UFBOh4P7a1W)&7L1=gFoxCI(J#D*|5=6SMss54Yxu zDJPLI#lc*<*k9ra9sGT_t*uQgkaD|Q+@$#Wmo^2rEOu}ULL!f3WO8zHs!n*U@MZsU zvFG_2pQ{5Dc&_)PmDJ#ai;Pe*^C*35j>+45l`t#wx*C(R)NzqiY6&tB8C=4McjbX9 ztU!8P11f(b>(+T5>@qM=W5hrAQRm9n zjlC5C`v}udnEz?zefOJ5=%IoRTTw@Td<>KL;utDiLShb^&voj@^{0N7eB|WhS3i5V zl4hE}-ncz)$QwT+Do{UiKej+l{?^>AA)`Mq>=jFLqQ$;(DuIS8B|E*iMRB}hM zauqg`&$?a5jM~)0bG8eMO78J}Z*8l{nQIvFzytYv@wJg+>X#1(e5{Y>O(fL3k1maH z6liwSO~hzQEp=`|2ZM5qAD~;-|7>mp098eW_}a%??_bvSW8>MgsHDnnyTA_F>d_{? zMzpaekBxrfL5oY|Th?{H53Q5m{7Fsn@+{I+35jDiar5J~<*=n(7d`inVKtc3JyT2W zmMhlINZ9%rStENSrd95ja}3DfSy@V*@i|7NaIDF2DA=`v>Oa@LRQE2zme)f?BP;zm zs_8P(AUSw{7S}?%h@#1%yIdG{d&>fH`l+UrSNZvmbE&{DF0*h1=?#DK{SPPBz-Ce_ zHk1XTTzs4_z`@ErJsYLb7qS!X+L$QLVXwT&ilCq%ifLzUL{+Hy=exhs< z#*GGjkibg-080TwLz#xnJ}x2nS{j<_{wJ9eeZr9#iK?56nJSgSz8 zTH<$--a{fdTs&I>8MLol%0g>Rm z#HC!FI|zJ|@C>mpUt9>1%tCfbS(Z^&{eNSu0L4?USfk&s!uycuJUu`yn-ecYf<@S+ z;B${UB@`yKUy!!l2u^rt%1cU$B6MWcB!Up781-9>MG~8PFIn-Mf#G^;K;QPY9=P8 z037PXc<2}@&xe!`U>nuc*>3OltEN1{a(-c*<>-G)S>Cr9r#Ro_n7Z6Q(9?vH5|U?6 zb#ce4w!2!`BRMs$!sMjLEW?mGx<^O@jJL^hw;_jF<*|~`n0|!HvHJ(DZA(Er_*PC& z9CxT{=1-v_2`6Q9Yh9jRtXLX5-hqsazkNQNlEm^*n(~dZw;((qtp1{3|8)Hf-m8vU zgYE zgb?!aZ@$OV^Q(t(fj=fX+N{7op#7j)v67}jPLrEi*!G#lmp{JBrd9h>s(dRaPaVfb zg*{c|Sz1+hPNx=z_(p;*|2_WY&-$;Ae()?nXC9L?0Xc4aY#GoGKEWTuqB9{&=x}&= zJ{h-isFkH%P(T1W33lcpb=zV%r7O2!#>HdUrWPMx?|HgQ;(WIQ(w#pb&NfU0?->{k zJT+`vgkBy-Ciu#anyA>3_P?Ug*qN48Jqael)=2|I>&$Cy^_@xptKeSFu zUHPwtR`iqFKm2UWWO%fsl*=9u4>PmC@}Vto)L#e$5oFIt^C-Q3Wvu3u+I&m2XZeJ! z!&>mPfC%|PQIT-QW}?c+OV)p3rLDMt8f6>v<;Ga&?j8@@WUep54rXg{>eSoYY2mFU zSE;i(#+y47?5|g5)n2^#1#Z>T)7~@u0s&%T;*Jhcs4`7sXJUQ@MlA4@LsI6eRrteS&6jIwze%P>P*;Yvg4@Y8!-}ryS9aM3 zb>~>c{%{y*Rf zd<+Ryd{o8EK`r)Q?&vcXDE^_X@+Xe`_Y1??xy=H1&rkAtHg7A7OAmyddDEn@u|9S#MMV*)D;2i$hkgjUmGtyx%=gj2VFB|2C;$lpJ`Q%w(}3iF zvUEDSjJQ>&VEL5BVE z=;&zm@%pjjxHDa`#5x?g1YPWe9oOUb(w~30b_!0qVwvg<{DqS`JmJ-0l%i{BU|?sx z58+7JMo=Ba9~c?|aa@@gI`)*n8EL-na_}eVv$&*z4fHWWMPpbiES}j~6(L$MY*JW; z9mfjvue41^RaaNT|G3hfVg%wS?IfV3HV5SaxeG^^dDe0IaXV0sL&u-^_^6{?oFd|w zAy{F&S?P|}tc>APlzu3{JR8WMSLv7u!?CPf8D+DPD9z1H8$|6$iJ?uYy@+>qf`;iU z$P-1B>p_D3{{EUcuDkaB>hA}a4mJpulEPFaB_+YNbPUue6#Tvi4_*V{heq6k^_T2g zo>4~}zE4s|A42W>>*o+Oh4!uz?mVpu#Oj65_QFd&plRiPyvB`4$W<#8pz8-gmv)ZZ zh6#WefglpvuMKlPeX4MI{1%Lk3n7Q;8DGdF^p1oX7UtYhv~3DZyuiw8sJ%9ilGgR; zk#}*Y5cNTn!wo&q#9`-emG9i?U8X?o`5_JU-E4DXFMum1|2uI zs-v$LwYa?*WkriCE5}P2iE>d-6a?~}>) z$sDndd^rEJi!D_lw)P2GTdXu>zVH4&JgCwYWqRA$fQ{=kH+$iBl5Q_u{CfgLigzP8Jdymfhn5@AD5-IDf-Nep(f1 zIsNOrWsC zVwVDjHaHCurr*?y=hgf0Nm%8Wi}RrGQaVC^7b5`Bs%Uohv-hcUSi!+~R6^kSZvatZ zHZ)Pt)d7s=keOq?z;(+AkK`N=HeDA@_wg>{m|n zwH$2hI~^tfo36@m{!C92ys3!wJnf1Hev@01)fUS>d0hD4Zy!I3Jqw}kGM}i3Y{cN} zo*xYE_*~d8bgFDuG|bxwnas#<{9LhW32BJ7@9GHmJX22%1Lrt&wUX}Ufxl&}*vM+A z$b?Ds9yeq+*{Tm-j6bTTfrj@0=q^5_1Ip=>r%wSsxe9_kToPttX!Opz5(#Ej5a&@^ zXK`;XEa%tGTal0Z^h+ zciB{W%c=@(RODSCR{}+z^_S{?r9hTE&X13WpI4<|IkZ7(Z2CA60I6eJu6GECfi1@_ zjy>|WL)28Yz}UbfUz_(4bII;H?6I6XTv zLntWy!zmKEPd{a7n(T_HIPk7#8ax%L?HKq`Vq~+nYso6t;LG>;(c}C4_DkcZUW-u~ zDiOr_4ZasWdiX4Ikvo1|czI3qYJ>=A2TT$xcn#^4t2t$O7zW|Eb65)tV`?RsY? z6?U7Lb|?&lb#Gi}9kvgj<+aLf_uqge75`=A7QIfTm}m+K;fRl|klpIZ2d@W({X<#S zHmVED%B|6{27J!C`3Z~s`25=H96<&2+$ay+N=~%h)*D)|T}}`(u#~fX>>cKmCJ9cf>aXH;Lth z`-A*6{mr*fOGJsCfjh48dwifc)yxpjac>&eS&h{OEmuE)<<-{E)b#PQfq&jWY3O{D zDE_Nz0HP9TUAuN5&3geiVG~lZXDLAB?Lo3R2x-3ZiO4LXGqm-?XZM6P3jXK{=d~Vy zWdJ5W({`nC^;+$%8y>(|(y7qO(dkX@U`2G?BO~+RdArkLVb*Y?*-XTN!SekTEH--5 zY4D)H-Kd_0NCU43r!!Jx1_N9<`dR~H<4i<`%}nDkzb|uvNv|P5tbk%-fWFQu9!Pl~ zA0K)uE__~+kujWRj0VEOHK48uF06t3o!$LtV_@LN0xjX2MR-XU{nmEFA%2sS2~(QB zY-KYBZjs@Uifv8g#xeN7Bz}^6O>WuKPIKWKw)~|fbiL~qtLSW*`K|*#7K54Pc<@D+ z1@bh1Jzf)-f1(Rd%hvgd+&dM5)fmk3$7INHOu?ZHR6w(#lCSUH&6k)_7lF8=vTMU- zl`-et^P~NktbcoLAnRizD;UN|D!=@SD)_WI?)gGQZ;Qhes5CY5DyQMTe)3Uh>v)M< z;b_al!;RmrS`^FMfp&QO_Itl}4a^+Ayf0Jr0cTi`&P2MiHq1#pS=4eo?1ABXH}YgN zXr20NpOE2d??F~^M7F%=Bi6NprPhd*c45+CTL4W1ee3NUUdkup7S{4xfnSbX6^0Xu z@eAD#EX$}oVzxP0^(Z0~jS756w(167in`EWuJi$;Egi!z+0BhldB>k|*Rv^`8Ii{# zoiSw^9z||U0Y~n8zTh;>nFwIv$HViiDf^=GFPxm>@Nzh|Mwx+bV3~$*hVn6o$T-^i zkoYiKxom&Z1fRBsdy<7WzOVWrIC;v95oUm{#<;Ho0DW>MD20wqYAJ>Ggzt@+-A>wEKJF+U=IHZzd2hV5&7x z(l5Uq z1~~%X9W3CI75@)=?->;J8g+YO~=(q)7q|2uM!Sgr^ zvB?dRGl+mllaVGT$uv1f;cnh=r)uiX)UB!cHq)Q1I(SNG`uXo?ueE+_Y9`Hkh=P&v z)+q-P)>%v-hoI#RI@+wfvGSeWH`th-dhYj%7vzFVjTcS@ZcRusgzWUvn5-@Bj?t7C*bTYa{|0659&Ld ziZqmUHJw|*R_L^rwT!#qJMlx*a(EWv0Qhr`tfebN`wi4OfP$V|n=CGAAx@Lrbb=oW zq|w6Vw(KVhdU|@|^n2O;4Y5u=`!g`WMzk(huGP*~;ga3TI%HbEikbY#b84Vf%6s0n zj)Bzn-~!s-AUm7~mxDcIW!&uE3&SVJRDOhaHR+KrC6@5)*|V8HH~;*BZiH)m%kg`r z5t?{%rYz}&f&PB>kcliUuHqfPz(9ja?JdgL$*@Y+FmP7|UjkeL2*B-opOd{Ipw>JM z_#JLN z5smJ}oRQnkz+5StWqP^VFD`y z9Yhv|M^ftTk6t`uT+U_wUg3C9BPQ#3go%+eAe)Ek(f=E#@V{tq3O~4&B~t`zx(yt% zMA5kNd*U~cl|O}REG7%`w)UnpSs5_4wyAia?F_m{SOg@@7us*Hm;3tECWt9FcuvHB z!WpuROiWO1(~Du?nX`9C)K;=T&qIIuDaIrD`jBh-)(4Mluds{LRS;8Ei?6QQc|I7K zF|mEZD;K+>ZaC>Q+U0)%E_&(k{N(I1+n1 zB_3Jrt&*2##a)i_1QerqfZI4ZvQ6%{GWJfAb@wTX)h9z9hV$yTSH_K6z@3drr(%lky5Tr zbiQ`b+2mmP4?);9>ZlJTzDsoq`wx@O`-=CUH{Pue`kHbRX@b9Rc zzt}H-XugP-K)d`YXhRr61D1eJ5=oUJ5)TLmUGWG89P1ew|GHY^3;SOF{1~K`>@R3F{A5n>Jd{nPr zT+;NJ9GmYG7dMNGNR=ki~_l$sS5NKc_M4QD58C z)z|$Mw=5U`FFoDzVyb%?V8}WDm_~K8e7v%lJp2>5H;fXl-i;MS9j}=c4QgCx0ZP&< zeZ{evskTqnlwD;_@^({!MWfb^W%^|jPO7BxZfmW*_h(6NUSfV0;6W#OR2hUVDz7rF zIX3yUOF@hM*&xj96(eEuUW<&5tv#OqF-Opmqj?g&PvDo>*n2MPqWJnNVBN-Nw;LR^ z)M3A2Mi7DH>qjF)H0|-hEs&1mHAs6CuA@M@zIxw zUC^y?Pj6-S0cEztFIC{ytp`&9N_{SWfHvbj8;4Fs!ZoBArOu_DvrEj*6XMJ-E(*K+uIU%8;#ZY#c1)>EcdGN-9c}rvmqb#;Qp| zCUeFrj$8PDN5oe#RsB}QjAe(FZ9#d*ht-&LOZpN8+|jRZxjdKyC=qUWx?XyBjI z43~zkET;@|32`H&Yus*5pOq-JO9qp6h`6_akZJ$HWIEqty+^~WWZC?*ZHswyGX)8RBMG z;jVAG&|H}1JwfIwLd=vRA&H{!d3iu~G?xz9x5eM__8wppU-z8Q1w9%K(0Ww4O&)q! z+5NG2iD`>9He!P9wvSuHw>-m?FG{4V(2LO6R4eqk?ZnoMie!A7s`JgUZGumFneEtf z@9}ZM&p_n8WAf?ch`hN<%-HmPpdR=a3aEKB^DaX%fsTW2CV)K;!TAc0cy+#jfPkNn z9bU+zRc$=+$gYl`Vky2>Vn5PbVkgO=9LZ-yaclepAf?d)xqdJuJito{3JRK?-+=76 z-wH~IlUuLoDKBW%eZ9Tf_%I%Q5mC@b_pBfT^6J#zroe*-p(&6ZsfNmjc%`IPl<)=P39fjfwi%+cQ*>ozz0KL0eCs)+% zqCOf!XC1z!>E-2_oN68LoTuKS$dBGWC3i8i!SIOhO$)S7laxFtht#_pKOE$`8&zqrJ+uuD;IZAE2>bM7T%$Johe{dx&7GB16x#Z`HGHNFA&N5 z97e)g?DTpB-7z^@x|LsOn^z)kpo_UQ^CnWNyWu{22n{bOY`j?xlq*-B#A*Hg+1v6W znuCNy=nf03_uqlUJn6PHS#(4!oI6*C^WS+@X{dQrQ2rkCI(H&3-)~wO6XC33M0G7^pSmUSX_9TF zAlK5&!-ra7AzGVVBZl8QJD)d@dO1r6m+D+tI#kzgPT!p0eEm!yt`x|*X{3doq-|Smhc&AcA~Gcw$z@fyCqR$256}6a?^TX zC_b5EzHz;y%x!df3%xA;^!&d#joT|cFG1=iqo4tgbeMo2w)2}+OcsGCR(UNb$b)Y8 zm(SlUxj)8MK9li3e}0PoZJuZSJ4xSM;S$+JB>2~n_P_Hz9(N!wFP2#}efW5kmACf6 zYketu2e-Vc&15!(SGA$xO7}Wyb)0^!hn_RHNZ4s-M(md*0AZYHGd09QC8~Y6I9846 zE_%O7O`4dimE8;Ccz>Y6XZu_PA{Iu`c!N$e)dkgghfP z+SabVzEYp#Q_IVZ;5!2m!hWpg?`(qLbi)P|_V_~EN|k-q|s@%p9RgTvfLH4p;|_-ww9$&S@UHc` zlb~SSg6e-c=ifO-nOTeUR`gjYrv?w2KF=Jx7+AS!!q@ht3np7a6w&=AmaDqF{4;Lw zLb3ZDgZ^8;(_6u!gw&Ay@uc=v#ZDHoOW!u)295vODq{nJ<|LaT`2Qf3w>01lB4Y^@ zxNViAXZXgnuzGe|7ZU>LMHhB_3)3B*QrzHBfvzuNhrOtShWDd`W`dx7Fdb@l2^ESo*FGXBo^UE}qU zM_*Hrmfgt{Mn;Gu&bGw>8`vi>=t_du_5k$K@IL!?Kb-AJY=4S^8qg%-W}gbg6@Y=F zAJQ!}YgqPz)TeCM~!{KGW)*R*|pnUudKra&LW%}~=#0PH! z0@fKzE;C3t&$P-E_xayLLw`4O_H{I3xEt`=5K%hPV*JOAr&9~*6BQG6B(zNxn2;&s zEj>A#H#*;^n^|K0*B%=i93H+g+0>ZsvJc*rHn&Gk8}!TB_k#Cd2GZs9h_Fm022Lqk zgg$QnTGD*)x9Z-Ho(4^h1^O^`rG+PqY=4cW|M>F$?fWl2@^N`gY85V@Jr`5WR~)vPtmf&vkSn+_z%(rQ%1L zD-0EyzD48?eS05n3h86Ku}zn~*T!4R(7acWvPB3TSbVyRv{O%@y zwOKPx@Dy}1l)rW``LzQU6f7i9p|yqiM{#@HSan+E~(h&8!x3LDDjEJ5q4~-Js9Os z(p;Owh|0WT)UO{uCf<@u_ZK~m*nrN|lPW*TRU-wF<9+_S?3y^#z>QK)CaEZq?F2CU zXJ&RZDyf&06nS!E+ZrZxyM>3IDcIx8EuSYA+4y_%k13agllpyXb$$`}Yc3!~*Giay zu-zRsM>NQlu>Gti;IOzP5Um5W0%|mYe{Obi{N?;HC%KyQgKq@|nt1+v$KI%WEqFd( zTp%|5#@2$Sl(z)VYr0;cE<~Mw+qGBJek_cwEVBE26E}c3gBe05IKuw?b0xZ{S69`O z)4s1fQb1)YE=I>i+dmMK++)?3nO=_BUU_-V;)%Hm#n3iEzEqQqOn8r!@b63JyhXve z`g&4VPQh7c*zWuK`f5HW3R$wgd2Y79{)%)5ML;utkNI9#XABxAZ0M1duDFD;xTc`D zuq@|Ft{*I3`1H)Fmz#jJfJFl8J}otM+GS5b!{oJ++1T6jmV5?dUbx7a4l|A>n1?FR zE4RqyF*B`J96Q4!BajNOfFLs<4BQ^4Ke1Z!gW5w*2dHIlb5GvT=}CNW27BfRcyaL8 zcuyT$!8I)#9B(k(hci-8m#8g7+Sz1Z{Ls)hUYxDd`X>pF!<2;7tsw35l|8P&XoiAy zu1kdzx$V@{=B}<;+}wA#1quz|0UGm&-U=2F)33^-27Z^hafH=fHCxZ5rl;7T1$gXy ze_e{jfvZEdfyG!-)gh)u0nWOOxVXW&Sitv2or^U7lO_Is(bhI~c=aVZzc@cMVGYLT zVmtMf1}j4UF*J>J4-A1L<_aK?mCC=Rgc;AQJYHNBIREA{K;sMB^XomrQN_{a3kGFA zJLXtqxit?*!Zn_m!9+){a=ydqOX(}4<@r`4lx8*`dCv(;zDxdQ`m?m^^{FH9L^yM5 zq_v8Ud8Sl*gRF{5ve6+3nY?~Yn>PfEWW>zyu%?FeaE+Hf@29g+Dfen_jt!(K&?`~q zk`j{HhGoD-j+dEK$hh?1y8xf~`Bkl`8)7dL>5=yR-=u|vdhmE_Xk_3Mxe|KuD+svd zX&01KH<8e78DQS#eM$H#2qM|&SiUUU`$#DKh zLFn>fFLaqt2Q_x_kCr@M#=(UtBb=t=*tBVez{l8F`4DT3c;FWlR0#q90!!oO)!!tj zKcG_%m1IWeqnEOx4OM985lS)X2d>nWV`WLOD7f<#*ooG#5KWM-W>ekg_h%rd7fW3H z`mqNw*Pow)Foc*4z^Px@k#z29r`5FHDW{Rn1bGXBF0qQ}j zM@$J*&!`!Y518M?);dpa)@lz!^^T!#wjPGy7~c>thD!0{ssq%k_Y1bFHO-EG4AVCUsSERl2%hQHyWevq<(x0HZSzD zl(bzhYW|qV0S=*z?d00+d8fXI#nEGa^qxyIUZ{bdhW_l-m~dqmo2=7#;0jO7v^bXR zN^Np1fSenA1{%qUDUUzzJAa4Do1$#r9@Kuv7?*#P@D0yv!CjPv;b;s;6O@=oSk(aQ zUqHB}gTFGSXfZK7lBe}+N74yL{LM9n>*ChC_qS+&D!aSGGx-PLd!{Be+DAuENX*2^ z$(Dxbm0F$ZRd)}MgZ+KHnT3S~w%wstwas>Q7}RWD-DKPQX=#7=DxInnj#tMI{9pb1Ci#)yKaEuIQ7q3y$7NPeqn_I*m|k`Mt0*&V6Te z!U2!-q{$m(b=72KW?^v|T25(F%Vc%VRwpPNN#OC_Rn{zoWedL1uca?i#A*s6p9z|_ zbCz=pq(u`@`%T|VQM4@lCsfkWx3p3PpftquTbvyFh&rwb%duipCurR&0M0?s?sln~ z{NHter``Ci@o@UivC;bWBbs+tt_O%S~C@6vTJ}qc|*OsvW7Q z*ypvA-0Hz(e5a|yVk3&HWsNzSl9>fR)U9zxbofN`aC1Yil6q0g6er52>5H)cjEaUg z^^ugzK3|H0cYiu`{B~DpSQ3`)p!XCMUq0Al%d1*v5ep5G$f|w!Ed%%M#aq(*%`!B6 zS%Zn1d4`j{IQ`@q;ISzL>~cS``~3U+_wVLy(aT^rF{Br={8rH1*$L%@GT|+NC}R+^ zOq4&n2eaE5((O{4K5g2Ig=$QMZ~ubKhRE`rY&oNwHB{>0u(vLa|GZMYlN+uB)S8G( z5H!$Wm0uaFP1wX!Qc{{2QbT?pPSa|SolPQPV+1W|xGL#FyaQ)mfFt}KKph5sxQMKB z(V(V?S7qs*>kor+nJsAeBV&}(A8ymFDu81!b#$-7r^8h#%ndjg=mVE{31#6sO~fme zrHf)y%i67-o%a{9n;X|Dp7lXV-|fxiLE?Jx(E1s`y8S)G-T)>z=0GHy*p+Qo?k9 z%UwA^boBLPKbCPMxzD?K1Y55JQfD=|>Yb!S=ClM;qnaQ;{VJb=1;vzKIz;liPSP9?`Q=FgF5wY&+ z92%$`_9}K*^xdo;N+YAz}tsbj=;3#8bV`DiJ(H&z9OBs20GyiTgeY=SU zrU>cv?>_(Hz8IRiie#z3OOAhWHWKP?Z_5xZtdsW-LcVS;wvT-D-Z^bZRyYW2U95M; zf5ZVSE_dJhfd|dds1liv@NW)#!j}eA*m*xKPn&1=oH2m+j!A zvH$qU!$QqJR!29$?{oR|EYsD0ljHULV!q6pXHDoRdKo$XQM#c*LW26yK59Jh;Wn(= zXHwT;eJ$^uE&0knC`iM6p(H;)40W6Pjv|M??vTylo5Y#VcS zSU>t#aAeK-g|XUN-I~v5Lunl?Sk;c^!73K|N|c0$ck6RV67eyGu5quhozdCIwKYD~ zj)~=&do$TdF=$2!%uIoN_IClSlIv1WYw_35BBzBtz5FD^ZHHY~_i-L{F~q)H*iV{X za%ziw{(q4Pkn&fKx{C=Zv%2cz=);;EZ=BsIj)Cw$SO>#^o2jJ-lZFVK3X?Iripg4mQz8^r>FiAMj7(x2(H{DM^BD018^d zP*&nTFkH+vV^r<=r1GJ$LOPubo!8n(hkc{6x$9L}Eauob(I6pTn@R%DV}xN;R8&j4 zvAum#jp5IE#6E^qjeyvsI&=q+~iP(B#wQflwaf~ak$vW zY|Llg!;Hk8r7+ik_$SWaza`|hg3sRcEExZ-HL2!@08b-o{Nqtz4??>fH4TPKp94EQ z_$&!wN^)@UvZ{RTgqrHWEoL=jzR5J$;vc=iKnLs+Lv?j^V>5f8P$3ri*_9of4Qh^L zcoVQN33IZC&3+}i=L?I9c9^KYEi(X&12xA>s53fG&S+~Iwj;yxeF6LQK}T1>aY{o& z>i6+)pFS1o)-KuN{3WiJ6bN;=@=*+&JNHq_7sGjU`ML}0npK**tC1`ViE^bCTCMX>{z%}5~L$^oHiqG6^ZQtbQ8yk~K(#$e{_^=mD_$eKDfNEVq z?R`|2^%omdCJEN@zU3^6`3<;38F#ni__4O{3gjv(D)*ElHdZ;qvQKfH%Gf|DMZDU4 zYzlRf3?Vrq6MQ16iszR0gNxR9}M1Fq$w6rupuW4s#>QH+oAAK4w>Ber!}^vQT`(>I{m;$@S3t#b{H4MJ70t`z1^u3j4l1D(bPVEz+c0&;P2ZFD@B|UjLqg@q21P+^{g05atTeJ8`$WTOJ>I zO&Gxo(hht6%@`5}a54c5XMv}dGzLvyF?+1&+j05w#ety#A)9A~Py~EWEi5qhuBaf- zV(@kx&ezuws@=?#BXjOspX3?lu=O9w{e4I;|I2>reob<=Sa5ZJx+Cyw?Cf%mjlxC& zg>uQD5#kzMTJ$7OUjj<{MF+n7;UO?sPhPvFopb;BCz4nH+YsgZrxPLQ4$jDv*Z=eis3|`8>hOg_6kunc z-gR3c`_C`9zj776;6GmfPjV55LDiM7{F&{>#iXe(zUihspVU7xGUDipjvw(Nj?Bk7 zqJIvI7+9du#AD1LnNYUNBJ*+RihclVX7+LcPCF$?qfs5sp+vmF^V5)x-f2>s{3 zg(OZEK1l!ZxVNe7#Q*qy)BhdcO=ba3Ufz>7SosgS*dQ7Ph7Ab``D+3%6dk@2 z?!)iUF|IQ>xWBBQTpJ<~u*xXxRjrX8o$ZKQ-*#vGcelH{n-k4=Fu|fUG$5&%Kpc(J z-&r1AUt7CKPEMuJ5;v6VJa92B%{+5uYxVVpU?f|7$B36#wQ5`I(jq--x`a!;K;^?_ z+Ekwh7_WjCT^-R|8q!N7yWS^0ZXuz~w}gg+Pyi7r>92tB^mpII!67drBO_MaX}USA zQYj)Uvq#Oq&2870s%Zp@=sc?e0}8~&#HKnDYTCe6l;lvS#S zQPXir4Ue#XzTrOAc{H`9f86QV`fi?#41*1F+`>0zQYjeLy6T!%Cxvv)`{r{>)9h(~iDz@n`iK2e}otTAegqV~i6`IE5JOcnBg zj&L;@RUq9ySyzS5%xFopJ>Z|E-GaeFMK9ys+T1)}_x59V?Mp~Rz)r%b(p6Qr&|$0w z{GiGt(p-8-Vn%;iS;WH5sH1nNQXG{AY*s1%1C; z?s&D0Hh7o4mPaw?#O@tj3^7pJ4K-9~y5~*gz3A>vdr88}y^x0FdudyJz3ToQizf^# zqu7Yh1*`sYuiqi{Kr<+t2(L{H7a1zaVd}}t?p2-8eH7p}w%7inw$J)eFaVF{hYDqt znmiBZ9z>G0`Ou5Fu5TJ)g`Au`v@>n>)NYVmgT~eF^nD;L04+VTyVnTfqVOAau*3B0 z3r|e=`PZ*s_e(h@8M z&e{dn8jpj`Ow;v!8JbpJ{Tjqu`L@)eC{c?K2-nsH%^H*IAz!Gu43$p5(!{t+t0@k2jT4}lxN-C8$GGjn|!@W(gCYLD-~3;5&fP{RCvakzW~et^<$o)lu+b6v7h#W{x6 z^6xKB%biGA_BE$ZHYd*eS^1Zl{;o`7 znWO(V z+T)R58YCp+VSD@TTxK*u&1*YLEXFH%Ac_`~w3SdP@BZ#Jpx!&U_5eq^8<;@8h0bUgpG4{4%K6Qm@(IJF>|| z*wugW+PWd4C9_y9SsE>z%bPG;HD;bBRaWJ?6^i^<(s(~3U*ism;tik%mESdQ{hHu& z#1iOH4LpB9RpTe{5!n?k3u)Z2G2DT8(eBziN&e$Qr&Ykg!yZBH?<)Y3Q3_9^;FAhS zC_4*_bX$Y6mw&z)%gQ}?7kWd~o?eLE>U%CJ+(7!j8D0am7v%bs(ZU1x#`pIR4qB$6 zf{c)(t9*L3@v6%_J-OPL#;n7y>74WX8uB|F`dPv9q|Ac97vFjuKdt%{RyDV@h1~rt8m{w#-ckt8O43%xWnD|h=i^<%*&hw)R zhKDY_*C@==yuW8h^8G4L{pPtveG-EZ7LF10@kLT|OeMYbm_ogHF*Q0eG_|f)IAfjq zd?$v#{tmT76!bt&I!4gjI@=@Ys$8KI3J2v2U=TLG1;}P-i#*ZZIay5^94J z;Qkrj15d94_bocgSA0N1l$agwO6^^Fdr%}Ss=Rgv9rRfh08R9?5v%2U69f+yznQ*_ zRiex0mOi}lBT=9Ecv02EQ&wA;%(T*Dz$u3a6FJQUk>+{#>yo@(-ZPj-u zTc=B$TNO2+7eNz({JFzi{^^%a@|*o=$(;oA4s-U%HkAU&{sDaYpToOJjv0=wDV}8I zp)y#gR`14Km1=MLc5y3 zk8C}2z~w?cy|3XM>Q9k5;R23DT%xff2?uA-et02>*SncuAl)?lg=wJU?~?xf{t_v| z#5G5P>iD}&DdR7*wZg)VxK7>sRbQEdsNb8s-!SWK+1y^z*Fmo&2H&k#p^ezo-R>c5 z%oonq@EQ7DqTov`J9}_XgpP(w!;law@}*b2wf@I#HKVe%;RQLQZS4DU$NV1{|7$AI z5;x&AD{m=1G(78@-EL)L963~?nSYoWFvmng<1^VVD@yKg{Azr6w{+qa2BjOxr6s13 z7Ub5nTcyKO;=Z?Ad--w*%~-=v&b-3sku7?r0jJl-8mDdArTI=b@J4NIa<>-HaE?D^ zuZ1EHQ@GIV@49EdX}O14{9V&mUdR>!J0uw!oS=8N3f6yTDW;E8JC_w}+mVYyC3(Kb z^XL^T8z!0K+!sWan!oa=l)uNuRi{)5aB`wH%QEZg9wgnWM3qGXGL;hbp}OiBD7IGBtaDw`gKbd~dT+N*sNvHZd7xr<(k5X(I`L|HVf&MK+d55^jv$2%erGR8HKAY_uU{J2`PAtsHY;;<1^g2LScf zy;phIL!^rII5=-R=>2P{JP5^3)2zY!>1*C^@+(&gYj!>&Flu<7t(0rr3=H+rwas`E1 ze3yX8V(tDurN(c5D8!*fLGk#un_s8k7G5;hXuiM1cR%{IH3Km6#|Bb$xzy(fOQhvq z8AeYRKP7pI;*7A4^f!iLYdelB?Y-ItP?M9B$A&`3r31eSP1i?C%&L)Bn{C%^&aPgk zz$AB9+_h*A`iFO9L~BXNhL9C;>pi0tA&bR@ycQO$#nv1(nINSo^J?ZIVdE;Hw;*<> zd!HL3bO>tL!wuqMw#9#Uo1Ew)X?Lz%XQm{~w1tgMS5gquLgi@SZf)1}ibZ4hDWM`pVLbc}7Y8y;=Jj}eB$v^3| zlzhHpkM5}Ip(%Vj{9HUFO00qrr;9yD>`!tRq1+S{ z;J}LHGI+L6-q_gWFyNy3n1}HGoqX$&sjB6(cfAaaT!Kq|W-zV@3AW|d9BuC17}}=) zUD1N8465=pR5WUhx+EI@Oe5~N<+O4QdV?ACGdqkKotDlminHFOJdwQS%F@DmozHP~KT!-w;SskS2_ z37?6;nSW5DGc@1%+=i-aZ?Eg=Sv*rr7}QH^EK`3Qmi`6t@Y@2*>CUMKQ^VqdGlv2^ z*c6wgev`F=Et#SPkYXhEcJCD5y;>LL2EO{mfw!bP1_mR*&VpyUZoUZ%qigB#fA0d$ zoOwGX>0WDdXz#ER++7E*j7I+HOcWA{L|otn(d7=!V2NF%+shSD&dMvKFI~J?0I$MA zhv70vlx}-Gf$%<{x-_W0yu7f-_?F01upCy2*HiK>q=czyvNfuZILm4{3Pfcnc0nw3 zHC)n4dbsyHy(3lHPGWWG7W~F^R3A<~G_O|46fF(cjgGm@)TwSd{hkXQ-Xj2aY|N4q z4Ob{%jIlOaEg{T8>#h5zsa(zc(|QL1Ex4&idYaX!%wcgzY#WKoyi^ZO6pq=Mb!j!T zj=dXZeu}M9q>XABs-`@Uql1aAa`k8)dIqWk-(wHv{t67+A8K8=g~W=87tqp*^=C>@ zEU>j4m-pw`B7`XH+k|odgqQB+h;>g!y%-IxQc3l znX2s*_RxXyOMP5>aWKEY>@efhO2x>C`GdE^M6)`YeEe)=Ie5QiY`vu~Zu*kdK_r;iy1E#wkpZ( zov$y9!`+kfP0p&Bg*>RHw@#yAFHU{5_viZ7!dDM+W2X8m~}bq>iZho2kh zI&6M^^KO@{Hn)C;dXmb(MM3W^-nO^B$w29W3Yr6tBBdj|{8>5+v zoxQe;WMkyi*3<<`EvZrtyL{V!mFWyJwu=u2&GMl2nf*K zeg1uOUd9X`{@APf{rko+whK(PXGW{-SZQcz(&>5b-hHYRB_LuJ!u%dCzXCuHnsRZ6 z`qP(xk^^XKZ5wV;BA|47wj%*7#HxmcC@j%D=$CP$R<=$JW+M%m0W}dH$r#}EXuQ(v zpu}mG(*D)n!2z1svj(I(Fsj4p%b96L$kIC4T5zzlW1ytuCh|QCzWdU`&CLx|f`$ik zf}d9|)?w|{P0EI?6nQk53vOWVd3pK>QfYbA3$R?$&ekNRi?=*F&2lVATH@#ZF z2>X8i+Yv)xVfF{IOkM2efPZPpBI1fhfz5IAsDYlMZwi;6!bCI`DoZQhmft^!o>d@F zPZ+0FsVBa5>Y~G#2lpG1a%76qJX3U^`gyx+Y*j0rQT4uYb8^(48}0RoMlCz8k`<{W z`lJVON^PTZubFGJ^{Wq=ltn$hS_O3YV3&pv$YwI1ko>4!pFpK3BqXXy|3RZ+DI*)(6M_3`1lakgX1RBZ<9vCfIOvR7`ID7FNS);>wjmb$$RO_ zrdF3H7S+fo<#OO;_2Z@KM5GTiW5f-Kgp!bqF`JPz8JSxr>>lhFw_I7!yyrQAniqKd zR!&cUh*RIF)*IKlhT|0x*`C?rLhYG7x30sk%K?}x$G)wAIFfN)dejiUvtF^lsm~1J zyR&Zwy7G)XFzy*X$4nxqhqLz8Sn}qO`YhS> zUNB%9@e$(I&pG)VO_$bAtv6F;k$+5H57E$xm`QLUirDr}d_>4CO*OtfaJ@n+B7b`I zIwFVzw5GVKH=nv^TB9cC=gZ+{x7B&q;)#}i7~uL+ombPHbO{D)kKi*C`nCnQ1Xit*;lshVWM^9UR4=%K*Fw^M_?h^pYt1vc z29iTX>Q~;D_#AtecP2ilhvx~${0o?@!D2X;Z~7y{W`Yg1Mbrv;r}Q2DHrBB8-Mg&O z@IqQT#e{~w6*K58LD~-{Mu+O*x!Y8&Yy3UpJ+NOpw^q9J9`l$HI3vQ@mME5wW(B{} zI2Yt;RLA}-;+*Bp)Qiy5kAkzhAni(){}yU*LfE1_QTkm*m}$KEis*BpyT1eyXUV%G z@0_lSkBiQnMgQ32LPy|G2deMOSRJjDp6v;+KUy%BEXvCB-us|?MTtg@c($)bOKQuv zUGRmXQQjBgO6+kD_{`R zU+NPiqGZj%72HvyEll7$hUr(CWP2V@KP6`zuJl|R#>MnOL5sn`6O~v2<$VEaUft{v+Y<2Ik9{{r=|*iLm15}> zc{Q`u6;gh?=MH1SANQl}$Jk+(@O^|Z4OF}Xo(&1ukhpJ!j^afeMu+o^#&&nz%v7iG zS*!wJ&4Sf|?fUInuLIZCuc`WU!~^Sp{!y$@hl&X3ZUcVW;Fu5JbqkXhFH*hfpa?v) z{Ur)@vRWj>r1&AVroJ+-h0`4Gu~{MW*wi#}Ti_VT>`E)p%GSpJ6Az1?kXI`gcz9j> zhsmD&+Ryy^Om1|uCA^LzM1A5Z3LZD^NJ!(NQUAT;7F~E#anqP6i@rYP-SkPf&cBU${~ZhDc1hd}}Z( z^9yG95mGe&CAxW{BObP z*E`bG>(YiT+N0FL5w_5g$Ql*(z$pW5tYzCuDTf5JYj4>Y5coY3^%d62z zTPJsWB0Tv=i@XV|OjM_ZL)b)YkbjS;WH8PpKjeTKuddX7)h9|^98?AoNFg;*NZJn+ zxeiWs;iKurbAEf%r8KE(C7@=T{+NRDFk0!MzbABldjWNX_nJ2?vewo=8!4+0pmszt zXGHABS};oT6z1B{EQ#ueFW5N!j z54*2Wv8t$yp4NO98pf*nMgOT`t+#l@O3`4J22c_$kK2V;BY zCu*Oyt33@5BkMr_Hn+b{7wx=$itlX)va?(BL6o_|y9nlHu+guQKP?`iI)z-0)eaST zzdqzsK8~=F*>8>7;%<<&(*RFgY2L9DpA2aGn_AJG=g-;9Tc+`E1*=5yB9SiZ4njW% z3Y*Vu-H7q#)2zp*szp9oSjb#98&?Q8yU&uV$7SZ(4PBu^$cO)cNa*SD<45RnGaTsV zhpDtg{YG?S`-r=Ru+ZX=;Gc!$f35`45B3$fcwiRP1ZPVulPmJX6V+yMF}>P2!=WDn zO-|&cR83?f6&TgOPrr!gAjAV{_(IR)WA}f@*2yZ^Xj+c0lalWkS*YA#!v`@*m7!;~ zVc%HHm}Qo|ZHCu$WB5DR(R9!p_Hm(?J1pj9`@Yr0B^e`Z-}Wq`#?uV9;536@`r0=i zrTpWQiyEU;u!Sa{eo1nbR*u8(s?Yx*xHNOD!&SHV0a*0#-W?L7!r-d}8uk!9c#oe* z@rF~fj^>XQTBt<{V5iIM)+Xd+Kaxc_WWd0ul5q4J(p9PGLcB8orBtE{q696x4|lu3 z7#iM;kC;q%CL=DjG0@XLNsvzAClBgJgMLFswhbHhAb9BgjvFt40sy@}oy=tG5KhC2 zxjDulHdfG$zrXSgJ^ol{`%Y~v0s7CGit>J<+m3H(j{RhsVZWUl!BKFabG|@-h zOQdSU!wCneLXL&nGlOsLMrA%05ij#x-b#LXN@2b>IWni(P|*_cwRmM!cU~51h#$S9 zVN7z@QVl4?YW{8&irH+mysV4+1M7IvWohrj27~#6IBHm3KB+;dscre0H7A>8_hKq! z$Es7Im|H@jDf@r{Obnp>)&IJSB26p2ys>2sGncC(HYt>WgiuRfl} zn_bF7*4$+Id=pxn;KB0sp6Yehi|J|%RODL>;OfNLIpJ{va)}&R?)@zr>~QwVbDYaQ zIbH6(zPKS##6@1G*?cN;Gso85c@5TptB`fcdsgxdlkIw))0(N^@b{-nDpp~z_@hTk zd(Lr_M!xsEwlG>)doLl#O-i!Fj+v#z00w$g`NfEZ9=jK|>vA8YNmmmf4+#TqY6G>8 z(wnp=IZz`9*!2q|KMf2F67FG=r)*&14gfg6?i`c>^OhIz3p^nRE^b%!O)mKKxOR@;3-qR+-_j0)wH>c^&R6y#`w{ovoG4UkrAP=n z*iKV`=}BljCS*kE4(Z5@$29#LS;6ArswFF~#s`Vey4l0d>1IK4KBnWoF8=lCva;QJ zHUQ-wMJjG0ky0|8#n*)7&wKiWWj(Njj3nwI(%bKYEmRZs9`|SMJn1G-_??66Br|(5pXO&F z-Io8P)f~Zztdl!pziVG6mZUUe;)@uyrq?uTT0YXtyM< z)pmJdfd{J!lx&L*1I?N@Sq4*}#5_)j2vPS#zYGl2;&}0@HcLYp~T)h30tRX;w-zEQ4i=hwrmXcG7`F(Zm%6>UT(`2 z;sBUnh3k?#2r)f2@#0SIkduSEO-TWs-}GldMh5$Z-S*B2MCnvBmgstoVdH08{ZL)I z$ksmJ8CmMQ;aia&&ep6Q35DK6b=jh!@=Exe$Y$PByJe*K`cwgMc(y=a;M>qiN3{lb zUeixL_c5!RWr04SaI#}4D1oi9^KLd`xj*f7FY7zvl)B`V!mK0P4AK6ctJ?{p4VT<# z4>zi`h&J3jDvb^gf2g&IC_EXW_sJXF>u)sJ{}E7Nz7;|FA&8>ZMT;|BvAUp_;*Jd7 zqLz4h{=M`1ziAVPGP`3>c=bPgIQ42J(vFw#U5xM0Q&>dUrYp_xE$heQ0guW3>*Q!) z-N~iK0tlNI0S(Qg%bse&qKT?_9;)~ER^GH!JvoQSG+(!W81v~|p471_U zdPC~?o*O)zR6Tbhr7H6g)bpdqSScmc#5sP^zR+erORWob3=Cls<3odk_5%g*3y9uX z9%+IPnhP-%p?VF(SlHNO0jX>8EBL|bnj2>$E3j|?vUrNSEDw^_N9tRr@KZY%gi+3Z z&_*a942v)f3jMO$7=3w5)LusA@7Gft0gnG^bhd z-PRhWrK7%O_`+<|+T|4AZ;6jvvjt|6SwTWn)6>f|r(SnQ!atwbNa1si zE1ZMV%h+fltB`K)qr#ox*FwV0M@-=Gxlamw@lQqk+(JFW!^tQD9f#J&Hp1+scutY$ zyQ@ZM{UU2NM!E%u(K5QVWI&hcBSS{-BS>O1p%Im`s%pPbobu% zmZOT7%SBaXK4b0De?KUa)QB)6E1}ZS(ozwQ78qj!0t1V+FI~E%VI!7l_C+UT_zJz) z-92}T*5>yJtP)_IrAFX3X$l(zd27U=pBlN5{q9DMpypA&2eh%3K0Ij1kegx<4LXX^_pzIO(UA8 z$yxjPv$*?u0kWj%ZbS@~w3D)RtW2WL4X}g0bleC>xa{pq;TGbUD%h*r?d-U*LV^OC z#ZH&Wy1JupO3VbhDKc@ygczGk5q|mZFqK>5CYNr!{DY74Adjz*(%DkSM+nY47%D|# zj_`K#wbR7heGVO7qbm7`Zn`}K&7K9#!H?eZWEf4fz{tI-l$2uh4H~{d&=8p_%Z?9D zG1oR|4NYsG`#XWwvU&e2LuZx`(l{$?8ahyZjXOg!SF@zwTWY6Ui{2?YbKvqAtK2m0 zpLMEWqhDOnN@S%p3YBC$IdfvL>Cq(fm|owD?8gx2x;ke{#RWomSHXO_&4 zZfj}4_-LgNA%67UDSdS5>&C_t?i!~d`Ns!>@!ArC5;HELl(W~Y2|2ETcu26}r}u_s zGhuHC8~*H(12xyV9)n5SQ=SF@FV)D_Zi5}V>&oa7uuM3#;69awXpa(%3>Q&XS7#0g zYXPn5G1eU3qlnDbAbRmwL@^bcCVvLNm}b394@E(}&UE<;8rt+j*a=M+j+EHx15gvx zC-x)A6hxHzsebz?L>$OEXTASg&qk~1hdNY2nSA_9_gPAZB70m(T@4h@n)=*B>71SDsW zBsu2>npwQ>-e+pp%&D1EHK%GmY`$nIsrv4F-Rr;Bb^R_eZ)aHT6!5MdPp`4E6{U37 zueWNWxFbSB0|P^PDC#P_%=<3i({{B*4b2AaKr%Rgoz=?fK`7_J)&I*Si$4b`QdM`-DF_#)*2`_ z5OrLvZo-0Ks-`~guURLoi-aw5Dk@NJimO5sx7aIM^m$x}4J(6VS!iM*od8t;xV&*YOD)y{Rk4v;VtO$6(d;>Dn;C`osz7*shP}O9WG;-!m|n;%ME2%q3leU8 zbj0f{EhKY4iflnj@n`+w7|Zn0G*Pd>=8IjQBv$2`E7GvNz@+rI*hD|;M9UxU6qX&> zFD@oZ7JaquECFKxqow%I`ak=L)L~kBhbe--#+~1Luugnado#P}=Gq&%*>i0cX~mgI zsedDj_2O?JkA4?VbW?1n=@`-5+c@YRoy*k@2=YL$tq(OTkIhQi<4=6zl+L`q!K4YX zJ{3w6Dff8`4bnx^4IzX#$b{hXt|re4@z4tp{LDCf{~={~X}@inYj>?jE=3Sg;?usV z)vhu0ncpYbBP2?(K#S$JZqAWTjaPUbC)8eWjzzXXx*UaIwYHm1H@6cW&+g%B>pC^~ z(7nXF*@liTzqT&ApjRco;uS*Ts?IGl<AN zZr#nQ8$2|~S+g1{V|KiN(l1+X*x0iX#CYBPB5J?r$T(@~E^zhRKpwEa944WTd_J1* zqS-;gGu&plJg;SVrjg2*BA?qi%Ix}{a&P4d5j*mcw=Svf95{4=t4YV+U!XnF&ujj! z9Ndj2zv7d7mqBkzsui!WkxwP``da zkDe=s&W9qp1OVp5!uk?M(-9hbK`+I}y_U94VI{uk}BkqPMxBPDy)nm4xQ=kKtmlF3srzD*b8 z;TH4$i#4b1yyiXYzg3~XC$-Cjl_HppCjAKy$lfC4ZbSUL@6%MG_KCcn=6NqJjyE)6 zK+_W3ol{y;nyX)SRHCjesVjU4?WlfS;>?%FTid(pYb{AVO{HG()Yumpg3+DGYDTjJ zm>Pgi=Ftm7V(3@>PxwSiBg?AV4*sZ$M+OOXC`&6P7?#^16&%$!)P2?m70p$dg}jcY zem2_}e{hM!eC)6$S4vu7BD!L?-8;)gW)qOQ3=TMWR`zYp7p;(=9CnbUy7XPb={684 z;5Ses4$lu$jQ^Tmm001<*RF9hqTiUz){ZsxWmA+*ZPY9rhEqMBbZYO>BNjy9SXRTy zOFiB^#2mVC=LV~D<0hy5^bpA?P~#CvRif93di%0MwvjatlT0oxwWRbZ0| z?+E+hj}wypjI_Lwg$C&I03K$|JYrH(RLMZ7NOP&FV#1@x4C`6_T;>xchqm?4B3(ye zfQ1nmX^Sp+X=Ajow1j&1TJ43K;B1Zn_JwH9xoJB5_EI6E+KY@Xx}5_4zs^kIgHEbm zYd%NonC@Cc?!*`KpH1yqor(yb92dncG6s#b=foR?Cm{92wxmqG6m(w5>b+?;UY){J zYc=uJsDvcz8W|fM?Tt&gebsK(^Nd?`!9rzM|bkH~j z*jXs8g}sI9IW}wKknjyDrGehDYz9oU{wbA}yg;~yCfW->s!k!3rv)1wP=)4-Wq35? z{p0FKLVqB4he7>OI)p*q6uQlZ;P=LD!=zj-LfxppFm<*^9n>M&u&-|oW8CHBkh@pm zI9$E}tzQJ@%!VY;`bdiPL5c<-2A@gs?)!`zB)3@eEo97Jz2XOz)~WyP>NHPnkU@jO z;{5zSckZ|W$Jo9Pixlk3ytz;G4`T82^}F3fRKLxEvM6uNO-%0YhNCMsH%;(XS*u*+ z>I;a=R@!lBp4h4PS)HY)#P-9ZB#fAWsHra)uqkQU9L0$whO0f5EQu`^XnOqF7BH~7jz&_KqKdFS_%+3IG-@@3 zBY3%So9(uRBMCpo`Xkon_tX*Usl-SIWJ8|0Up|+wAtSrRG0#5Y<8?x5!c|V+4e5hM z;uAWe+S*E6YHITZ!^4>Ze7ckYu`br6B;%;weHF;ob^ddF!hiAJE7|D z5;Odrc(_`@BY;tbhkOc`?M57Ne&7R1CpWv3)tSEAtOnW@I8Q?p{Ysm)gHhwAa(is3 zr>kwx;L^A7t*o}F*zPBWh9|!_+C*wA!pUueme*ziY3?~&SveL>lxweQ6y4!<_yGq6 z$={6ZqK@v=l&xXxn~JJiWy&lgzQ5P&$=YK{K77%DEu};20^F;c*MQmWI!a--*E6m~<5Kd3)81U!>^U zBOz7;c@fUZI=!&V(|7(?=1zQhFFE0~Oh)$I4JwZi#?%nso{YZX6D{o#%raObJ7^z$q z&fMY7Y%mRbv2-fFVy)i)QKoKH&dr$3$3kq`h#O}%UPbzOMFT` z3>!fpuJ?rbFihsbGB^Y{Bshx{@fd+hcFwl`*k^fgW5&^JX<-4NC_8|J@BV5We47T`VPy-rV$g7;0r{%~&Lk z^8y7&aBxU~8!A@7_yz$Nv5D#aZz@*%dl;XyMw8)}F2&zpb}+#gUj0M=kJ%>m0|pir zYW#{mSL8BiEzMfi$7utoROsxfxTei)^wTw+oIZ92$3A_UuJ)oO?q~O}$4fa!tS4#g zu6>il;Xdr=Na<|kW0j6>1V6`};At+U5xk|Tu{^xLvFE!n`cqN(?b~x)OD2`2m5j*J zk69+6y?X$psP|p#*qU3#<9%RTrlrMukaTT^T}2kBQBqRU34HWwU%todCjEu}yu1Q! zF~u*RKVSdev+lJzIlJ(%;}JDABB?&UMrx9}ab;QT1>H6`lm4jc>N_9U@EpiXNQm1^ ze5Jg5H=H^4%Jcgnr6vl;SKt&1J`)iVq8ApXHkOIZ?$|)vXt{p3s}pZvG|mUn8GG-1 z$AiLFO#!pwLd|jXT#I3x3i2bCR#z$S++p|($#zR{($Lq}S4fqom>WmevaxkGFa~Da zVo*`}C@pXb`z+Ei4w;b(O8 zM`l_hn|pgT&S4ZT#Hz?q!szByE_5M60sd+ZmdD(ldx&%UFTGqY|Jxpw=JnKcb zeF#E@nApk2F@U)9)V8;8bCX$hSlHgd(b$;u>X~(R$PUrWRe5@|uu#;SLixkjyJ{{7eg^0WW@Q~!6r0`vw} zR!Q&nE?#*5A6Mm@Z{v?%*&l~s)cp4ac{2AKf<@1G$)p@O4%|K?N-xvq2DBJh)~){g z6aK%q*!}DK{Lr^c6JP~i%L&VzwpNr9663D zHJ)rbr2-+*zOPiPOryI=y1zB^{TaAuIkP!6(JTj+8=>Fz`aNxP%Gqd4e9dfQExTGV zm(8mxIQl7=+a2m=VlZ!-u$Hl%9eDFz^vFQOK(T7hMv~Zo86R)>nJ05nQc{F>e)~>0 zg|zZPfinUylz`*ksBQ>)egUWku@hltVPWBe{&a~CpoVY4V&#`GAb5BD`wPD_U>Ir3 z$;qh)Pj#^L|7(`N?hN*5r{FD#SU8wA(9r!2*$ziu@^3j5-P??W_xz)Uj0FAcF2^vl z?i{_6{i+UBzNI4T`<`Z^N*kS~+1B${&p7EndGZA9;arg&^8<*BZPCo2=rCX;q^6+( z^|FA^5kD#~YaLiXH(7N49Bj?C@&THy;K`dKJ#~er6koK9Q`!^x!otF0&v+%jvOjQE zNEM&UC?5EiHLU*D`-N$oZk#W|(J#@JYDK_J?`9C0{UYeop*F8ybQ5un6m9MDbvfpB z{Mdvo^p_4NmYe7k5kZg-FnHmH9W6%?BxmL`0|Ej-i3F$YydvkT^(Z%rJ`vYQlFz3mQ3k@V!;uX3#*IqD(S91f9Ye}Czknwny@%*^I0t$kq)5y$}Z z?9Z@tr=z>xaMsHkQdiKw!R~-klRLd-mG5zzM|xb;j+KEoO;pt zltMIWv}EKAqjl(UnF88m_KkW6Z)MG_L*;Ulwcs|3Ufr`Y!NyshHnR3rA-5zQ_Oq{7 z1srAq5dUyYy}L@vT?u;^FKiT}*ztBG1DxT7Kntkxi2!;KSR{8O?4Sj(U}N(=@m>WQ z(d^MiM{=#a-CYWbLMII}pqg%wgtYz+y2)5-Glsi%3)>b<{Sc*=6$H1kgxx`i?46sN ziiHe=I`JcV`gFzrP>`%n{bh{3zib5trQo;0iQ1?AM-;2B?9tK*=l+Iz*aziNs81b6 z<`HfE`Cbduce%28KiY$1Hk!P{=SAlyyJHqz7S@l!H|>641lcdjJ7FTRg`j*va5o55X!F7$|yoOiAt2<%JsmH9<(=JCI9^VV(9bG z$2hK=XI`Jw!%&ED2{xi2K+&tS=)63)v%9sUh81ueQ80Je9Q4T&wwSV_t$E-6&6$_*DXF1k4D>zNC{QUeaD9+^M=#ypNIcS8bQ>f@6B{@scMM1?;(M-DY`hC*Jv}!S_z&c-mL^ zycN3wB+mEUjRbhKN6wZ6nMv$_H3GeDP30C8G;RNTp89d0j_=ej9?+qAf31k>k$ZoI zG!~^;9j{fcg89nRW;^b#BWlD5v+z-y^#7a%Y*!Cf8(bbw+yL5YW+U~*tsBeHdX9l& zu{Pl{O;PR#`^Dqc%DPE{m9{qs)Mqa=FsoQ7y_unpp^x238y9<)ibg#8i0y}w>zb&Y z1#aI5bd+9(bx9z<-ISyry?L@K;Fj;($$LOi{!yG($#@|l`OVYxdrNzG$FK75Nmlgw z+CHTWl|UKv#aaK2YbMfglwsNTwA6bmD@LW9WtG^YB+sS1m&xdz-)E6qp|pZyjrz0s z9i6CluWo%S{&1@rE!1C7EbOY+cSl|_bvmb0Su}Yykj9ffY?XY!+tB#<$dDe?u`W$X zCelNt^uBDw_HdFs#zi}}^2NvNhu%4HEN~I&-rBgdf8%9mkw@@9>6!i4!o$Pmb-gvP zGsEQvPlYozE?)l7a&afHQ8`(VmW)Xw_qnlgWH(0MG@<*K*geSodiSxwfO|L3%%zlup1mzE>o>|p+p^%+) zBjTPp7G;P*5f}gId+0cpd^lW%2@OTo_=H91ErtT5D||dZ2O#k)MTmNKHc@0ezpU{R zm9Oo!uz+5jlZM9Wfr|y;Iw{?Xwe8fee#jPDvjP{RUcme4kAEM*tV4mznEEwRp8fob zu~3@9xg?e3x;vB3)ArXExmU%XeL`S}63XZ6D{qgNvAjQmaFHJx=`p~4YH0}&O(q8i z>tcT~&T=L#l%uO_k$Hg4@bMR!;F-Lv<=+@W@pKWjN9_}!4U_NAEL33}J2UXjY| zLT+^)$?LTz-{0nEv%)Ub3pfXx*T8EYO#oBb#h->g=h)ecN$Eq|X5p4)S~B|;w=lK2 z+!T@sk0Awf!|(zou0%3cJxSa@!1)(+wt$}HVhQG{9}Wyoi_B6;U2v(u?_dSe8W2w0 zn&9T?FE*^lK~whwK`zPSe9j-9yT$Tba9c^I{;M8STb)^1j5b~B$N z0^aDOuCjeW7&qda(yVK=69%CVxJ-Yk{1?*-&Zc;%POR9NO?}!*{#cWIFVIPi&8*W@ z2jdiu5I_MgZdJ@FI?)2I?rMsF{U>{u_CrTz3+T7e4J2&l~0l4iqM)Nq1uY`!QM$|27z2DZ{NDrmP=&qjq@hD1tXJca{BdHfW748X{ z*xYl~=p>bp@EhUHvs>Hc2@xbrtVn+B{l?^mx@zUjWwNE{(hs#U ziP(GnsC@}@4klXeK`Ugrt;mLNI*w<1_fSup|M^=q`$ zbS!&nA}1_anNri))YWtM<>DYdSbI%1|NVRa>&smT?~h6wlJIc3arG*Ra%Q1n>2p(L zjyQm`KArLEfthK&je$6V;l+1ogimlw#qL}A((zZWUq4!@62Q(!{`B|8d6iMG34xX( zS2bg%DT4YuNL=5P_A#h3)t>60!6Aix0YcYIqIhoe&zv&uF#J#8vxTVClGN1H7Nc1K ztke?$i{6%KX6;tzhQJEj2_^G3Mr07+Bp?+$NYWX9`edUym1KPkjCcl9mwY5&YAiZmFT2Mu zd(9*WHv3F6pI}MK_UEOUX@tQ`GlhZW2<8CUBFn=j#{2T`yno_cH5{4}H04v#ZkBCq znqnCShswLF-QsK$m_41VR9kapOA~0#zrXVDaxH1F=Pc8vcx-jhfE!K;kATM!d3R27 zlS-U`H4AC#{JW$*O&0xMKZH@%(*+s~4{7m6k$wtG6o0}05iW{^eVv%%d}`l=^bB;1 zrtk8=>YRJQ%oFv@n5pudu%+U1?UlYPDXUSogNya{2bP8WJF~65H5Bmu)`U)kBl_=GSk` za4la@;dF}rBbFf9uL2_VJx_V!tn-=#c%8`AFuZcQNZ!;BQ9xXD3{ z_1?GpA+-2eDi{<9@eo*Xbsy8EiEs^^vHaC5rtaI&SU)gOt^a|}VVMkY;%H4RkeW`=qhCL$C#9#We_aYE`@SNil5U|ox=xz?uxW>>?c&Byk1gpwyx|cF z?Pb98s=UQFx>t=3S&mFjY&*uihqEEfivHBoVv_L(hc|1In8PG8{;u?3ieqyU`*)~2 z)D3UmHQ94<7Jus(#go-Wt9WhWu&|`7RZu$!`vQ$i$dz63Yi8_C)bG!qc;?+hbl-zH z=TW|UHEsFC%{O@@DZ@g7?ML44aXK-y^4f76^yX?i=RJAb-p9qoK~@sOl9YHppcex3 zYIKZEZSoM+1#L7}zf~1EFY$sJUGp}mUjZbKO3}mPmX=|=M@MD6=hs22rp&5e{t6yl z&t7Jj3%|XBhikkKWX0BC>TLi(H(%bncH@TDuo6TDXc#XA5Yt3&=BZ!Z;%I1YE{6D8 zB$xHD6D@?ayi}h_luuM4w!~f}Ba4M@wq>@)agH%Biv`a8O9S*F_;9rx=RdXLb_a^B z5xUAVVGct*hmD}E0s1LBdw;%GQ!9t7ywGjNxB;wMo0pm5qINVvNr`>B(N(HsjOkgmU)dP+WF7A*3RN)r2iuiBGqmCa+OANx3@Ob47py; zl#j{Sj6>V~H}Xqc2m2x6-C`>e`^o)DsEc&0LQE#gL<1ND@|@J_N9<@zPX z$l*_`V8U&LjePvTq2;SZ7yp8t2wgBVU~@VUets z_ZWadH|`J1NO`A`8tDN^8i-3+>dnLFis7l1a1YBx(#cE665ogW_xJEH&hHRPJ)^)h z2x7S_g9f*!8WkWi2DW!4NxVUV^7WgL$xJJjR#W4i$eS-pQ$ect;#T^xT$UKz3iyCMC9rbJ<;aNY`US3W#p}X&q3Mn4EfYdNQOSOvgMIy zcfyRp09Z(4b=f0FnGxnd0eFyS-PkTPtWR+PlSl?0tpF&#c>Q3qU!Mp}|Hdr3PhGxv z@lr0kQp$n$T#8?`6sdX{@w9KplE$m;Pi&hBkurOpp6)H`8#5)HzTE12DN0a{HOaWzJx*KI}69E?<= zA|oR=v$cy50y{Vx8#%)D=u6S}M#n^E-UbIRhIW*hp@ap>vRzS~f>|>2lY`v>07NFwg<_M0)qXI1YGS1P z{Y$4P4{1pEL)4ne_2h&^`PtU5-kfi3aBq(w6W9w!xiu@D_f#1*Tj@DqfnpdXyBGC0 z2Hx26hXdvhKSZ~~pv69LO5e6aFA2yf3&t$ia%vfw<(|pG4MCR~s%8YjptiLn=6i zYG>B*$GfX!?8r|es^A7u6b7DG!$B0dmB4ubGD$75gzizbc|ayC%It1!MH0Ya{?vGn zJoo96)2)+Bfqd$Y74GFyXDm!N-*9{cph0GxT;B(vi#~k&F*H<*7q~A`-AJr#_~!ND ziRiGeFSFDIpzF1TafJ%wfMu^dPWE7vj7EAYx_2MWkHGs<$XyZh z(Ej^U9sHK7@+KT%2FL}82J>ZpCzM^6+Px(hRl3^f@_dom!ZxB@Y;;kDfI^BEmDbS_ zANZ7;o0(XuM=x=C-9Wcm#d#z{BTo&y!OG*w0w6bmigX%$T*nWO`vpS~sQiGn?+iR4 z=xu>Rsyf*aI1T=0I56fYCgRs8GvI%{ts?K*OTlYJ!jV-fotXuEG>d;I7XEV<07oTx zz?-bBtTe>ec^_(x-@JW0W|xMUF^BVGHVnZpJRpP#Eg@txrVIB~Jezq1{pLb{x*SvO zyLZ|A+n#p0iz|bIGC#`W{QY&K6+zV}?LT~Z?8-GQZa74(+$SkN24Go>LRfmS$;r(q!lBC7! z?(Pl(H#tNH3@IBvzkmO}ZXDt_c9)6XmC4J9C@AzHMMcL+UUNUFKI*^l{vB$(T$zpe zydtt@67e}sedC7`VKwW|GEr?&#PCsjfJumv1#2mb}_O+v+#M$M)gF`*$Bc1da944`|J;%oase`1=io zDC$+Mg?k-`pO@Pj4`NW4FB6wi2x<=45UcmNwKFq`E{N?YL<(xhFPbgkvK;Kp<7v$M zzXxer<~ZT+^|C+pj(X+{Vu@P!6|eq6Y2A=g&0GYt?^DrpXD3)p7T0^5L(5C#zP^Vw zO99Wb>jVU~&P%4U0dDFc^Wf)_CwwHeI5#u%sFSPc*H;?a-FmIm2d~!ue6K$dVX0%u0C4Pa2QxQA?!}%aHDG8h;0cNe?63*)M4<4kMw3Sy*RF$ZriE68b74kZu z>4hD8NQk23BMVDQ=Cc!gUxR$r0Z~`$w9~P_|E_Wgr^~5)zbD6OLrI@q!SA~(;l5h-{yF-C#A*t9EZP%5m zE=o~*wAa`H*tvX$>DU1VY1q;ioD=Pk{!*k+Y#tU*FucGy<65M)eDq46XZH)jO>-(O zxovH=#jqst)~_z5R^+QMJ-1|=h%wdLoDeU)^DTCRqM)dJYx3ACOT7~NkRN?*GsD#$ z;dN?;6KOCZg%Tp*v>P5@v#79h?8)cnM(g^(`V2vYwNqixgHjCFA0NeM zIE1xOH%59Wu8w1PSv1;u-xlEU-i=ce_2b@Yuk^qJu`2U9%aug^kkx7 zoUgYOaI+K+5GaT`8b>F#Tviv1s;a8mL@aLkoou#s%6Bd?s%Nw5tT)%ovC=Ch=w!*8 z(C2u8!$(UCR84s=TO;6{;s%YFodZEB_L*pw zLV=C^3S@9JH_I#`!8;666BF=y0?LF2x|_Fcw%G3(b0TQNmRTPWJ&*_uy^77gJ9<-| z6!$60xSwb((Mme)L=A?R>c*NvJ)lB!=erroa7;F8i*>68?jf5p*s874opyIF` zwGP8CI!ruhwD7K#VU95M57JAWjM^n%)h1zdSX)^nhnemc^VRxlOb|UD;#%X?wY>Sh z-9p>$6|zclf-OW+G8Y5F-}$I6@90LYm9M^Es9eE53&%(o_swse&6ca|(=^oMhewfP zgAH_MlqMYOCO>2v^mY;$Ew$K_=^b=JjDw~FX6(CjMStQdlDUk_M{%tWZShC%HHtM~ zO72fx5{Agf7BijQce%xoh7+v@gB=w4S13|aQZ8Toxf-jNFQdBM zLN8vs`H%kWX3(v+teeY&;l1|^%xmu6yjlJ#d@)54mOyZ29~}!#-CqM}Awx=`bF!0B${cZo3Snx4#w zu-E>l?tBe2=lU2m%qliEHdKp|IrxB3_l#aw=RN#fT>g@Jg*C{K4i_w2(tjS2-7tYU6wAyVtMdt4rp@*zG4BzITe*O>Uw8YpF$?jnKSk#Bnm0_4JX$)cIlQ({P1~i8hkRkUE_T9wl z-FDs9LJ9Fl-uY_j26Gs??0gq#)X1JjfAF`BW{mZXl?k>fRU$qz)%|pPplPtxZXc=Q zK9%0eXEX_?SGw1yPoXGZ7rZ>XfN^Q0TAVh}8=4_GNzk8p)swE>y}*O2c5GXO5du_< zsi?oloJK2~7qR_#2|9*ZAHQa1lW@0C%F%trD0NEL=A3dLVvue6)vt6 z_gIvFA05L1D`JTccz%bOEAA!0E&&TtWE&u*H1a{;$_+bXVa>&!Uly9+T7Vr-FT)NO z@kBcquTkZz(gShgwe!J(lV^_);F@ueFSkH{qbh5i-U0?SsQ zSfbq{x3lmd#f6x(Pi>$P$MN|&pPRVe1aB1TC<-3;r;E_&7S`6*(mMV~g_;(Dwx0+^H=`Wlf-~F?>Qo$l z0>jbSB1+vtZ}zdJMpY?AL{n2!pqILEMplh^Z-KU`&si+;_Fp|tqh3ICU6gRdU(VNLa^6_HU$e9H zlip@J`-GOQq2=)skO&uh0tSkZ{i7diW}r)4t6NvN*Vx#?P`kb1WAG*0ye!CbFkYP`n-)UQ9GdLLkJ}4$iR5CMHEHt5LCiBCt zHECF(|J77?>Iqx#p;Bu;35hJKWvik$u-EVw(?pwEkhFk4*P|7e-=UxWBG?=M_zv(nnc?rBO2*ivcr-Fh4 z?U?7mrpac;@n&1GOc-S}{a8vQ%tD(Wswke*q?2(vo%g-Rj;Qd9wJ1jQ zoM-V7^6z1s#B1KY4N%I@b+!}8%O4^^J6&2);lBH8r#mYc7#Tl$NND9-Oo%zbov^Sz zDe<|^=%+uJ6}5^GjWGCJpZtmCp8E0eE>~ak`D6jdei+lvUIRky0C;NhwTq2RqNm}X z28w9M?DT3JqA$Guvzb`p()JzvVh%NidsS-ng(7 z&RWgL1o<52@Hf~Y!(A)$p!`c%DR$Sye&?!6(3*HgFt{s* zp9g>(XVzIZlm7nxkxFaZ;qt(U>-ep(m_$Kp^05j_*Ei=bM)B;e2`QvT2S~!*ZcyVu zd!W~F5U{V75gHzTYQ~S2dnfsHCVFM0O3dddt}dA3MLs!?MK2`t^$i5P+w0BinTnXb zvG9UjfK{`~d8z-iRQ&99*(cie$&nmCWinem;z^jqJQiIJ$Bo?;{RE@I^xI#U9h+z9 z+9%=|{uuf#)eP|!i`lQ0wn-w1=}oxJtl(#qW6i%b%0%9}`9bWu&4djr3iXSJH&C|R zXC;VKX8V~Bcf3RJGiG8@sJaCzK9^0dJzz8{{8f&jGZ^7BAOG@|KLCdX0Z;bq1vRmf zFq1f#P=8sap46=WCqB=<@~nt{uzK{FGv9of`5B%tlE376b{MK#I?p^S-?xZx@fmVm z=ST6h|6-MTQgi(67O}mB6$upp8Y;bZsVp65?PW0;ErPMO!raqUCJWudy-FY-!RwW% z3vUYElE-KYx0M^lT8%n(G;E`Ztfm?u7lhO4*L9O!%J=Aup1Q_FZ5zSQ#kOfBa)t4= zZauaHd^eSN^TGpO%P1y!&y13Os5_uNGjW!bG!i^xaD4u(!MIYE6qsZ`UUvYRT_whf zUlFvyYDH>U*q$6V>OYZczu})@bq#C4w*>`>+Z?^A9JEtkLE~}^svM@Dy7{DL^UGfK zIwgBeGQ7OJmk#rEW~l9DM!mm4?WM=K39P;&o1jM|6VyEwW-CWHm!hdVH47oN?|vI-q&q3F@ZN z!nEPeIzS%&u8W*(w z<+qtK6%OYX^o~Q{g_JolNubb59pC_bw!$JQ2M2EN{V(bN{X(2$WMm#4qhB)>;f+p( z6Jel2hjA?5hl-Us^x2Qa!5JoX@7ChnBu;sIFqnXBSw%%fg4-+%Y6{-o$IAQ~uU_RA z6fAAcQDQJYkl;bR4bp2fAm{Z&2>2kUSjPqi2HM+MLDCDFAxH2VL-H9o|FLYK{$*oh z8y`o)Q4=hy)Xk6xkNZOkKF>wixFC@!QwF_FP5m$>2SXpp8J-KV6BRS3;Q$;^c6|tv4o^Ss zIvD;BkHCNHU;qF03UoI%HOaXdK2iBkRq~NG%d?P2bz!$-|9$a5^LmzoAj>#3jrYix6;`7`S~~*w$-!j639!J z>H|Z{xVcJ7Im@{DK+Oe2p+mO3y3Hx6+w2ul`EhZ40Jl^y-%qb|CneumtYCQA^6C5I z0t}4vF{keIVkE7XRZHAMV;AG;sC;;riF$9gjIG2kmF2J__lJl7mb$hq{fT3@#T<3d z{d13mgq)1|8V+~qZ9nNh$}5wr7A7W<19`78J{ACBq@e*C9_=;VvU{ej5mM(~vwv%x zUFe}#22Cok3xNCkx79e7*>x4xi+6L5kW8euuV8HNWp{$9ulIFwa!MC}-+)v)*aGaV zE{+VG^`!UiG12-6adB|Sp*NxOgAS8Y%p*!bA?E0S07yZZYPv};-Q21fUV;t~>NCk} zi_FnQByJs@H39S1jrMM#1$=~16fG!A>JsM_7 z+*x{y^u8R-KGzy{oAS0+mblTuXK-ihmb-y`QnUaTx9v$B$KUYYt$dJd8*)!1+wkPg z=QQUD=(z~*az8+<5Gtxd3vB3T+j@tWBQ*voawX&%JpN8! zp&K9X3~S%mSEI%hhf<2B>1Hf~vc~9SJlP~j2*K~Fhtr@IUg<#_Fx}c+9v-eR4i4C> zpHZgZTOQC=FYiwkwtwfPN`0QT;==mS%5Y7Fcun`IT=-E0oG{o7O5MP7(v%H;{JUa` zYvUDV@QVTV-wPB2{l)ss-XugsVdWa0>wlhR1ps8Rg^0*lhB8xRhvzEubc%F#uvymxhs=0n z7s+Ygb{Sk&l24V-EFg%rJszAV1&!P*p$3y*Nw#RxVA!~)scEb zstuNI84uVCL^K9>t2xfBr#qx0YBmpvD2J=fgR`=-bhh{QQu+2=L$IBxp4&?!`fSAr zSk-Yn^Kq5*T>kh%YYlO<}^GJ&RYk-A*oM?!O)0k3v=g z=tF~P6zwfRfw-wxX){scNTk{l{?lT=C6Ftdwj3wmxESXrfy^q+6-cu+tErn`bA}b) z=?hk7N{j9atg{{7zDr2XQ~BkK8!$D%RS#)*yBM_R4C({Xu_M3~AZDT^xTvU8i^VW| znYsUj+fl1EB8$_kg93(Q-XpO3qhn;{fHVb@xgrrN;rK{;Wl3pCNogr}(LJ47(~!i( zrOU2YZ`=^|-2I3^mRM9|82`|JWraXgu_a3y&4sQ+<~-A6YHn^Onjg?+Q%>SOh&=pq z{u41`ioC>h>ulE5mwv;y5_t-XR3E}70a!#|I*qw3SDD<~f%;p1DYkWC!K4&xL`NBo zy#?lUUD;PZQkx>@iil5bQ^x^Uh}ODl`Yso&KtW?Ho+vGLir-w+mtx_!SdJR0Dxwl7 zH)KK9dZ^-L_mbB_Df-!W1Z`|OXT+oPd7`6dne*SCsoK*=gi?+huBwV|#U5O|c01^qF>-P0-A&lh{$Wn{_kTXwk?|?lEI&76 z=96;ze~Oo2H+TdwMR~%u|F0$3+f)Bj33fW-wi$DWKy=Ug7LnIcuL0X!r)8`#Xr0M& ztb@M-syvb;XJf;Ym|;qmv0d`*5XyU6EXF3B#Iz!qAS{5J^A`k?yICaG;J?j}*}{rGHF0ih4o=|LuPW{vLbfBQZNeW) z_$wvXGJKzf?;EAqnJT7@y)Wb}KR)*rd3*`P^70u9Vqz0uSeHvptUn>BQOULvFv)VG zp3U>~^P9cf1mHH|DTAC%lQ!)x|NHxUaAhvSLv-)H=+e@jT9j1B{?^QQf8Cuv<^ZCG zU~QYNUBXW!X3Z%aMrn0EpkFRV(t8!L8r1y(Pluq`v#-Z&%Ae52Ya1Y`jAm1Pd=M!u z(_A>@_2feKy(Q(O8q=h)T;-}52f@56*RSbT@=hAtcZnY?_OsMO*5<*xtOoPy=@--7 zmy$;-#LP=%zt^4jtO!?gIqpB7HJW8W!>9>&$wC+9v zLM5^vAH3KzFicphoGO@Kk$}N0)*b_Tok=5~DV9paE{33T4bvyKXGb+!pNPO)Zrnqa zg`EFZuaWPx;P)Xk6G?A?%Ut05)y^7Kit&lb6g01H&Zs-g+Lv7DHs2n@*dKr0eyF9@ z@9>cOk)$po;0V6;f<03H)iB9OahS!cHeIn3wd%*#gKs)tx4L64QU_D~&IsApmbbc8 z?UC{KnYD}(GDck?UH>qfu1Wgdt(f8wP#ZCQHXbh={jvv8pnFS@hLYW>n<88h_5Pjz z$?n`&p}h)Q@B8=B4#$+>6k#bERq_6(bLmqXSOpfNceXkEP`c?VXPaSQmPhn9UYh!% z7PhM^m2>BG`TbSc!_9X-Eybb6Vn^?%w32LIHpGN%kxnQKP~8``hkgXeTeZ$x2?z$= ze5+ygws!IXtD(#UUhQ`|;Kf(U%LcL_LM65OXp>emOu_T;oloc#^In*rM~mfcJepT~ zRhZ<=kr)V_n2izFPPh2jgDo-@EOdAP3F;c$n`$JG_jvX4*w%=5vb3^OZG! zZf46)1~`Vb=@r)4u=|=>C6H?yWqWjV(0o*&O-gqqU#Fz|&qE2?GpXlUp~Cz%Z34)% zUe#hcot+@0W+Jx5ctWFz0C6$qpWWC~{C&G&&d_qLzH?8V0N>7#Q?8lCNds45sC8+j-T-n_c9s`6EJ3m}bWxZEO&ijeaIxI|G%;Mf3 z)QVY5@jPHMDWLd!+FoUY4{T?3T#HR#M~6)p!x5dU>gC!V{!?C9O&~4cX)K%WgII;A zXG@uc%9LV9_GwIJg{oyXi;liz?5e{MIxsfo4E>4%8GA0WY;{_O2#G@YrA zhy5s}`y)W}MA?N?!}x;CwgbA?_{TOM)!aw-uXe{8LB43Qk6(t&jbMke><~Jo1QMXZ zdh}j#w;XX7K~VWb)VWWBc>QjJDUAAmC|&6)-W984&)=WRakx<9 zJ~w$z$;FgzXnJ~r&7e%7C@q!NabPzDHkbc~phqW_fB_HE+J{ROoJq|G@R8`+@=tA5 zYa&`dnqiTEbG3~YD6^Oe73^zc^bE`yuiI*Qoq~!K(*{bH|F(G!$4yfu^0o$g!H2F> zMLLyn?`s01E`_Vg6-OA@SS~08g@owW&r;PSfn+LrR_0?ExScT4q)qtTZ9KBXRDUsj z=JHsbeT8FfHu2iV-ET6A+`YY;tsvSDt9$!fF}xbx`q}k0V#Hz*C~%Fl@$nwX^?yIMrt#jBt-7H|*QrNxcSruEZ(IK| z079D6r^$}aznwuHdz9ZEGLiqV_&>y+qK;d2G5lULK)Q(DnPVG^A)u}y?_IhaGUVBS zq)ZWwEgB=81xOrJdxnzi&;KF%i8z`4o`)49lsD@YJ|NE*0pjHEZH_wgRujE0t@n=F#g76Xth|99`) zs%wL*)SK(vXLUP@4QqGXI=QkMT+)JLk63xpaBw|v3~LUfY;10xV^?{gOj*0V5N~*4 zU-Ck%prR#9oS@8M5+L~norH}(8qrpiKjt$Zs@7ui6qxNxfh`RHB4wn}5hP~6Z+|y= z^-684iTdLGid7Sfh<5(k@KMf<_rA|JuTAlH&qekE=*}?momhv;D_2&zhMx^J-?+JogKit_8w6_kHhu?d$q}KVPGC$p^--44$v* z7u*V5AHdLRmdpXn2N_QqCRoIk(xieJIb-a7J4Zwu0)l*Ip$01HKSwx348|E@ob6Z6s*%l5f@ z`iw_JM8yj~eMs(8t%hKK;#lDk=~dkwEUfvH(U@1@)X|INC3i_m(s@U<35rlT;=v># z^O(zCj`^$5lmk^V?B2f?A8W3#Y>fJe-Y&ma&!trvDlHC6;jAkM~X3t%3< zJllpCtqswXt3k%Vj8VJBii1Xuc}Z&c)L1V5F}aw^CGHK6b-^52rOc=asEp4}51G|7 zeo9f;86x}Q+z&QnfD;`5ON8WS)D!%=G!WH-uN#at)=%fL)$nY?RK_j3S@)kOa)B>0 zGc#iq5>C0YE91K_R$B`vhet@_)OX_&3j>r=b*WNej3D@j_#rfB^VPx?*Tpnm1M-vFmj zb0UUK{jA1KE;ybo<;+(e#wBH5z}%|J&F!#>ND#D67Fk99Ffan;DH$a(pYzxYx6yL- zB+GG|MbG`}K-oLDuQ9qW8&oItal%n_SQo9baV%9v>wD$da=o;gd$SGe)7&H=t`qZI zB&~lzwL}+%@P@@V(z_04yydr=)2)zFcRQSfdQ%EnwVT>7N0)<96!4c*sTivMfnek> z3SYUETNoaCqLSCxFK8vcKQ@wzh~b{LGwSO8%JwCqRp1dKjl71c*CC`&(y`WUJ#x(D zf$}z13AF%^JjaK1fJyym_EUYDkTP;PYhAo0-JaT`x@d%i0nNg^v=TOpJ5t_Pawiw? zgAcD-PmklRWSe@j%ZNl5rZKsMaB+ zOC>*l|I@8uJq}EhqZcFFEL1A%R~U@Of%q@5@EY}Lj3aL2b%Q-a7E25tI7%6q z=&Vk2yzHk18Z167gOJ1hIVJ0tD_>$N_~^g&8z)KJ{8TY)=!iLUC(Y|!3&ljY%=g8z zu#tcOFI7R-gbN3I%rJMI?n`*nNy5*>yppjl&X=c&V&0s{Z$S@e5qGfCLYBqQ4anIG z*K85MSoLrzOdJQ@*HjUY3d@LQDq+c-caZFec$J}u#l)85Ex}x+nwnuvbW2v7~s9{M#P|p=Rc``|p2!6tCOaMCsl>hy;gH=H<+- zk#S*UYWKetf6@uV9>~k4bOUC-!bZ=V)QMA&!fR-xPL&9#wjJ>4&~ldDyF# zi{iH>^w^#AQQ-*Dd%kNgOC>>SFGh#_$twsDUce#TYYZ|lL60<-XD};%D1_ol?Iu+| ztb*C&#=}^SDC!Z7fIj^6z3h1Po>)P94pNT0^f?w*{RUuIb5OSAi^LVruuZaQ6%;j^ z9Gy%bCGxVyGl8UAyPB}wE-zx053^G_-Z;y{4Tz_8M~ms1NZu#p{7T+=q7Bu1bEz*y z6j2Xjv7eA;pAi-~H0*myYoQLBx4fMPlk4cQE+z}qigRLo62%lOGv)AZUtV3YhJ?Ej z+@AuBiOs&5<+qPOU{kDT7ib-~+_@?&?E#G53(fYlBtDxTRDU((VugUK>h0yxhXmnK zKNAZwR9G#?9RX@~LDu+GcYJ28?f4bepWmw@^>U5(q?_DR+*3eEF_T-yKRal_9Czsw zjr^==BTD7&D6LZ9W%FU1r_mH}zLcFOLsn7BwkOUD@v!Tt#5RM<4Ol9j5iTi-e<`YC zMe0>MApQ_}6pGNio~rTE570fz5HKiq_YJ<(cq3@G|04?Q z8rx`~Xh?i%6q)=WwZU|FP@O^s5(Tk{jie9Fr_k-tqV?`=&^hb+2a9Jm92wWU^!;_QI4Kjok zpN5t4%S$8952&uXk&yTzWh%PC1#zN>p!;7BdgJz}UgR;E&aFj6o^nNw%Gudjfm+#I zx3ZDNc!`m@Igll8T<%N*AtDadrF6b(nPK)DfBfb+KJyu?*O=zsOYqXT>`pWp*z2KHcJfKLO6w&qzuA0|G*d!GMa8ghpdQ&63r^ z$OyD1!RuAeDzr9`*YilFRQ2BxnsjR6KqF!i&^RK{pqyml6eEN>o>u38Ubn{jk#9EO zCbPsK7C;XZmA<@!C|Rhr?OIp?l~Ttr$Tnn(hMMyJDOJr(RvBdp(8{cjMgCB`>razT zFl1(KLw33q8v*@N9{&zWHiq6{7H~jTfl_p+>1Lr-6gRe64 zw^#)>_*6{&$1uQI1C;T@E@lyt5sdMVOUDIJD&q&liWTlrB*~*tw!oobOrkl|1L4GD z{Ad`(ZuC8r)At)_Jj5pcjg_9S%DxLuwX%;zx&PY>5S%Q9#ywgeOs*V4Egcq#LK}Od z=;h4bsc^>C)(-J`wEdN5STG0VI*_nAxk5tNl*>a?QhXa4wgC(jlMHD93}#Re_M_bj z8r9kUu7rYuNSNsQd6|*gK-1g2+?Z&B76d1}AcumRmscI3{GrA<%r&cgzgsyoTMZ;b zySLVsY4ZpDK3vz5X)zwsZvsU02GEkI=S?^GQEWY!P~km1HIr>w(-DH7BfdS=yt~Rg z+RmN_GaZQGciLW9F~SzU|6*AE`hN(1w7)h__-|Zpl_@j3G+zTIW}#NI)Qvpd<+KJ^Y1frvN~&TFx$_9J+u^XUjd7rxZ5d>y!5{GJ!El>I)L ztP5E^3N>G`P~#8-2@h^|Yn;c$MVOGt-dmoy_n%PG7&hQ)uR) zkr!&@Eknd!<^n=14*0jK*|*MiCq`y^b&s#PP4b`(%k6lg(NqdKp@#Xf??f2Gw$z#6 zzFhZy#QZqPKIQF68MRo;`bwjzj?mK+XY|0cv9N}NNjF*9zBC@=OAu>YKqO}-8u-p0 z6wNGye}>Tn0S(|ItnZ)Q&NK~GSitJ~{i5(h@GdrX7^T>2*_|-4{RrdD$|sSk4T`?L z(ws~LNAH3N9u|M7C@i7D2m|^!&gh#)82kulmi0=aC{)s zUN-ux9Qnd?e0q3za&qFjhe!B~iFlxYVE1S$mPt#+>+V7f_y{3hG$P@lp_NrtPQ5V1 z!UDBwj(pT!PBL)O<{w;Iaxcz@b`7Jsag!Zjo?EL2oUavm| zW3hi-e+2)(srmT7ArJX~|CeqW8m$0>%?qag-W4<}agG%Jf^-OY7XEb!1^m@8Eehj5 z@6pr2x=4O}vhWV;%Mi6g&SU(AE>oBP@n!#u&G`SJPk=G=E(HMz2^Ah`wo`PfKEn-L z0zJi33W{SPAt}PgPo8jPVf2^xYizTB9sHvVK%tpmy@?-K{EPWVp#NO7DOBpa*542% zC9`+$taNv<3rWeyj0dm3D(Q-gf3-z44>$5fig`z59Mb8mb_l#S<7D z?0zbHD*j5fQ+~SKRH;uNOrX=#se3c>^R?<7H&^XHfBv(rSt{Am$|cM;b;P;>)I`-x|fBks%3gc|jTP{;?_9CJSe*k3@<;J>6Bd$jwNp8`Pu$L?rmQ~AakMagP0 zNGh0Z@JM317Fizx(Kk#^Y~{If$S=$JeZVGI<-QA|OtUHHmpwkFolCX4fU49J-B?%Q zbECCAQMSVHnCjI?clW#PiAN-ZN+}|M<`_b@9YV-1PJ|Hi_3Zy*=#$$9S+^2}%H)hjW1m_Z|KF2#*6t zAnOJZ(A2pBn5LgU_YX&99C=_pVDAiebFGzu`Nppw^Guwaoy9c&{DOsXFjdOQ23~7R z$OmZ~mxQxiv09&Vkn9~o{7cY#=f~Z}D~FZKQ}FBM#ArV>jLus`W+ScNO9Exem~5){ zLmD5e`GO9pPmZ(FySA7fg4nSm=uDl_754VdJ6@mbPvH2fRpYF%u1FyZ9;ekFqkkF6 zp|92hIDj6BNI3`8>tECW1V_p*AUE4&O93=Ij7a*gv}DQmE8iw3RCq2w<(aK1!hEEF zKw$(>HXTAv^8mgu-P9ur@{4A$ue+EbCJ+e7sYS*cn7-)B^1?RK?WB8YPfs|vd8M{2 zlfMYcnB4+~AjF*7OkNDz6W)t_L!juNER`D#^5Tm8SZzJ3-Pnr%Me?yy=6`{Qmx(}! zDBfZaky=kB;x5L;vQ$!AWBa>V!c?25g!&dn6O=f+I^$p)_@mz3ydXQ>K`Y=fBlS8O z%r_Q$vyTsLS8kvedQF~Sa>*Me4D>zS{!{^BDIzzTn+~(hPHkYsBkz?ZSI6iod;dm2 zc8>&__xV*ZkqyGfCW$+~`~rBTf5+eAFM(=Re`@*L_KP)=!kMeu_q)5;@XE3Kg{>{8 zaT&mM&`O8(tgq79T#JCn(}jHpkcLWU3*t}vK_HK?;|3FFXM6i&N}l)AfX{OjKMhwe zR^c_uktg9yhKX4h1n#Z^A03YENTt?Rrn9xH9x+l7@8yXlCS0D4T z#T=^!;8b#niKQmcf7x#jA>nl1n@eW{Sz})PQ-{5VK&Pouq)q!!*4YhtL{DF<3l3>f zu4JU7yAapfcabzlhJ6~veZYiYEK6TUo%`;PnEl#vIje^`<$K_Dy~{3sYIV+uc@5B3 zseS7M@Pv2yThoO#C)5gwdB?&Cpb1&lc5FkS=dzlh~9NEzz@#;?7dqU)p3H)Kp{AV6t87ckglWTwY$fK1gt- zpvlxvNc>bWXrNqXHEbo7ui!yJMr^marH5#rYyn~>X4_N{*^VBNpEk$XHF~km+j(Cz z@NpzXilZBy!NMcj=t%`ReHzcXr?0J*2*Ts_D&H@xDpBjJ+a+YYz_Bqh#sg7$*It$VmohC6&_{ckS-;{Ky|FLg+iQtaI`@{-prG<6UjdjEQ0+X|-?R>$GUZau zl|)b^aOgC1xhRg`Z3J&SNspCw4)jj5+qLOIRd4jH6tf4a@u_&KE-~IvcoU-}I!_nYIwXM2Rd%VgFKq5E~_wGi(>ia@K zqmxCa{73m_caR)*c&aK@{Bg^QAMVNDuE|1KKE7*yNT4ULPmdH79^adoJCr3ixo13I zj4^_ru}Iuv6@aiz54}HcB3Iy!1Bz~i^&m&9L?vCaLF1H*&XVNCNDYNYM2ZmX zB!0YO{CKBBC*x0vvFPTC*G>fOOQuiLGxHfQqFSiEW-%hRv6t6!gSgo%rX|4?SB}=q zEP4B`D`#yA!Z;ysKE(QEU-WQ`GMTNH)OxX&ql4ENr(l!VW+c6eY zJY9u;jeY(VGN|5d ze4NVb66=1O3F!8c4`&~OSUl=)6+rPkUwiycHVMoInDVpV^@2?M0xNvxDM0e98e||O zC$BM_PX!c3%OW6!<+dIEzPipvZ}JUjqY3cv=*^G+gIZ*Save$@6WauagiL;bTkUe3 zj;YK~%u7ckH_~~9DqY!iOX>=Hv;h5BX9&gmb=|})|Lg5=n<1|#0BOXINF6{q%x`b- z+@J_K2fxAj&9mZyK>-Tyhwg6ev3uQuomJeydf_Bnx;q!d&H?m$98e*+^Qj7+_#UF+ zxMw9Gf5+0sW@+)KlH1S3L>cqN_4UjcYbysZOR5Km){lsiw?61*^R_MS+;A{>kbxC| zu&VD;Quc1=g988)U^-WB7pA)hF{?ePn{V`f%e-EFwLepK`vP-mxADvC3bzCV_kflO zq|&^wYH(iIC8m7j_;T6*xVM*>UB{dPuF{^CBIKe>3`C>xjf$^df4MvnBVbT0QEv!8 z*?-pLAx%_mGrmCOd7Klbz3s!%EG7o*qTxyw54r(*q{@8{Sr}7npLaPG{O8XQ+F3aJD%#B3m~VQ@w*PQk2i+3&8K{pdMW7Rd2mrDebmiz?n z$w7}$meX-=6;fGTQD3IY6pjfY5_1jvM<64WW4ozA>B{`?z96=NYFHeZn)-CTLCqRx zKKMo_$I71+-cN#&g$0$)^b_IkBgm&pLn3C#nC&VDQk(9FC-Kl`KX|SZNgv|9Uebio z=YhuX7ejfiR{d6eB8O5$^LbEKHxklyN9#u$OI~(w?dv@Kd;gU%zApj1ryaDhf01}U zMa(nGKa^)#FnN;1=l1rhFh|g8Wk`F+rb)D}|N4goI_?&A1M35n8nG@LNkCvxH_*G$imt!b$X(he5z6p&r$l3l6Y6+F-g*L`0 zDUwvNW_Uu7#bqBC{UeP+wXIyiK1mJwlI=0ncG$LhBtHejL0zy$%88X9e*aaB8V^0--wkS-s|LL>{;IDN}^iBVvkF=Nup_y`1ETqfNwID-rIYmi@($%8)~gUmfp zA5YdBHN>5Q|1SG^mSWfuIDST{j)6UlH&3sp_4-cDzRiMqiYLeH^Of&Tn$3@bG-+WH zHg!7nuHva8$K9|=j%?q+$!d*pF3_q!u$%NAKgk5+`Q^vRBgX3;EUb5+${nvkKus)Q zTT7Q!ePApB(gN)_xscgI;+x4Fm1Zj3ZvKG>2N05e;&Gq|JtMf!WqB;YC>Y}C;?e{b zG!Uc2!(xRf*XIcQINU-FJCSMc%iz(7#n&mgM4^%eopu({^S!-Va5VL;*BP@lHm7^z zdI`^U^HimIH#FXV{82vA+RBwp`!j*Q%EE^2ax`uv@)W5QEY z^?{UaZQw>hAA~D?;Y$Cp^BbK-{mcDiXBUYCRN@rjYMt4sUS#dm2PhN<(CLNz>x3;E zK$b^`6|}*Ej(rdjSns^u5(7nR%)E=^%p@DS*c!-+ob+(EDS34$S2yAx+oROzdHq*2 zmp^k5g5?Mxi9BmH%=%OLfRJpD>f9Z34?)x(n}L|OzZ3r9?yo5ww_=N%y*Xo3;R@tx zsw#2`*>y4lE{799sMG77?GJ>DasUiy{0^r7$svwddv7=7pQ9tE@or|`!WfMxP9&E{ zev<(Ieu1`FeXSd_o7U+4aR5{l{0b!5GE^(0-p{wn$u?Z}r_^Ks(5i56Aw54b{cEG^ zcFCFdTt|q&U$kJ7@`QL(w2C&)lLo0ufO8+$Cwpf$0_5wM zp$*l2oDXNYJYVOAk`iznkNzzoJbzp$77ouSgT6o~2tJ;z-e3O7Z_c*Q$G!y^oN|}X z$9&}y!mJ0+C>laKm~EW7unIfx zf%~yAKg(zRLBY*&cgnCo@T;taR*_`>BTcG6?no@p^V_#?e@6?{+Gii(QZYPfer1cM zGVYCw?fWYJwb$Lj;W#GT$R>4ci1X#k_g`7utJ|`S zs=46i9$3gpC}6b#>=QXcr{KWA;E;%#p<<*pN2!-$-Ek?%S*%vq)omOhZ8VtgwRg=D ze?BqW1+modExNC@&=@ojBh$CvPe2j@Fh+o$P!mQmqhJ*L8DuHJ98krI)fvXpw+ zSg*%zw=1 z@TzMs4eq{$jxnluQ@u|GmD77?d(HGK=~Kpf4vaFr=&c#^f0$zalxae0UCO`F2D<`Ya=M9kE~ za;=rEbJzvxvuE!x;Wg_H*;WPybyvUhukS_W{5<&VLe6dYyN5tp((SmD*Z(2Om7f7q zgY&+ylS-@0#qKPhJd9Tp3WZKP9=uiw>6TK^mW6)ze~SaeD7n}7!1?5rorBqUFOe#P z(%8?29L$%mz`^pKxUID{rhNE{qHNrlSAI)_sH9rX%*MHEnwA2al~_|GMuI!VN#O{3-g&*Y_V8?YLWf4x#!=~ zLNyt3ZjkCOzg z?L*OJg)v;f%;3YJ8n#ERD3ul0BYY5Ct57udf%7&SUXz)M3Y z3)1^eKsJ|sr3SlpRkW|1-C$P)dbHAQZ?jPQ>f>wZ>1JzoyW-q2G0r2rfwQ%QnwT^3fD3>B)f zY<6!`vhJcWM2Mqm({nE~g@ zJjZICDWZCu_1V`yYb>0Dm^KYc5xew$QH5fE0ZL!BYquf&}LIbHpff=%VUI;;uxpndV;^|~FE?f7c! z=J$XU@$XyAf(T@3g?D7g(u;%hV@|mCOWx0QQlZb7@5DoflBw^+!>_L^JO>T{w6}M< z$;Br{2yaXJ;iE@@1I>Wos%AW1wuYskB9;t6PS@q%~X|2{w1X+NNtXP(`pxbkEsH5) z*dEy9-o%^o&CILF^=$|faVlT*<5XfoqM#@3{$Wj1Jw3|#fx)Rb6gb~41cGxiY0&Kk zjrP2n>sqEO!p!GX`|K=Sp6}>6m1=7AJx|u%0AL>}+mm+&!02-hcZL5Hs}>sEsZT;b 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 SWCnh7Pg+7jyioMr$NvKvy^w1F literal 0 HcmV?d00001 From 777479b9fb97405418c4d0af4257e9e94305fb7c Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:41:19 +0100 Subject: [PATCH 05/31] chore(desktop): format Electron migration files --- apps/desktop/app.config.ts | 7 +- apps/desktop/electron/backend.cjs | 19 +- apps/desktop/electron/main.cjs | 145 ++++-- apps/desktop/electron/native.cjs | 416 +++++++++++++----- apps/desktop/electron/windows.cjs | 3 +- apps/desktop/scripts/build-electron.mjs | 5 +- apps/desktop/src/app.tsx | 10 +- .../src/components/CapErrorBoundary.tsx | 4 +- apps/desktop/src/components/Cropper.tsx | 10 +- apps/desktop/src/components/Tooltip.tsx | 2 +- .../desktop/src/components/selection-hint.tsx | 2 +- .../src/components/titlebar/Titlebar.tsx | 3 +- .../controls/CaptionControlsMacOS.tsx | 2 +- .../controls/CaptionControlsWindows11.tsx | 2 +- apps/desktop/src/electron/bridge.ts | 8 +- apps/desktop/src/electron/clipboard.ts | 9 +- apps/desktop/src/electron/core.ts | 8 +- apps/desktop/src/electron/deep-link.ts | 4 +- apps/desktop/src/electron/dialog.ts | 60 ++- apps/desktop/src/electron/dpi.ts | 25 +- apps/desktop/src/electron/event.ts | 19 +- apps/desktop/src/electron/fs.ts | 117 ++++- apps/desktop/src/electron/menu.ts | 105 +++-- apps/desktop/src/electron/notification.ts | 17 +- apps/desktop/src/electron/opener.ts | 9 +- apps/desktop/src/electron/os.ts | 18 +- apps/desktop/src/electron/path.ts | 44 +- apps/desktop/src/electron/shell.ts | 54 ++- apps/desktop/src/electron/store.ts | 85 +++- apps/desktop/src/electron/updater.ts | 15 +- apps/desktop/src/routes/(window-chrome).tsx | 7 +- .../(window-chrome)/new-main/CameraSelect.tsx | 2 +- .../new-main/ChangeLogButton.tsx | 4 +- .../new-main/MicrophoneSelect.tsx | 2 +- .../(window-chrome)/new-main/Recents.tsx | 2 +- .../(window-chrome)/new-main/TargetCard.tsx | 8 +- .../routes/(window-chrome)/new-main/index.tsx | 27 +- .../src/routes/(window-chrome)/onboarding.tsx | 10 +- .../src/routes/(window-chrome)/settings.tsx | 10 +- .../(window-chrome)/settings/automations.tsx | 4 +- .../routes/(window-chrome)/settings/cli.tsx | 4 +- .../(window-chrome)/settings/experimental.tsx | 4 +- .../(window-chrome)/settings/feedback.tsx | 4 +- .../(window-chrome)/settings/general.tsx | 4 +- .../(window-chrome)/settings/hotkeys.tsx | 2 +- .../(window-chrome)/settings/recordings.tsx | 8 +- .../(window-chrome)/settings/screenshots.tsx | 6 +- .../src/routes/(window-chrome)/update.tsx | 4 +- .../src/routes/(window-chrome)/upgrade.tsx | 2 +- apps/desktop/src/routes/camera.tsx | 18 +- apps/desktop/src/routes/capture-area.tsx | 14 +- apps/desktop/src/routes/debug.tsx | 2 +- .../src/routes/editor/AudioLibrary.tsx | 2 +- .../desktop/src/routes/editor/CaptionsTab.tsx | 4 +- .../src/routes/editor/ClipsSidebar.tsx | 14 +- .../src/routes/editor/ConfigSidebar.tsx | 13 +- apps/desktop/src/routes/editor/Editor.tsx | 10 +- .../src/routes/editor/EditorErrorScreen.tsx | 4 +- apps/desktop/src/routes/editor/ExportPage.tsx | 10 +- apps/desktop/src/routes/editor/Header.tsx | 8 +- .../src/routes/editor/ImportProgress.tsx | 2 +- .../src/routes/editor/PerformanceOverlay.tsx | 2 +- apps/desktop/src/routes/editor/Player.tsx | 5 +- .../desktop/src/routes/editor/ShareButton.tsx | 2 +- .../src/routes/editor/Timeline/ZoomTrack.tsx | 2 +- .../src/routes/editor/Timeline/index.tsx | 6 +- .../src/routes/editor/TranscriptPage.tsx | 4 +- apps/desktop/src/routes/editor/captions.ts | 2 +- .../src/routes/editor/editor-skeleton.tsx | 2 +- apps/desktop/src/routes/editor/index.tsx | 4 +- apps/desktop/src/routes/editor/utils.ts | 2 +- .../src/routes/in-progress-recording.tsx | 20 +- apps/desktop/src/routes/mode-select.tsx | 6 +- .../desktop/src/routes/recordings-overlay.tsx | 2 +- .../src/routes/screenshot-editor/Editor.tsx | 6 +- .../src/routes/screenshot-editor/Header.tsx | 10 +- .../screenshot-editor/OcrSelectionOverlay.tsx | 2 +- .../src/routes/screenshot-editor/context.tsx | 2 +- .../src/routes/screenshot-editor/index.tsx | 4 +- .../popovers/BackgroundSettingsPopover.tsx | 6 +- .../screenshot-editor-skeleton.tsx | 2 +- .../screenshot-editor/useScreenshotExport.ts | 4 +- .../src/routes/target-select-overlay.tsx | 28 +- apps/desktop/src/routes/teleprompter.tsx | 7 +- .../src/routes/window-capture-occluder.tsx | 2 +- apps/desktop/src/store.ts | 2 +- apps/desktop/src/utils/auth.ts | 8 +- apps/desktop/src/utils/importMedia.ts | 2 +- .../src/utils/macos-window-material.ts | 2 +- apps/desktop/src/utils/recording.ts | 2 +- .../src/utils/teleprompter-window-options.ts | 2 +- apps/desktop/src/utils/teleprompter.ts | 2 +- apps/desktop/src/utils/titlebar-state.ts | 6 +- apps/desktop/src/utils/web-api.ts | 2 +- scripts/build-desktop-binaries.mjs | 5 +- 95 files changed, 1115 insertions(+), 492 deletions(-) diff --git a/apps/desktop/app.config.ts b/apps/desktop/app.config.ts index b48bbc4747e..cbddaa58318 100644 --- a/apps/desktop/app.config.ts +++ b/apps/desktop/app.config.ts @@ -61,12 +61,7 @@ export default defineConfig({ "import.meta.vitest": "undefined", }, optimizeDeps: { - include: [ - "@tanstack/solid-query", - "@openpanel/web", - "uuid", - "cva", - ], + include: ["@tanstack/solid-query", "@openpanel/web", "uuid", "cva"], }, }), }); diff --git a/apps/desktop/electron/backend.cjs b/apps/desktop/electron/backend.cjs index fd893571e9c..18b6ecad292 100644 --- a/apps/desktop/electron/backend.cjs +++ b/apps/desktop/electron/backend.cjs @@ -28,7 +28,8 @@ class RustBackend { this.server.listen(0, "127.0.0.1", resolve); }); const address = this.server.address(); - if (!address || typeof address === "string") throw new Error("Invalid backend address"); + if (!address || typeof address === "string") + throw new Error("Invalid backend address"); this.child = this.spawn(this.binaryPath, [], { cwd: this.resourceDir, env: { @@ -45,7 +46,8 @@ class RustBackend { this.child.stderr.on("data", (data) => process.stderr.write(data)); this.child.once("error", (error) => this.fail(error)); this.child.once("exit", (code, signal) => { - if (!this.stopping) this.fail(new Error(`Rust backend exited (${code ?? signal})`)); + if (!this.stopping) + this.fail(new Error(`Rust backend exited (${code ?? signal})`)); }); return this.ready; } @@ -61,7 +63,8 @@ class RustBackend { socket.on("data", (data) => this.read(data)); socket.once("error", (error) => this.fail(error)); socket.once("close", () => { - if (!this.stopping) this.fail(new Error("Rust backend connection closed")); + if (!this.stopping) + this.fail(new Error("Rust backend connection closed")); }); this.send({ type: "hello", token, protocolVersion: PROTOCOL_VERSION }); } @@ -71,7 +74,9 @@ class RustBackend { while (this.buffer.length >= 4) { const length = this.buffer.readUInt32BE(0); if (length > MAX_FRAME_SIZE) { - this.fail(new Error(`Rust backend frame exceeded ${MAX_FRAME_SIZE} bytes`)); + this.fail( + new Error(`Rust backend frame exceeded ${MAX_FRAME_SIZE} bytes`), + ); return; } if (this.buffer.length < length + 4) return; @@ -87,7 +92,8 @@ class RustBackend { } receive(message) { - if (!this.commands && message.type !== "ready") this.startupMessages.push(message); + if (!this.commands && message.type !== "ready") + this.startupMessages.push(message); if (message.type === "ready") { if (message.protocolVersion !== PROTOCOL_VERSION) { this.fail(new Error("Rust backend protocol version mismatch")); @@ -100,7 +106,8 @@ class RustBackend { const pending = this.pending.get(message.id); if (!pending) return; this.pending.delete(message.id); - if (message.response.status === "ok") pending.resolve(message.response.value); + if (message.response.status === "ok") + pending.resolve(message.response.value); else pending.reject(new Error(message.response.error)); } for (const listener of this.listeners) listener(message); diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index d7e6a870304..3c9420ec869 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -1,4 +1,10 @@ -const { app, ipcMain, net: electronNet, protocol, session } = require("electron"); +const { + app, + ipcMain, + net: electronNet, + protocol, + session, +} = require("electron"); const { spawn } = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); @@ -9,8 +15,24 @@ const { NativeBridge } = require("./native.cjs"); const { WindowManager } = require("./windows.cjs"); protocol.registerSchemesAsPrivileged([ - { scheme: "cap", privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: false } }, - { scheme: "cap-asset", privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true } }, + { + scheme: "cap", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: false, + }, + }, + { + scheme: "cap-asset", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + stream: true, + }, + }, ]); const development = !app.isPackaged; @@ -24,7 +46,10 @@ app.setPath( "userData", process.env.CAP_ELECTRON_USER_DATA_DIR ? path.resolve(process.env.CAP_ELECTRON_USER_DATA_DIR) - : path.join(app.getPath("appData"), development ? "Cap Development" : "so.cap.desktop"), + : path.join( + app.getPath("appData"), + development ? "Cap Development" : "so.cap.desktop", + ), ); if (!development) app.setAsDefaultProtocolClient("cap-desktop"); @@ -32,10 +57,13 @@ const hasSingleInstanceLock = app.requestSingleInstanceLock(); if (!hasSingleInstanceLock) app.quit(); if (hasSingleInstanceLock) { - app.whenReady().then(start).catch((error) => { - console.error(error); - app.exit(1); - }); + app + .whenReady() + .then(start) + .catch((error) => { + console.error(error); + app.exit(1); + }); } app.on("window-all-closed", () => { @@ -91,14 +119,22 @@ async function start() { ? path.join(path.dirname(process.execPath), backendFilename) : path.join(process.resourcesPath, "bin", backendFilename); if (!fs.existsSync(binaryPath)) { - throw new Error(`Rust backend is missing at ${binaryPath}. Run pnpm build:backend first.`); + throw new Error( + `Rust backend is missing at ${binaryPath}. Run pnpm build:backend first.`, + ); } - backend = new RustBackend({ binaryPath, resourceDir: process.resourcesPath, spawn }); + backend = new RustBackend({ + binaryPath, + resourceDir: process.resourcesPath, + spawn, + }); await backend.start(); const windowManager = new WindowManager({ backend, - devUrl: development ? process.env.CAP_DESKTOP_DEV_URL ?? "http://localhost:3002" : null, + devUrl: development + ? (process.env.CAP_DESKTOP_DEV_URL ?? "http://localhost:3002") + : null, publicDir, preloadPath: path.join(__dirname, "preload.cjs"), }); @@ -111,12 +147,24 @@ async function start() { return; } if (message.type === "nativeOperation") { - void nativeBridge.invoke("main", message.operation, message.payload).catch(console.error); + void nativeBridge + .invoke("main", message.operation, message.payload) + .catch(console.error); } if (message.type === "nativeRequest") { void nativeBridge.invoke("main", message.operation, message.payload).then( - (value) => backend.send({ type: "nativeResult", id: message.id, result: { Ok: value ?? null } }), - (error) => backend.send({ type: "nativeResult", id: message.id, result: { Err: String(error) } }), + (value) => + backend.send({ + type: "nativeResult", + id: message.id, + result: { Ok: value ?? null }, + }), + (error) => + backend.send({ + type: "nativeResult", + id: message.id, + result: { Err: String(error) }, + }), ); } }); @@ -125,17 +173,32 @@ async function start() { backend.invoke(request.windowLabel, request.command, request.arguments), ); ipcMain.handle("cap:native", (_event, request) => - nativeBridge.invoke(request.windowLabel, request.operation, request.payload), + nativeBridge.invoke( + request.windowLabel, + request.operation, + request.payload, + ), ); ipcMain.on("cap:emit", (_event, request) => { - backend.send({ type: "event", event: request.event, payload: request.payload ?? null }); + backend.send({ + type: "event", + event: request.event, + payload: request.payload ?? null, + }); }); ipcMain.on("cap:window-drop", (_event, request) => { - windowManager.sendWindowEvent(request.windowLabel, { type: "dragDrop", paths: request.paths }); + windowManager.sendWindowEvent(request.windowLabel, { + type: "dragDrop", + paths: request.paths, + }); }); globalThis.__capMenuSelection = (id) => { for (const window of windowManager.windows.values()) { - window.webContents.send("cap:event", { type: "event", event: `menu:${id}`, payload: null }); + window.webContents.send("cap:event", { + type: "event", + event: `menu:${id}`, + payload: null, + }); } }; queueOpenUrls(process.argv, process.cwd()); @@ -148,13 +211,22 @@ function queueOpenUrls(values, workingDirectory = process.cwd()) { pendingOpenUrls.push(value); continue; } - const candidate = path.isAbsolute(value) ? value : path.resolve(workingDirectory, value); - if (path.extname(candidate).toLowerCase() === ".cap" && fs.existsSync(candidate)) { + const candidate = path.isAbsolute(value) + ? value + : path.resolve(workingDirectory, value); + if ( + path.extname(candidate).toLowerCase() === ".cap" && + fs.existsSync(candidate) + ) { pendingOpenUrls.push(pathToFileURL(candidate).toString()); } } if (backend?.socket && pendingOpenUrls.length > 0) { - backend.send({ type: "event", event: "electron://open-urls", payload: pendingOpenUrls.splice(0) }); + backend.send({ + type: "event", + event: "electron://open-urls", + payload: pendingOpenUrls.splice(0), + }); } } @@ -167,7 +239,10 @@ function rustTargetTriple() { "linux-arm64": "aarch64-unknown-linux-gnu", "linux-x64": "x86_64-unknown-linux-gnu", }[`${process.platform}-${process.arch}`]; - if (!triple) throw new Error(`Unsupported Electron target ${process.platform}-${process.arch}`); + if (!triple) + throw new Error( + `Unsupported Electron target ${process.platform}-${process.arch}`, + ); return triple; } @@ -175,9 +250,13 @@ function registerProtocols(publicDir) { protocol.handle("cap", (request) => { const url = new URL(request.url); let relativePath = decodeURIComponent(url.pathname).replace(/^\/+/, ""); - if (!relativePath || !path.extname(relativePath)) relativePath = "index.html"; + if (!relativePath || !path.extname(relativePath)) + relativePath = "index.html"; const filePath = path.resolve(publicDir, relativePath); - if (!filePath.startsWith(`${path.resolve(publicDir)}${path.sep}`) && filePath !== path.join(publicDir, "index.html")) { + if ( + !filePath.startsWith(`${path.resolve(publicDir)}${path.sep}`) && + filePath !== path.join(publicDir, "index.html") + ) { return new Response("Not found", { status: 404 }); } return electronNet.fetch(pathToFileURL(filePath).toString()); @@ -187,7 +266,8 @@ function registerProtocols(publicDir) { try { const encoded = url.pathname.replace(/^\/+/, ""); const filePath = Buffer.from(encoded, "base64url").toString("utf8"); - if (!path.isAbsolute(filePath)) return new Response("Invalid asset path", { status: 400 }); + if (!path.isAbsolute(filePath)) + return new Response("Invalid asset path", { status: 400 }); return electronNet.fetch(pathToFileURL(filePath).toString()); } catch { return new Response("Invalid asset URL", { status: 400 }); @@ -196,16 +276,23 @@ function registerProtocols(publicDir) { } function configureSecurity() { - session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => { - callback(["media", "notifications", "clipboard-sanitized-write"].includes(permission)); - }); + session.defaultSession.setPermissionRequestHandler( + (_webContents, permission, callback) => { + callback( + ["media", "notifications", "clipboard-sanitized-write"].includes( + permission, + ), + ); + }, + ); app.on("web-contents-created", (_event, contents) => { contents.setWindowOpenHandler(({ url }) => { require("electron").shell.openExternal(url); return { action: "deny" }; }); contents.on("will-navigate", (event, url) => { - if (!url.startsWith("cap://") && !url.startsWith("http://localhost:3002")) event.preventDefault(); + if (!url.startsWith("cap://") && !url.startsWith("http://localhost:3002")) + event.preventDefault(); }); }); } diff --git a/apps/desktop/electron/native.cjs b/apps/desktop/electron/native.cjs index 8aa32fc4191..52d5be56b52 100644 --- a/apps/desktop/electron/native.cjs +++ b/apps/desktop/electron/native.cjs @@ -1,4 +1,16 @@ -const { app, clipboard, dialog, globalShortcut, Menu, nativeImage, nativeTheme, Notification, screen, shell, Tray } = require("electron"); +const { + app, + clipboard, + dialog, + globalShortcut, + Menu, + nativeImage, + nativeTheme, + Notification, + screen, + shell, + Tray, +} = require("electron"); const fs = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); @@ -20,7 +32,10 @@ class NativeBridge { this.windowManager.backend.send({ type: "event", event: "update-download-progress", - payload: { downloaded: Math.min(progress.transferred, 0xffffffff), total: Math.min(progress.total, 0xffffffff) }, + payload: { + downloaded: Math.min(progress.transferred, 0xffffffff), + total: Math.min(progress.total, 0xffffffff), + }, }); }); autoUpdater.on("update-downloaded", (info) => { @@ -35,79 +50,191 @@ class NativeBridge { async invoke(windowLabel, operation, payload = {}) { const window = this.windowManager.get(windowLabel); switch (operation) { - case "app.exit": this.windowManager.beginQuit(); app.exit(payload.exitCode ?? 0); return; - case "app.dockVisibility": if (app.dock) payload.visible ? app.dock.show() : app.dock.hide(); return; - case "app.activationPolicy": if (process.platform === "darwin") app.setActivationPolicy(payload.policy); return; - case "app.version": return app.getVersion(); - case "app.name": return app.getName(); - case "app.relaunch": app.relaunch(); app.exit(0); return; - case "clipboard.readText": return clipboard.readText(); - case "clipboard.writeText": clipboard.writeText(payload.text); return; - case "dialog.open": return this.openDialog(window, payload); - case "dialog.save": return this.saveDialog(window, payload); - case "dialog.message": return this.messageDialog(window, payload); - case "dialog.ask": return this.questionDialog(window, payload, false); - case "dialog.confirm": return this.questionDialog(window, payload, true); - case "fs.exists": return this.exists(resolveBaseDirectory(payload.path, payload.baseDir)); - case "fs.readFile": return Array.from(await fs.readFile(resolveBaseDirectory(payload.path, payload.baseDir))); - case "fs.readTextFile": return fs.readFile(resolveBaseDirectory(payload.path, payload.baseDir), "utf8"); - case "fs.writeFile": await writeFile(payload); return; - case "fs.writeTextFile": await writeTextFile(payload); return; - case "fs.remove": await fs.rm(resolveBaseDirectory(payload.path, payload.baseDir), { recursive: payload.recursive ?? false, force: true }); return; - case "fs.mkdir": await fs.mkdir(resolveBaseDirectory(payload.path, payload.baseDir), { recursive: payload.recursive ?? false }); return; - case "fs.readDir": return readDir(resolveBaseDirectory(payload.path, payload.baseDir)); - case "fs.stat": return stat(resolveBaseDirectory(payload.path, payload.baseDir)); - case "hotkeys.configure": return this.configureHotkeys(payload.hotkeys ?? []); - case "hotkeys.escapeEnabled": return this.setEscapeEnabled(payload.enabled); - case "menu.popup": return this.popupMenu(window, payload.items, payload.position); - case "notification.permission": return Notification.isSupported() ? "granted" : "denied"; - case "notification.requestPermission": return Notification.isSupported() ? "granted" : "denied"; - case "notification.show": new Notification(payload).show(); return; - case "opener.openPath": return shell.openPath(payload.path); - case "opener.revealItemInDir": shell.showItemInFolder(payload.path); return; - case "os.arch": return normalizeArch(process.arch); - case "os.hostname": return os.hostname(); - case "os.locale": return app.getLocale(); - case "os.platform": return process.platform; - case "os.type": return normalizeType(process.platform); - case "os.version": return os.release(); - case "path.appDataDir": return withSeparator(app.getPath("userData")); - case "path.appLocalDataDir": return withSeparator(app.getPath("userData")); - case "path.audioDir": return withSeparator(app.getPath("music")); - case "path.cacheDir": return withSeparator(app.getPath("sessionData")); - case "path.configDir": return withSeparator(app.getPath("userData")); - case "path.dataDir": return withSeparator(app.getPath("appData")); - case "path.desktopDir": return withSeparator(app.getPath("desktop")); - case "path.documentDir": return withSeparator(app.getPath("documents")); - case "path.downloadDir": return withSeparator(app.getPath("downloads")); - case "path.homeDir": return withSeparator(app.getPath("home")); - case "path.pictureDir": return withSeparator(app.getPath("pictures")); - case "path.resourceDir": return withSeparator(process.resourcesPath); - case "path.tempDir": return withSeparator(app.getPath("temp")); - case "path.videoDir": return withSeparator(app.getPath("videos")); - case "path.join": return path.join(...payload.paths); - case "path.resolve": return path.resolve(...payload.paths); - case "shell.open": await shell.openExternal(payload.path); return; - case "shell.openPath": return shell.openPath(payload.path); - case "store.load": return this.loadStore(payload.path); - case "store.save": return this.saveStore(payload.path, payload.value); - case "theme.get": return nativeTheme.shouldUseDarkColors ? "dark" : "light"; - case "tray.configure": return this.configureTray(payload); - case "tray.setMode": this.trayMode = payload.mode ?? "studio"; return this.updateTrayIcon(); - case "tray.setRecording": this.trayRecording = payload.recording === true; return this.updateTrayIcon(); - case "updater.check": return this.checkForUpdates(payload.channel); - case "updater.configure": return this.configureUpdater(payload.channel); - case "updater.downloadAndInstall": return this.downloadAndInstall(payload.channel); - case "updater.setChannel": this.updateChannel = payload.channel ?? "stable"; return this.configureUpdater(this.updateChannel); - case "window.action": return this.windowAction(windowLabel, payload); - case "window.all": return [...this.windowManager.windows.keys()]; - case "window.create": this.windowManager.create(payload); return payload.label; - case "window.currentMonitor": return this.windowManager.normalizeDisplay(window ? screen.getDisplayMatching(window.getBounds()) : screen.getPrimaryDisplay()); - case "window.primaryMonitor": return this.windowManager.normalizeDisplay(screen.getPrimaryDisplay()); - case "window.availableMonitors": return screen.getAllDisplays().map((display) => this.windowManager.normalizeDisplay(display)); - case "window.monitorFromPoint": return this.windowManager.normalizeDisplay(this.windowManager.displayFromPhysicalPoint(payload)); - case "window.teardownVisualEffects": return; - default: throw new Error(`Unknown Electron native operation '${operation}'`); + case "app.exit": + this.windowManager.beginQuit(); + app.exit(payload.exitCode ?? 0); + return; + case "app.dockVisibility": + if (app.dock) payload.visible ? app.dock.show() : app.dock.hide(); + return; + case "app.activationPolicy": + if (process.platform === "darwin") + app.setActivationPolicy(payload.policy); + return; + case "app.version": + return app.getVersion(); + case "app.name": + return app.getName(); + case "app.relaunch": + app.relaunch(); + app.exit(0); + return; + case "clipboard.readText": + return clipboard.readText(); + case "clipboard.writeText": + clipboard.writeText(payload.text); + return; + case "dialog.open": + return this.openDialog(window, payload); + case "dialog.save": + return this.saveDialog(window, payload); + case "dialog.message": + return this.messageDialog(window, payload); + case "dialog.ask": + return this.questionDialog(window, payload, false); + case "dialog.confirm": + return this.questionDialog(window, payload, true); + case "fs.exists": + return this.exists(resolveBaseDirectory(payload.path, payload.baseDir)); + case "fs.readFile": + return Array.from( + await fs.readFile( + resolveBaseDirectory(payload.path, payload.baseDir), + ), + ); + case "fs.readTextFile": + return fs.readFile( + resolveBaseDirectory(payload.path, payload.baseDir), + "utf8", + ); + case "fs.writeFile": + await writeFile(payload); + return; + case "fs.writeTextFile": + await writeTextFile(payload); + return; + case "fs.remove": + await fs.rm(resolveBaseDirectory(payload.path, payload.baseDir), { + recursive: payload.recursive ?? false, + force: true, + }); + return; + case "fs.mkdir": + await fs.mkdir(resolveBaseDirectory(payload.path, payload.baseDir), { + recursive: payload.recursive ?? false, + }); + return; + case "fs.readDir": + return readDir(resolveBaseDirectory(payload.path, payload.baseDir)); + case "fs.stat": + return stat(resolveBaseDirectory(payload.path, payload.baseDir)); + case "hotkeys.configure": + return this.configureHotkeys(payload.hotkeys ?? []); + case "hotkeys.escapeEnabled": + return this.setEscapeEnabled(payload.enabled); + case "menu.popup": + return this.popupMenu(window, payload.items, payload.position); + case "notification.permission": + return Notification.isSupported() ? "granted" : "denied"; + case "notification.requestPermission": + return Notification.isSupported() ? "granted" : "denied"; + case "notification.show": + new Notification(payload).show(); + return; + case "opener.openPath": + return shell.openPath(payload.path); + case "opener.revealItemInDir": + shell.showItemInFolder(payload.path); + return; + case "os.arch": + return normalizeArch(process.arch); + case "os.hostname": + return os.hostname(); + case "os.locale": + return app.getLocale(); + case "os.platform": + return process.platform; + case "os.type": + return normalizeType(process.platform); + case "os.version": + return os.release(); + case "path.appDataDir": + return withSeparator(app.getPath("userData")); + case "path.appLocalDataDir": + return withSeparator(app.getPath("userData")); + case "path.audioDir": + return withSeparator(app.getPath("music")); + case "path.cacheDir": + return withSeparator(app.getPath("sessionData")); + case "path.configDir": + return withSeparator(app.getPath("userData")); + case "path.dataDir": + return withSeparator(app.getPath("appData")); + case "path.desktopDir": + return withSeparator(app.getPath("desktop")); + case "path.documentDir": + return withSeparator(app.getPath("documents")); + case "path.downloadDir": + return withSeparator(app.getPath("downloads")); + case "path.homeDir": + return withSeparator(app.getPath("home")); + case "path.pictureDir": + return withSeparator(app.getPath("pictures")); + case "path.resourceDir": + return withSeparator(process.resourcesPath); + case "path.tempDir": + return withSeparator(app.getPath("temp")); + case "path.videoDir": + return withSeparator(app.getPath("videos")); + case "path.join": + return path.join(...payload.paths); + case "path.resolve": + return path.resolve(...payload.paths); + case "shell.open": + await shell.openExternal(payload.path); + return; + case "shell.openPath": + return shell.openPath(payload.path); + case "store.load": + return this.loadStore(payload.path); + case "store.save": + return this.saveStore(payload.path, payload.value); + case "theme.get": + return nativeTheme.shouldUseDarkColors ? "dark" : "light"; + case "tray.configure": + return this.configureTray(payload); + case "tray.setMode": + this.trayMode = payload.mode ?? "studio"; + return this.updateTrayIcon(); + case "tray.setRecording": + this.trayRecording = payload.recording === true; + return this.updateTrayIcon(); + case "updater.check": + return this.checkForUpdates(payload.channel); + case "updater.configure": + return this.configureUpdater(payload.channel); + case "updater.downloadAndInstall": + return this.downloadAndInstall(payload.channel); + case "updater.setChannel": + this.updateChannel = payload.channel ?? "stable"; + return this.configureUpdater(this.updateChannel); + case "window.action": + return this.windowAction(windowLabel, payload); + case "window.all": + return [...this.windowManager.windows.keys()]; + case "window.create": + this.windowManager.create(payload); + return payload.label; + case "window.currentMonitor": + return this.windowManager.normalizeDisplay( + window + ? screen.getDisplayMatching(window.getBounds()) + : screen.getPrimaryDisplay(), + ); + case "window.primaryMonitor": + return this.windowManager.normalizeDisplay(screen.getPrimaryDisplay()); + case "window.availableMonitors": + return screen + .getAllDisplays() + .map((display) => this.windowManager.normalizeDisplay(display)); + case "window.monitorFromPoint": + return this.windowManager.normalizeDisplay( + this.windowManager.displayFromPhysicalPoint(payload), + ); + case "window.teardownVisualEffects": + return; + default: + throw new Error(`Unknown Electron native operation '${operation}'`); } } @@ -138,7 +265,8 @@ class NativeBridge { } async messageDialog(window, options) { - const normalized = typeof options === "string" ? { message: options } : options; + const normalized = + typeof options === "string" ? { message: options } : options; const buttons = Array.isArray(normalized.buttons) ? normalized.buttons : normalized.buttons @@ -155,13 +283,17 @@ class NativeBridge { } async questionDialog(window, options, isConfirm) { - const normalized = typeof options === "string" ? { message: options } : options; + const normalized = + typeof options === "string" ? { message: options } : options; const result = await dialog.showMessageBox(window, { type: normalized.kind ?? "question", title: normalized.title, message: normalized.message, detail: normalized.detail, - buttons: [normalized.okLabel ?? (isConfirm ? "OK" : "Yes"), normalized.cancelLabel ?? (isConfirm ? "Cancel" : "No")], + buttons: [ + normalized.okLabel ?? (isConfirm ? "OK" : "Yes"), + normalized.cancelLabel ?? (isConfirm ? "Cancel" : "No"), + ], defaultId: 0, cancelId: 1, }); @@ -210,35 +342,61 @@ class NativeBridge { windowAction(label, payload) { const window = this.windowManager.get(payload.label ?? label); - if (!window || window.isDestroyed()) throw new Error(`Window '${payload.label ?? label}' does not exist`); - if (payload.action === "state") return this.windowManager.windowState(window); - if (payload.action === "cursorPosition") return this.windowManager.cursorPosition?.() ?? { x: 0, y: 0 }; - this.windowManager.operation(window, { type: payload.action, ...payload.value }); + if (!window || window.isDestroyed()) + throw new Error(`Window '${payload.label ?? label}' does not exist`); + if (payload.action === "state") + return this.windowManager.windowState(window); + if (payload.action === "cursorPosition") + return this.windowManager.cursorPosition?.() ?? { x: 0, y: 0 }; + this.windowManager.operation(window, { + type: payload.action, + ...payload.value, + }); } configureHotkeys(hotkeys) { - for (const accelerator of this.hotkeyAccelerators) globalShortcut.unregister(accelerator); + for (const accelerator of this.hotkeyAccelerators) + globalShortcut.unregister(accelerator); this.hotkeyAccelerators.clear(); for (const hotkey of hotkeys) { - if (!hotkey.accelerator || this.hotkeyAccelerators.has(hotkey.accelerator)) continue; - if (globalShortcut.register(hotkey.accelerator, () => { + if ( + !hotkey.accelerator || + this.hotkeyAccelerators.has(hotkey.accelerator) + ) + continue; + if ( + globalShortcut.register(hotkey.accelerator, () => { + this.windowManager.backend.send({ + type: "event", + event: "hotkey://trigger", + payload: hotkey.action, + }); + }) + ) + this.hotkeyAccelerators.add(hotkey.accelerator); + } + const settingsAccelerator = "CommandOrControl+,"; + if ( + !this.hotkeyAccelerators.has(settingsAccelerator) && + globalShortcut.register(settingsAccelerator, () => { this.windowManager.backend.send({ type: "event", - event: "hotkey://trigger", - payload: hotkey.action, + event: "hotkey://settings", + payload: null, }); - })) this.hotkeyAccelerators.add(hotkey.accelerator); - } - const settingsAccelerator = "CommandOrControl+,"; - if (!this.hotkeyAccelerators.has(settingsAccelerator) && globalShortcut.register(settingsAccelerator, () => { - this.windowManager.backend.send({ type: "event", event: "hotkey://settings", payload: null }); - })) this.hotkeyAccelerators.add(settingsAccelerator); + }) + ) + this.hotkeyAccelerators.add(settingsAccelerator); } setEscapeEnabled(enabled) { if (enabled && !this.escapeRegistered) { this.escapeRegistered = globalShortcut.register("Escape", () => { - this.windowManager.backend.send({ type: "event", event: "hotkey://escape", payload: null }); + this.windowManager.backend.send({ + type: "event", + event: "hotkey://escape", + payload: null, + }); }); } else if (!enabled && this.escapeRegistered) { globalShortcut.unregister("Escape"); @@ -256,7 +414,11 @@ class NativeBridge { else this.tray.popUpContextMenu(); }); } - this.tray.setContextMenu(Menu.buildFromTemplate((payload.items ?? []).map((item) => this.trayMenuItem(item)))); + this.tray.setContextMenu( + Menu.buildFromTemplate( + (payload.items ?? []).map((item) => this.trayMenuItem(item)), + ), + ); this.updateTrayIcon(); } @@ -274,7 +436,11 @@ class NativeBridge { } emitTrayClick(id) { - this.windowManager.backend.send({ type: "event", event: "tray://click", payload: id }); + this.windowManager.backend.send({ + type: "event", + event: "tray://click", + payload: id, + }); } updateTrayIcon() { @@ -303,25 +469,41 @@ class NativeBridge { this.setUpdaterFeed(channel); if (this.updateTimer) clearInterval(this.updateTimer); if (!app.isPackaged || channel !== "nightly") return; - this.updateTimer = setInterval(() => void this.checkForUpdates(channel).catch(console.error), 2 * 60 * 60 * 1000); + this.updateTimer = setInterval( + () => void this.checkForUpdates(channel).catch(console.error), + 2 * 60 * 60 * 1000, + ); } setUpdaterFeed(channel) { if (!app.isPackaged) return; autoUpdater.channel = channel === "nightly" ? "nightly" : "latest"; const baseUrl = process.env.CAP_ELECTRON_UPDATE_URL; - if (baseUrl) autoUpdater.setFeedURL({ provider: "generic", url: `${baseUrl}/${channel}` }); - else autoUpdater.setFeedURL({ provider: "github", owner: "CapSoftware", repo: "Cap" }); + if (baseUrl) + autoUpdater.setFeedURL({ + provider: "generic", + url: `${baseUrl}/${channel}`, + }); + else + autoUpdater.setFeedURL({ + provider: "github", + owner: "CapSoftware", + repo: "Cap", + }); } async checkForUpdates(channel = this.updateChannel) { if (!app.isPackaged) return null; this.setUpdaterFeed(channel); const result = await autoUpdater.checkForUpdates(); - if (!result?.updateInfo || result.updateInfo.version === app.getVersion()) return null; + if (!result?.updateInfo || result.updateInfo.version === app.getVersion()) + return null; return { version: result.updateInfo.version, - notes: typeof result.updateInfo.releaseNotes === "string" ? result.updateInfo.releaseNotes : null, + notes: + typeof result.updateInfo.releaseNotes === "string" + ? result.updateInfo.releaseNotes + : null, channel, }; } @@ -350,29 +532,47 @@ function normalizeMenuItem(item) { } function normalizeFilters(filters) { - return filters?.map((filter) => ({ name: filter.name, extensions: filter.extensions })); + return filters?.map((filter) => ({ + name: filter.name, + extensions: filter.extensions, + })); } function normalizeArch(arch) { - return ({ x64: "x86_64", arm64: "aarch64", ia32: "x86" })[arch] ?? arch; + return { x64: "x86_64", arm64: "aarch64", ia32: "x86" }[arch] ?? arch; } function normalizeType(platform) { - return ({ darwin: "macos", win32: "windows", linux: "linux" })[platform] ?? platform; + return ( + { darwin: "macos", win32: "windows", linux: "linux" }[platform] ?? platform + ); } function resolveBaseDirectory(filePath, baseDir) { if (!baseDir || path.isAbsolute(filePath)) return filePath; const names = { - 1: "audioDir", 2: "cacheDir", 3: "configDir", 4: "dataDir", 6: "localDataDir", - 7: "desktop", 8: "documents", 9: "downloads", 11: "home", 15: "pictures", - 18: "public", 20: "resource", 21: "temp", 22: "templates", 24: "videos", + 1: "audioDir", + 2: "cacheDir", + 3: "configDir", + 4: "dataDir", + 6: "localDataDir", + 7: "desktop", + 8: "documents", + 9: "downloads", + 11: "home", + 15: "pictures", + 18: "public", + 20: "resource", + 21: "temp", + 22: "templates", + 24: "videos", }; const name = names[baseDir]; if (name === "resource") return path.join(process.resourcesPath, filePath); if (name === "temp") return path.join(app.getPath("temp"), filePath); if (name === "home") return path.join(app.getPath("home"), filePath); - if (name === "localDataDir" || name === "configDir") return path.join(app.getPath("userData"), filePath); + if (name === "localDataDir" || name === "configDir") + return path.join(app.getPath("userData"), filePath); if (!name) throw new Error(`Unsupported base directory '${baseDir}'`); return path.join(app.getPath(name), filePath); } diff --git a/apps/desktop/electron/windows.cjs b/apps/desktop/electron/windows.cjs index 7fe8cabede1..8f767a812ca 100644 --- a/apps/desktop/electron/windows.cjs +++ b/apps/desktop/electron/windows.cjs @@ -329,7 +329,8 @@ class WindowManager { const source = ["system", "light", "dark"].includes(operation.theme) ? operation.theme : "system"; - if (nativeTheme.themeSource !== source) nativeTheme.themeSource = source; + if (nativeTheme.themeSource !== source) + nativeTheme.themeSource = source; break; } case "setTrafficLightPosition": diff --git a/apps/desktop/scripts/build-electron.mjs b/apps/desktop/scripts/build-electron.mjs index 2f008346977..6452e4bbb95 100644 --- a/apps/desktop/scripts/build-electron.mjs +++ b/apps/desktop/scripts/build-electron.mjs @@ -4,7 +4,10 @@ import { fileURLToPath } from "node:url"; import { build } from "esbuild"; -const desktopDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const desktopDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); const outputDir = path.join(desktopDir, ".electron-dist"); await rm(outputDir, { recursive: true, force: true }); diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx index 3eb053c327c..f2920eb3f2f 100644 --- a/apps/desktop/src/app.tsx +++ b/apps/desktop/src/app.tsx @@ -1,10 +1,5 @@ import { Route, Router, useCurrentMatches } from "@solidjs/router"; import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; -import { - getCurrentWebviewWindow, - type WebviewWindow, -} from "~/electron/webviewWindow"; -import { message } from "~/electron/dialog"; import { createEffect, createSignal, @@ -14,6 +9,11 @@ import { Suspense, } from "solid-js"; import { Toaster } from "solid-toast"; +import { message } from "~/electron/dialog"; +import { + getCurrentWebviewWindow, + type WebviewWindow, +} from "~/electron/webviewWindow"; import "@cap/ui-solid/main.css"; import "unfonts.css"; diff --git a/apps/desktop/src/components/CapErrorBoundary.tsx b/apps/desktop/src/components/CapErrorBoundary.tsx index 01960ca7701..21de4c17dc4 100644 --- a/apps/desktop/src/components/CapErrorBoundary.tsx +++ b/apps/desktop/src/components/CapErrorBoundary.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; -import { getCurrentWebviewWindow } from "~/electron/webviewWindow"; -import { writeText } from "~/electron/clipboard"; import { ErrorBoundary, type ParentProps } from "solid-js"; +import { writeText } from "~/electron/clipboard"; +import { getCurrentWebviewWindow } from "~/electron/webviewWindow"; export function CapErrorBoundary(props: ParentProps) { return ( diff --git a/apps/desktop/src/components/Cropper.tsx b/apps/desktop/src/components/Cropper.tsx index f8e3a9a3640..a64846575d7 100644 --- a/apps/desktop/src/components/Cropper.tsx +++ b/apps/desktop/src/components/Cropper.tsx @@ -1,10 +1,5 @@ import { createEventListenerMap } from "@solid-primitives/event-listener"; import { createResizeObserver } from "@solid-primitives/resize-observer"; -import type { - CheckMenuItemOptions, - PredefinedMenuItemOptions, -} from "~/electron/menu"; -import { type as ostype } from "~/electron/os"; import { type Accessor, children, @@ -21,6 +16,11 @@ import { } from "solid-js"; import { createStore } from "solid-js/store"; import { Transition } from "solid-transition-group"; +import type { + CheckMenuItemOptions, + PredefinedMenuItemOptions, +} from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; import { createKeyDownSignal } from "~/utils/events"; import { commands } from "~/utils/tauri"; diff --git a/apps/desktop/src/components/Tooltip.tsx b/apps/desktop/src/components/Tooltip.tsx index 6ba00b17263..b1ed58bebb2 100644 --- a/apps/desktop/src/components/Tooltip.tsx +++ b/apps/desktop/src/components/Tooltip.tsx @@ -1,7 +1,7 @@ import { Tooltip as KTooltip } from "@kobalte/core/tooltip"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import type { ComponentProps, JSX } from "solid-js"; +import { type as ostype } from "~/electron/os"; interface Props extends ComponentProps { content?: JSX.Element; diff --git a/apps/desktop/src/components/selection-hint.tsx b/apps/desktop/src/components/selection-hint.tsx index 4c2bef078d9..7899765992d 100644 --- a/apps/desktop/src/components/selection-hint.tsx +++ b/apps/desktop/src/components/selection-hint.tsx @@ -1,5 +1,5 @@ -import { type as ostype } from "~/electron/os"; import { Match, Show, Switch } from "solid-js"; +import { type as ostype } from "~/electron/os"; export type SelectionHintProps = { show: boolean; diff --git a/apps/desktop/src/components/titlebar/Titlebar.tsx b/apps/desktop/src/components/titlebar/Titlebar.tsx index 885284f403c..535c6ccaa1d 100644 --- a/apps/desktop/src/components/titlebar/Titlebar.tsx +++ b/apps/desktop/src/components/titlebar/Titlebar.tsx @@ -1,7 +1,8 @@ // Credits: tauri-controls -import { type } from "~/electron/os"; + import { cx } from "cva"; import { type ComponentProps, Match, Switch, splitProps } from "solid-js"; +import { type } from "~/electron/os"; import titlebarState from "~/utils/titlebar-state"; import CaptionControlsWindows11 from "./controls/CaptionControlsWindows11"; diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx index 229d8075995..09d1cda9b1d 100644 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx @@ -1,4 +1,3 @@ -import { getCurrentWindow } from "~/electron/window"; import { cx } from "cva"; import { type ComponentProps, @@ -8,6 +7,7 @@ import { Show, splitProps, } from "solid-js"; +import { getCurrentWindow } from "~/electron/window"; export default function CaptionControlsMacOS( props: ComponentProps<"div"> & { diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx index 1c9ecfe0875..f30500a1e2c 100644 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx @@ -1,4 +1,3 @@ -import { getCurrentWindow } from "~/electron/window"; import { cx } from "cva"; import { type ComponentProps, @@ -9,6 +8,7 @@ import { Show, splitProps, } from "solid-js"; +import { getCurrentWindow } from "~/electron/window"; import titlebarState from "~/utils/titlebar-state"; import { WindowControlButton as ControlButton } from "./WindowControlButton"; diff --git a/apps/desktop/src/electron/bridge.ts b/apps/desktop/src/electron/bridge.ts index a0efe64381d..3cc77df5ea7 100644 --- a/apps/desktop/src/electron/bridge.ts +++ b/apps/desktop/src/electron/bridge.ts @@ -31,7 +31,8 @@ declare global { } export function bridge() { - if (!window.capElectron) throw new Error("Cap Electron preload bridge is unavailable"); + if (!window.capElectron) + throw new Error("Cap Electron preload bridge is unavailable"); return window.capElectron; } @@ -39,6 +40,9 @@ export function toIpcValue(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } -export function native(operation: string, payload?: Record) { +export function native( + operation: string, + payload?: Record, +) { return bridge().native(operation, toIpcValue(payload ?? {})); } diff --git a/apps/desktop/src/electron/clipboard.ts b/apps/desktop/src/electron/clipboard.ts index 12611b626f3..f090864a6a7 100644 --- a/apps/desktop/src/electron/clipboard.ts +++ b/apps/desktop/src/electron/clipboard.ts @@ -1,6 +1,9 @@ import { native } from "./bridge"; export const readText = () => native("clipboard.readText"); -export const writeText = (text: string) => native("clipboard.writeText", { text }); -export const readImage = () => Promise.reject(new Error("readImage is not implemented")); -export const writeImage = (_image: unknown) => Promise.reject(new Error("writeImage is not implemented")); +export const writeText = (text: string) => + native("clipboard.writeText", { text }); +export const readImage = () => + Promise.reject(new Error("readImage is not implemented")); +export const writeImage = (_image: unknown) => + Promise.reject(new Error("writeImage is not implemented")); export const clear = () => writeText(""); diff --git a/apps/desktop/src/electron/core.ts b/apps/desktop/src/electron/core.ts index fb9d8d1a7f6..383943b2a18 100644 --- a/apps/desktop/src/electron/core.ts +++ b/apps/desktop/src/electron/core.ts @@ -33,12 +33,16 @@ export class Channel { } } -export async function invoke(command: string, args?: Record): Promise { +export async function invoke( + command: string, + args?: Record, +): Promise { return bridge().invoke(command, toIpcValue(args ?? {})); } export function convertFileSrc(filePath: string, protocol = "asset") { - if (protocol !== "asset") return `${protocol}://localhost/${encodeURIComponent(filePath)}`; + if (protocol !== "asset") + return `${protocol}://localhost/${encodeURIComponent(filePath)}`; const bytes = new TextEncoder().encode(filePath); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); diff --git a/apps/desktop/src/electron/deep-link.ts b/apps/desktop/src/electron/deep-link.ts index fe3b2831144..19ce16e8d09 100644 --- a/apps/desktop/src/electron/deep-link.ts +++ b/apps/desktop/src/electron/deep-link.ts @@ -1,6 +1,8 @@ import { listen, type UnlistenFn } from "./event"; export const getCurrent = () => Promise.resolve(null); -export const onOpenUrl = (handler: (urls: string[]) => void): Promise => +export const onOpenUrl = ( + handler: (urls: string[]) => void, +): Promise => listen("deep-link://new-url", ({ payload }) => handler(payload)); export const register = () => Promise.resolve(); export const unregister = () => Promise.resolve(); diff --git a/apps/desktop/src/electron/dialog.ts b/apps/desktop/src/electron/dialog.ts index e79d371b481..88d7ae70b51 100644 --- a/apps/desktop/src/electron/dialog.ts +++ b/apps/desktop/src/electron/dialog.ts @@ -1,6 +1,9 @@ import { native } from "./bridge"; -export interface DialogFilter { name: string; extensions: string[]; } +export interface DialogFilter { + name: string; + extensions: string[]; +} export interface OpenDialogOptions { title?: string; filters?: DialogFilter[]; @@ -10,18 +13,51 @@ export interface OpenDialogOptions { recursive?: boolean; canCreateDirectories?: boolean; } -export interface SaveDialogOptions { title?: string; filters?: DialogFilter[]; defaultPath?: string; } -export interface MessageDialogOptions { title?: string; kind?: "info" | "warning" | "error"; okLabel?: string; buttons?: string[] | Record; } -export interface ConfirmDialogOptions extends MessageDialogOptions { cancelLabel?: string; } +export interface SaveDialogOptions { + title?: string; + filters?: DialogFilter[]; + defaultPath?: string; +} +export interface MessageDialogOptions { + title?: string; + kind?: "info" | "warning" | "error"; + okLabel?: string; + buttons?: string[] | Record; +} +export interface ConfirmDialogOptions extends MessageDialogOptions { + cancelLabel?: string; +} -export function open(options: OpenDialogOptions = {}) { return native("dialog.open", { ...options }); } -export function save(options: SaveDialogOptions = {}) { return native("dialog.save", { ...options }); } -export function message(message_: string, options: string | MessageDialogOptions = {}) { - return native("dialog.message", { message: message_, ...(typeof options === "string" ? { title: options } : options) }); +export function open(options: OpenDialogOptions = {}) { + return native("dialog.open", { ...options }); +} +export function save(options: SaveDialogOptions = {}) { + return native("dialog.save", { ...options }); +} +export function message( + message_: string, + options: string | MessageDialogOptions = {}, +) { + return native("dialog.message", { + message: message_, + ...(typeof options === "string" ? { title: options } : options), + }); } -export function ask(message_: string, options: string | ConfirmDialogOptions = {}) { - return native("dialog.ask", { message: message_, ...(typeof options === "string" ? { title: options } : options) }); +export function ask( + message_: string, + options: string | ConfirmDialogOptions = {}, +) { + return native("dialog.ask", { + message: message_, + ...(typeof options === "string" ? { title: options } : options), + }); } -export function confirm(message_: string, options: string | ConfirmDialogOptions = {}) { - return native("dialog.confirm", { message: message_, ...(typeof options === "string" ? { title: options } : options) }); +export function confirm( + message_: string, + options: string | ConfirmDialogOptions = {}, +) { + return native("dialog.confirm", { + message: message_, + ...(typeof options === "string" ? { title: options } : options), + }); } diff --git a/apps/desktop/src/electron/dpi.ts b/apps/desktop/src/electron/dpi.ts index 5bff4672f1c..a7caa029b2e 100644 --- a/apps/desktop/src/electron/dpi.ts +++ b/apps/desktop/src/electron/dpi.ts @@ -1,26 +1,41 @@ export class LogicalPosition { - constructor(public x: T, public y: T) {} + constructor( + public x: T, + public y: T, + ) {} toPhysical(scaleFactor: number) { return new PhysicalPosition(this.x * scaleFactor, this.y * scaleFactor); } } export class PhysicalPosition { - constructor(public x: T, public y: T) {} + constructor( + public x: T, + public y: T, + ) {} toLogical(scaleFactor: number) { return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor); } } export class LogicalSize { - constructor(public width: T, public height: T) {} + constructor( + public width: T, + public height: T, + ) {} toPhysical(scaleFactor: number) { - return new PhysicalSize(this.width * scaleFactor, this.height * scaleFactor); + return new PhysicalSize( + this.width * scaleFactor, + this.height * scaleFactor, + ); } } export class PhysicalSize { - constructor(public width: T, public height: T) {} + constructor( + public width: T, + public height: T, + ) {} toLogical(scaleFactor: number) { return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor); } diff --git a/apps/desktop/src/electron/event.ts b/apps/desktop/src/electron/event.ts index 1dfe77a7cc7..49374b0274f 100644 --- a/apps/desktop/src/electron/event.ts +++ b/apps/desktop/src/electron/event.ts @@ -12,14 +12,21 @@ export type EventCallback = (event: Event) => void; let nextEventId = 1; -export async function listen(event: string, handler: EventCallback): Promise { +export async function listen( + event: string, + handler: EventCallback, +): Promise { const id = nextEventId++; return bridge().onEvent((message) => { - if (message.event === event) handler({ event, id, payload: message.payload as T }); + if (message.event === event) + handler({ event, id, payload: message.payload as T }); }); } -export async function once(event: string, handler: EventCallback): Promise { +export async function once( + event: string, + handler: EventCallback, +): Promise { let unlisten = () => {}; unlisten = await listen(event, (payload) => { unlisten(); @@ -32,6 +39,10 @@ export async function emit(event: string, payload?: unknown): Promise { bridge().emit(event, toIpcValue(payload ?? null)); } -export async function emitTo(_target: string, event: string, payload?: unknown): Promise { +export async function emitTo( + _target: string, + event: string, + payload?: unknown, +): Promise { bridge().emit(event, toIpcValue(payload ?? null)); } diff --git a/apps/desktop/src/electron/fs.ts b/apps/desktop/src/electron/fs.ts index c8f843185d2..a13613a63fc 100644 --- a/apps/desktop/src/electron/fs.ts +++ b/apps/desktop/src/electron/fs.ts @@ -1,28 +1,101 @@ import { native } from "./bridge"; export enum BaseDirectory { - Audio = 1, Cache = 2, Config = 3, Data = 4, LocalData = 6, Desktop = 7, - Document = 8, Download = 9, Home = 11, Picture = 15, Public = 18, - Resource = 20, Temp = 21, Template = 22, Video = 24, AppConfig = 25, - AppData = 26, AppLocalData = 27, AppCache = 28, AppLog = 29, + Audio = 1, + Cache = 2, + Config = 3, + Data = 4, + LocalData = 6, + Desktop = 7, + Document = 8, + Download = 9, + Home = 11, + Picture = 15, + Public = 18, + Resource = 20, + Temp = 21, + Template = 22, + Video = 24, + AppConfig = 25, + AppData = 26, + AppLocalData = 27, + AppCache = 28, + AppLog = 29, +} +export interface FsOptions { + baseDir?: BaseDirectory; +} +export interface RemoveOptions extends FsOptions { + recursive?: boolean; +} +export interface MkdirOptions extends FsOptions { + recursive?: boolean; +} +export interface DirEntry { + name: string; + isDirectory: boolean; + isFile: boolean; + isSymlink: boolean; +} +export interface FileInfo { + size: number; + isFile: boolean; + isDirectory: boolean; + isSymlink: boolean; + mtime: Date | null; + atime: Date | null; + birthtime: Date | null; } -export interface FsOptions { baseDir?: BaseDirectory; } -export interface RemoveOptions extends FsOptions { recursive?: boolean; } -export interface MkdirOptions extends FsOptions { recursive?: boolean; } -export interface DirEntry { name: string; isDirectory: boolean; isFile: boolean; isSymlink: boolean; } -export interface FileInfo { size: number; isFile: boolean; isDirectory: boolean; isSymlink: boolean; mtime: Date | null; atime: Date | null; birthtime: Date | null; } -const options = (path: string, value?: FsOptions) => ({ path, baseDir: value?.baseDir }); -export const exists = (path: string, value?: FsOptions) => native("fs.exists", options(path, value)); -export const readFile = (path: string, value?: FsOptions) => native("fs.readFile", options(path, value)).then((bytes) => new Uint8Array(bytes)); -export const readTextFile = (path: string, value?: FsOptions) => native("fs.readTextFile", options(path, value)); -export const writeFile = (path: string, data: Uint8Array, value?: FsOptions) => native("fs.writeFile", { ...options(path, value), data: Array.from(data) }); -export const writeTextFile = (path: string, contents: string, value?: FsOptions) => native("fs.writeTextFile", { ...options(path, value), contents }); -export const remove = (path: string, value?: RemoveOptions) => native("fs.remove", { ...options(path, value), recursive: value?.recursive }); -export const mkdir = (path: string, value?: MkdirOptions) => native("fs.mkdir", { ...options(path, value), recursive: value?.recursive }); -export const readDir = (path: string, value?: FsOptions) => native("fs.readDir", options(path, value)); -export const stat = (path: string, value?: FsOptions) => native("fs.stat", options(path, value)); +const options = (path: string, value?: FsOptions) => ({ + path, + baseDir: value?.baseDir, +}); +export const exists = (path: string, value?: FsOptions) => + native("fs.exists", options(path, value)); +export const readFile = (path: string, value?: FsOptions) => + native("fs.readFile", options(path, value)).then( + (bytes) => new Uint8Array(bytes), + ); +export const readTextFile = (path: string, value?: FsOptions) => + native("fs.readTextFile", options(path, value)); +export const writeFile = (path: string, data: Uint8Array, value?: FsOptions) => + native("fs.writeFile", { + ...options(path, value), + data: Array.from(data), + }); +export const writeTextFile = ( + path: string, + contents: string, + value?: FsOptions, +) => native("fs.writeTextFile", { ...options(path, value), contents }); +export const remove = (path: string, value?: RemoveOptions) => + native("fs.remove", { + ...options(path, value), + recursive: value?.recursive, + }); +export const mkdir = (path: string, value?: MkdirOptions) => + native("fs.mkdir", { + ...options(path, value), + recursive: value?.recursive, + }); +export const readDir = (path: string, value?: FsOptions) => + native("fs.readDir", options(path, value)); +export const stat = (path: string, value?: FsOptions) => + native("fs.stat", options(path, value)); export const lstat = stat; -export const copyFile = (_source: string, _destination: string, _options?: FsOptions) => Promise.reject(new Error("copyFile is not implemented")); -export const rename = (_oldPath: string, _newPath: string, _options?: FsOptions) => Promise.reject(new Error("rename is not implemented")); -export const truncate = (_path: string, _length?: number, _options?: FsOptions) => Promise.reject(new Error("truncate is not implemented")); +export const copyFile = ( + _source: string, + _destination: string, + _options?: FsOptions, +) => Promise.reject(new Error("copyFile is not implemented")); +export const rename = ( + _oldPath: string, + _newPath: string, + _options?: FsOptions, +) => Promise.reject(new Error("rename is not implemented")); +export const truncate = ( + _path: string, + _length?: number, + _options?: FsOptions, +) => Promise.reject(new Error("truncate is not implemented")); diff --git a/apps/desktop/src/electron/menu.ts b/apps/desktop/src/electron/menu.ts index 29ee5109268..96bf5a51125 100644 --- a/apps/desktop/src/electron/menu.ts +++ b/apps/desktop/src/electron/menu.ts @@ -1,6 +1,6 @@ import { native } from "./bridge"; -import { listen } from "./event"; import type { LogicalPosition, PhysicalPosition } from "./dpi"; +import { listen } from "./event"; let nextMenuId = 1; @@ -13,8 +13,13 @@ export interface MenuItemOptions { items?: MenuEntry[]; } -export interface CheckMenuItemOptions extends MenuItemOptions { checked?: boolean; } -export interface PredefinedMenuItemOptions { item: string; text?: string; } +export interface CheckMenuItemOptions extends MenuItemOptions { + checked?: boolean; +} +export interface PredefinedMenuItemOptions { + item: string; + text?: string; +} export class MenuItem { id: string; @@ -29,9 +34,15 @@ export class MenuItem { Object.assign(this, options); } - static async new(options: MenuItemOptions) { return new MenuItem(options); } - async setText(text: string) { this.text = text; } - async setEnabled(enabled: boolean) { this.enabled = enabled; } + static async new(options: MenuItemOptions) { + return new MenuItem(options); + } + async setText(text: string) { + this.text = text; + } + async setEnabled(enabled: boolean) { + this.enabled = enabled; + } } export class CheckMenuItem extends MenuItem { @@ -40,9 +51,15 @@ export class CheckMenuItem extends MenuItem { super(options); this.checked = options.checked ?? false; } - static async new(options: CheckMenuItemOptions) { return new CheckMenuItem(options); } - async setChecked(checked: boolean) { this.checked = checked; } - async isChecked() { return this.checked; } + static async new(options: CheckMenuItemOptions) { + return new CheckMenuItem(options); + } + async setChecked(checked: boolean) { + this.checked = checked; + } + async isChecked() { + return this.checked; + } } export class PredefinedMenuItem extends MenuItem { @@ -51,38 +68,70 @@ export class PredefinedMenuItem extends MenuItem { super({ text: options.text }); this.item = options.item; } - static async new(options: PredefinedMenuItemOptions) { return new PredefinedMenuItem(options); } + static async new(options: PredefinedMenuItemOptions) { + return new PredefinedMenuItem(options); + } } export class Submenu extends MenuItem { - static async new(options: MenuItemOptions) { return new Submenu(options); } + static async new(options: MenuItemOptions) { + return new Submenu(options); + } } -export type MenuEntry = MenuItem | CheckMenuItem | PredefinedMenuItem | Submenu | MenuItemOptions; +export type MenuEntry = + | MenuItem + | CheckMenuItem + | PredefinedMenuItem + | Submenu + | MenuItemOptions; export class Menu { constructor(public items: MenuEntry[]) {} - static async new(options: { id?: string; items?: MenuEntry[] } = {}) { return new Menu(options.items ?? []); } + static async new(options: { id?: string; items?: MenuEntry[] } = {}) { + return new Menu(options.items ?? []); + } async popup(position?: LogicalPosition | PhysicalPosition) { - const unlisteners = await Promise.all(this.items.flatMap(flattenItems).filter((item) => item.action).map(async (item) => { - const id = item.id ?? `cap-menu-${nextMenuId++}`; - item.id = id; - return listen(`menu:${id}`, () => item.action?.(id)); - })); + const unlisteners = await Promise.all( + this.items + .flatMap(flattenItems) + .filter((item) => item.action) + .map(async (item) => { + const id = item.id ?? `cap-menu-${nextMenuId++}`; + item.id = id; + return listen(`menu:${id}`, () => item.action?.(id)); + }), + ); await native("menu.popup", { items: this.items.map(serializeItem), position: position ? { x: position.x, y: position.y } : undefined, }); - setTimeout(() => unlisteners.forEach((unlisten) => unlisten()), 30000); + setTimeout(() => { + unlisteners.forEach((unlisten) => { + unlisten(); + }); + }, 30000); } - async append(item: MenuEntry) { this.items.push(item); } - async prepend(item: MenuEntry) { this.items.unshift(item); } - async insert(item: MenuEntry, position: number) { this.items.splice(position, 0, item); } - async remove(item: MenuEntry) { this.items = this.items.filter((entry) => entry !== item); } - async removeAt(position: number) { return this.items.splice(position, 1)[0] ?? null; } - async itemsList() { return this.items; } + async append(item: MenuEntry) { + this.items.push(item); + } + async prepend(item: MenuEntry) { + this.items.unshift(item); + } + async insert(item: MenuEntry, position: number) { + this.items.splice(position, 0, item); + } + async remove(item: MenuEntry) { + this.items = this.items.filter((entry) => entry !== item); + } + async removeAt(position: number) { + return this.items.splice(position, 1)[0] ?? null; + } + async itemsList() { + return this.items; + } async close() {} } @@ -91,7 +140,11 @@ function flattenItems(item: MenuEntry): MenuEntry[] { } function serializeItem(item: MenuEntry): Record { - if (item instanceof PredefinedMenuItem && item.item.toLowerCase() === "separator") return { type: "separator" }; + if ( + item instanceof PredefinedMenuItem && + item.item.toLowerCase() === "separator" + ) + return { type: "separator" }; return { id: item.id, text: item.text, diff --git a/apps/desktop/src/electron/notification.ts b/apps/desktop/src/electron/notification.ts index 1f00db9ee37..a9a70f19081 100644 --- a/apps/desktop/src/electron/notification.ts +++ b/apps/desktop/src/electron/notification.ts @@ -1,9 +1,18 @@ import { native } from "./bridge"; export type Permission = "granted" | "denied" | "default"; -export const isPermissionGranted = () => native("notification.permission").then((value) => value === "granted"); -export const requestPermission = () => native("notification.requestPermission"); -export const sendNotification = (options: string | { title: string; body?: string; icon?: string }) => - native("notification.show", typeof options === "string" ? { title: options } : options); +export const isPermissionGranted = () => + native("notification.permission").then( + (value) => value === "granted", + ); +export const requestPermission = () => + native("notification.requestPermission"); +export const sendNotification = ( + options: string | { title: string; body?: string; icon?: string }, +) => + native( + "notification.show", + typeof options === "string" ? { title: options } : options, + ); export const registerActionTypes = () => Promise.resolve(); export const pending = () => Promise.resolve([]); export const cancel = () => Promise.resolve(); diff --git a/apps/desktop/src/electron/opener.ts b/apps/desktop/src/electron/opener.ts index 70383091215..52b2096fc7f 100644 --- a/apps/desktop/src/electron/opener.ts +++ b/apps/desktop/src/electron/opener.ts @@ -1,4 +1,7 @@ import { native } from "./bridge"; -export const openPath = (path: string) => native("opener.openPath", { path }); -export const openUrl = (url: string) => native("shell.open", { path: url }); -export const revealItemInDir = (path: string) => native("opener.revealItemInDir", { path }); +export const openPath = (path: string) => + native("opener.openPath", { path }); +export const openUrl = (url: string) => + native("shell.open", { path: url }); +export const revealItemInDir = (path: string) => + native("opener.revealItemInDir", { path }); diff --git a/apps/desktop/src/electron/os.ts b/apps/desktop/src/electron/os.ts index af3a64ee499..37113aa56ac 100644 --- a/apps/desktop/src/electron/os.ts +++ b/apps/desktop/src/electron/os.ts @@ -1,11 +1,23 @@ import { native } from "./bridge"; export type OsType = "linux" | "macos" | "ios" | "android" | "windows"; -export type Arch = "x86" | "x86_64" | "arm" | "aarch64" | "mips" | "mips64" | "powerpc" | "powerpc64" | "riscv64" | "s390x" | "sparc64"; +export type Arch = + | "x86" + | "x86_64" + | "arm" + | "aarch64" + | "mips" + | "mips64" + | "powerpc" + | "powerpc64" + | "riscv64" + | "s390x" + | "sparc64"; + import { bridge } from "./bridge"; export const arch = () => bridge().os.arch as Arch; -export const exeExtension = () => type() === "windows" ? ".exe" : ""; -export const family = () => type() === "windows" ? "windows" : "unix"; +export const exeExtension = () => (type() === "windows" ? ".exe" : ""); +export const family = () => (type() === "windows" ? "windows" : "unix"); export const hostname = () => native("os.hostname"); export const locale = () => native("os.locale"); export const platform = () => bridge().os.platform; diff --git a/apps/desktop/src/electron/path.ts b/apps/desktop/src/electron/path.ts index 6802967372d..bed56c0bdc4 100644 --- a/apps/desktop/src/electron/path.ts +++ b/apps/desktop/src/electron/path.ts @@ -4,7 +4,8 @@ export const appCacheDir = () => native("path.cacheDir"); export const appConfigDir = () => native("path.configDir"); export const appDataDir = () => native("path.appDataDir"); export const appLocalDataDir = () => native("path.appLocalDataDir"); -export const appLogDir = () => native("path.appDataDir").then((value) => join(value, "logs")); +export const appLogDir = () => + native("path.appDataDir").then((value) => join(value, "logs")); export const audioDir = () => native("path.audioDir"); export const cacheDir = () => native("path.cacheDir"); export const configDir = () => native("path.configDir"); @@ -21,17 +22,32 @@ export const resourceDir = () => native("path.resourceDir"); export const runtimeDir = () => native("path.tempDir"); export const templateDir = () => native("path.homeDir"); export const videoDir = () => native("path.videoDir"); -export const resolveResource = (resourcePath: string) => native("path.resolve", { paths: [resourcePath] }).then(async () => { - const root = await resourceDir(); - return join(root, resourcePath); -}); +export const resolveResource = (resourcePath: string) => + native("path.resolve", { paths: [resourcePath] }).then(async () => { + const root = await resourceDir(); + return join(root, resourcePath); + }); export const tempDir = () => native("path.tempDir"); -export const join = (...paths: string[]) => native("path.join", { paths }); -export const resolve = (...paths: string[]) => native("path.resolve", { paths }); -export const normalize = (path: string) => native("path.resolve", { paths: [path] }); -export const dirname = (path: string) => native("path.resolve", { paths: [path, ".."] }); -export const extname = (path: string) => Promise.resolve(path.match(/\.[^./\\]+$/)?.[0] ?? ""); -export const basename = (path: string, ext?: string) => Promise.resolve(path.split(/[\\/]/).pop()?.replace(ext ?? /$^/, "") ?? ""); -export const isAbsolute = (path: string) => Promise.resolve(/^(?:[a-zA-Z]:[\\/]|\/)/.test(path)); -export const sep = () => native("os.type").then((value) => value === "windows" ? "\\" : "/"); -export const delimiter = () => native("os.type").then((value) => value === "windows" ? ";" : ":"); +export const join = (...paths: string[]) => + native("path.join", { paths }); +export const resolve = (...paths: string[]) => + native("path.resolve", { paths }); +export const normalize = (path: string) => + native("path.resolve", { paths: [path] }); +export const dirname = (path: string) => + native("path.resolve", { paths: [path, ".."] }); +export const extname = (path: string) => + Promise.resolve(path.match(/\.[^./\\]+$/)?.[0] ?? ""); +export const basename = (path: string, ext?: string) => + Promise.resolve( + path + .split(/[\\/]/) + .pop() + ?.replace(ext ?? /$^/, "") ?? "", + ); +export const isAbsolute = (path: string) => + Promise.resolve(/^(?:[a-zA-Z]:[\\/]|\/)/.test(path)); +export const sep = () => + native("os.type").then((value) => (value === "windows" ? "\\" : "/")); +export const delimiter = () => + native("os.type").then((value) => (value === "windows" ? ";" : ":")); diff --git a/apps/desktop/src/electron/shell.ts b/apps/desktop/src/electron/shell.ts index 2e84d6245a8..368864afad4 100644 --- a/apps/desktop/src/electron/shell.ts +++ b/apps/desktop/src/electron/shell.ts @@ -1,14 +1,52 @@ import { native } from "./bridge"; -export const open = (path: string, _openWith?: string) => native("shell.open", { path }); +export const open = (path: string, _openWith?: string) => + native("shell.open", { path }); export class Command { - constructor(public program: string, public args: string[] = [], public options: Record = {}) {} - static create(program: string, args: string[] = [], options: Record = {}) { return new Command(program, args, options); } - static sidecar(program: string, args: string[] = [], options: Record = {}) { return new Command(program, args, options); } - execute() { return native("shell.execute", { program: this.program, args: this.args, options: this.options }); } - spawn() { return native("shell.spawn", { program: this.program, args: this.args, options: this.options }); } + constructor( + public program: string, + public args: string[] = [], + public options: Record = {}, + ) {} + static create( + program: string, + args: string[] = [], + options: Record = {}, + ) { + return new Command(program, args, options); + } + static sidecar( + program: string, + args: string[] = [], + options: Record = {}, + ) { + return new Command(program, args, options); + } + execute() { + return native("shell.execute", { + program: this.program, + args: this.args, + options: this.options, + }); + } + spawn() { + return native("shell.spawn", { + program: this.program, + args: this.args, + options: this.options, + }); + } } -export interface CommandOutput { code: number; signal: number | null; stdout: string; stderr: string; } -export interface Child { pid: number; kill(): Promise; write(data: string | Uint8Array): Promise; } +export interface CommandOutput { + code: number; + signal: number | null; + stdout: string; + stderr: string; +} +export interface Child { + pid: number; + kill(): Promise; + write(data: string | Uint8Array): Promise; +} diff --git a/apps/desktop/src/electron/store.ts b/apps/desktop/src/electron/store.ts index f55f6b97a6f..15ee41cbacb 100644 --- a/apps/desktop/src/electron/store.ts +++ b/apps/desktop/src/electron/store.ts @@ -4,10 +4,15 @@ export class Store { private data = new Map(); private loaded: Promise; - constructor(public path: string, _options?: { autoSave?: boolean }) { - this.loaded = native>("store.load", { path }).then((values) => { - this.data = new Map(Object.entries(values)); - }); + constructor( + public path: string, + _options?: { autoSave?: boolean }, + ) { + this.loaded = native>("store.load", { path }).then( + (values) => { + this.data = new Map(Object.entries(values)); + }, + ); } static async load(path: string, options?: { autoSave?: boolean }) { @@ -16,23 +21,67 @@ export class Store { return store; } - async set(key: string, value: unknown) { await this.loaded; this.data.set(key, value); } - async get(key: string): Promise { await this.loaded; return this.data.get(key) as T | undefined; } - async has(key: string) { await this.loaded; return this.data.has(key); } - async delete(key: string) { await this.loaded; return this.data.delete(key); } - async clear() { await this.loaded; this.data.clear(); } - async reset() { return this.clear(); } - async keys() { await this.loaded; return [...this.data.keys()]; } - async values() { await this.loaded; return [...this.data.values()]; } - async entries() { await this.loaded; return [...this.data.entries()]; } - async length() { await this.loaded; return this.data.size; } - async save() { await this.loaded; return native("store.save", { path: this.path, value: Object.fromEntries(this.data) }); } + async set(key: string, value: unknown) { + await this.loaded; + this.data.set(key, value); + } + async get(key: string): Promise { + await this.loaded; + return this.data.get(key) as T | undefined; + } + async has(key: string) { + await this.loaded; + return this.data.has(key); + } + async delete(key: string) { + await this.loaded; + return this.data.delete(key); + } + async clear() { + await this.loaded; + this.data.clear(); + } + async reset() { + return this.clear(); + } + async keys() { + await this.loaded; + return [...this.data.keys()]; + } + async values() { + await this.loaded; + return [...this.data.values()]; + } + async entries() { + await this.loaded; + return [...this.data.entries()]; + } + async length() { + await this.loaded; + return this.data.size; + } + async save() { + await this.loaded; + return native("store.save", { + path: this.path, + value: Object.fromEntries(this.data), + }); + } async reload() { - const values = await native>("store.load", { path: this.path }); + const values = await native>("store.load", { + path: this.path, + }); this.data = new Map(Object.entries(values)); } - async onKeyChange(_key: string, _callback: (value: T | undefined) => void) { return () => {}; } - async onChange(_callback: (key: string, value: T | undefined) => void) { return () => {}; } + async onKeyChange( + _key: string, + _callback: (value: T | undefined) => void, + ) { + return () => {}; + } + async onChange(_callback: (key: string, value: T | undefined) => void) { + return () => {}; + } } export const load = Store.load; diff --git a/apps/desktop/src/electron/updater.ts b/apps/desktop/src/electron/updater.ts index 192303cb9e3..ddfd462b46e 100644 --- a/apps/desktop/src/electron/updater.ts +++ b/apps/desktop/src/electron/updater.ts @@ -1,5 +1,12 @@ -export interface CheckOptions { headers?: Record; timeout?: number; target?: string; } -export interface DownloadEvent { event: "Started" | "Progress" | "Finished"; data?: { contentLength?: number; chunkLength?: number }; } +export interface CheckOptions { + headers?: Record; + timeout?: number; + target?: string; +} +export interface DownloadEvent { + event: "Started" | "Progress" | "Finished"; + data?: { contentLength?: number; chunkLength?: number }; +} export interface Update { version: string; currentVersion: string; @@ -11,4 +18,6 @@ export interface Update { downloadAndInstall(listener?: (event: DownloadEvent) => void): Promise; close(): Promise; } -export async function check(_options?: CheckOptions): Promise { return null; } +export async function check(_options?: CheckOptions): Promise { + return null; +} diff --git a/apps/desktop/src/routes/(window-chrome).tsx b/apps/desktop/src/routes/(window-chrome).tsx index b4f581b5976..526f2f3c3fe 100644 --- a/apps/desktop/src/routes/(window-chrome).tsx +++ b/apps/desktop/src/routes/(window-chrome).tsx @@ -1,7 +1,4 @@ import { type RouteSectionProps, useLocation } from "@solidjs/router"; -import type { UnlistenFn } from "~/electron/event"; -import { getCurrentWindow } from "~/electron/window"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, @@ -10,10 +7,12 @@ import { type ParentProps, Suspense, } from "solid-js"; - import { AbsoluteInsetLoader } from "~/components/Loader"; import CaptionControlsMacOS from "~/components/titlebar/controls/CaptionControlsMacOS"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import type { UnlistenFn } from "~/electron/event"; +import { type as ostype } from "~/electron/os"; +import { getCurrentWindow } from "~/electron/window"; import { applyMacOSWindowMaterial } from "~/utils/macos-window-material"; import { initializeTitlebar } from "~/utils/titlebar-state"; import { diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx index 5a5f9ecb93d..b2322282cad 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx @@ -1,5 +1,4 @@ import { createTimer } from "@solid-primitives/timer"; -import { CheckMenuItem, Menu, PredefinedMenuItem } from "~/electron/menu"; import { cx } from "cva"; import { type Component, @@ -8,6 +7,7 @@ import { createSignal, Show, } from "solid-js"; +import { CheckMenuItem, Menu, PredefinedMenuItem } from "~/electron/menu"; import { trackEvent } from "~/utils/analytics"; import { createCurrentRecordingQuery } from "~/utils/queries"; import { diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx index 2bd8dbbc450..26939ef2b95 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx @@ -1,9 +1,9 @@ import { makePersisted } from "@solid-primitives/storage"; -import { getVersion } from "~/electron/app"; -import { getCurrentWindow } from "~/electron/window"; import { createEffect, createResource } from "solid-js"; import { createStore } from "solid-js/store"; import Tooltip from "~/components/Tooltip"; +import { getVersion } from "~/electron/app"; +import { getCurrentWindow } from "~/electron/window"; import { commands } from "~/utils/tauri"; import { apiClient } from "~/utils/web-api"; import IconLucideBell from "~icons/lucide/bell"; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx index fe7906de0cc..d866c473656 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx @@ -1,4 +1,3 @@ -import { CheckMenuItem, Menu, PredefinedMenuItem } from "~/electron/menu"; import { cx } from "cva"; import { type Component, @@ -6,6 +5,7 @@ import { createSignal, Show, } from "solid-js"; +import { CheckMenuItem, Menu, PredefinedMenuItem } from "~/electron/menu"; import { trackEvent } from "~/utils/analytics"; import { createTauriEventListener } from "~/utils/createEventListener"; import { createCurrentRecordingQuery } from "~/utils/queries"; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx index a1375d73431..268456c6ec5 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/Recents.tsx @@ -1,4 +1,3 @@ -import { convertFileSrc } from "~/electron/core"; import { cx } from "cva"; import { createEffect, @@ -10,6 +9,7 @@ import { onMount, Show, } from "solid-js"; +import { convertFileSrc } from "~/electron/core"; import IconLucideClapperboard from "~icons/lucide/clapperboard"; import IconLucideHistory from "~icons/lucide/history"; import IconLucideImage from "~icons/lucide/image"; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx index 0f2cf2bd102..8290b2a1eb5 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx @@ -1,13 +1,13 @@ import { ProgressCircle } from "@cap/ui-solid"; -import { convertFileSrc } from "~/electron/core"; -import { ask, save } from "~/electron/dialog"; -import { remove } from "~/electron/fs"; -import * as shell from "~/electron/shell"; import { cx } from "cva"; import type { ComponentProps } from "solid-js"; import { createMemo, createSignal, Show, splitProps } from "solid-js"; import toast from "solid-toast"; import Tooltip from "~/components/Tooltip"; +import { convertFileSrc } from "~/electron/core"; +import { ask, save } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import * as shell from "~/electron/shell"; import { createScreenshotShareLinkFromProjectPath, type ScreenshotExportStatus, diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index 2dbe56eb8e3..7b4f14431f8 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -6,21 +6,6 @@ import { useQuery, useQueryClient, } from "@tanstack/solid-query"; -import { Channel } from "~/electron/core"; -import { emit, listen } from "~/electron/event"; -import { - getAllWebviewWindows, - WebviewWindow, -} from "~/electron/webviewWindow"; -import { - currentMonitor, - getCurrentWindow, - LogicalSize, - PhysicalPosition, -} from "~/electron/window"; -import * as dialog from "~/electron/dialog"; -import { relaunch } from "~/electron/process"; -import * as shell from "~/electron/shell"; import { cx } from "cva"; import { createEffect, @@ -40,6 +25,18 @@ import { Transition } from "solid-transition-group"; import Mode from "~/components/Mode"; import { RecoveryToast } from "~/components/RecoveryToast"; import Tooltip from "~/components/Tooltip"; +import { Channel } from "~/electron/core"; +import * as dialog from "~/electron/dialog"; +import { emit, listen } from "~/electron/event"; +import { relaunch } from "~/electron/process"; +import * as shell from "~/electron/shell"; +import { getAllWebviewWindows, WebviewWindow } from "~/electron/webviewWindow"; +import { + currentMonitor, + getCurrentWindow, + LogicalSize, + PhysicalPosition, +} from "~/electron/window"; import { Input } from "~/routes/editor/ui"; import { authStore, diff --git a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx index fd195b5b5e9..7d9c7eef5bb 100644 --- a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx +++ b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx @@ -1,10 +1,5 @@ import { Button } from "@cap/ui-solid"; import { makePersisted } from "@solid-primitives/storage"; -import { getCurrentWindow } from "~/electron/window"; -import { ask } from "~/electron/dialog"; -import { type as ostype } from "~/electron/os"; -import { relaunch } from "~/electron/process"; -import * as shell from "~/electron/shell"; import { cx } from "cva"; import { createEffect, @@ -17,6 +12,11 @@ import { Show, } from "solid-js"; import { createStore } from "solid-js/store"; +import { ask } from "~/electron/dialog"; +import { type as ostype } from "~/electron/os"; +import { relaunch } from "~/electron/process"; +import * as shell from "~/electron/shell"; +import { getCurrentWindow } from "~/electron/window"; import { generalSettingsStore } from "~/store"; import { isPermissionGranted as isPermitted, diff --git a/apps/desktop/src/routes/(window-chrome)/settings.tsx b/apps/desktop/src/routes/(window-chrome)/settings.tsx index 8cb3d0bcb95..d0f4bd02023 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings.tsx @@ -1,11 +1,6 @@ import { Button } from "@cap/ui-solid"; import { A, type RouteSectionProps, useNavigate } from "@solidjs/router"; import { createQuery, useQueryClient } from "@tanstack/solid-query"; -import { getVersion } from "~/electron/app"; -import { writeText } from "~/electron/clipboard"; -import * as dialog from "~/electron/dialog"; -import { fetch as tauriFetch } from "~/electron/http"; -import * as shell from "~/electron/shell"; import { createEffect, createMemo, @@ -20,6 +15,11 @@ import { import toast from "solid-toast"; import { CapErrorBoundary } from "~/components/CapErrorBoundary"; import { SignInButton } from "~/components/SignInButton"; +import { getVersion } from "~/electron/app"; +import { writeText } from "~/electron/clipboard"; +import * as dialog from "~/electron/dialog"; +import { fetch as tauriFetch } from "~/electron/http"; +import * as shell from "~/electron/shell"; import { authStore, userProfileStore } from "~/store"; import { resetUser, trackEvent } from "~/utils/analytics"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx b/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx index 8300b1689cc..ea220e269ed 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/automations.tsx @@ -1,6 +1,4 @@ import { Button } from "@cap/ui-solid"; -import { CheckMenuItem, Menu } from "~/electron/menu"; -import { open } from "~/electron/dialog"; import { cx } from "cva"; import { type Component, @@ -15,6 +13,8 @@ import { createStore, produce } from "solid-js/store"; import { Dynamic } from "solid-js/web"; import toast from "solid-toast"; import { Toggle } from "~/components/Toggle"; +import { open } from "~/electron/dialog"; +import { CheckMenuItem, Menu } from "~/electron/menu"; import { presetsStore } from "~/store"; import { ACTION_LABELS, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx b/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx index 5e353bf7867..7f88369ec82 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/cli.tsx @@ -1,8 +1,8 @@ import { Button } from "@cap/ui-solid"; -import { invoke } from "~/electron/core"; -import { writeText } from "~/electron/clipboard"; import { createResource, createSignal, Show } from "solid-js"; import toast from "solid-toast"; +import { writeText } from "~/electron/clipboard"; +import { invoke } from "~/electron/core"; import { Section, SectionCard, SettingsPageContent } from "./Setting"; type CliInstallStatus = { diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index ce46f075e98..f561aaf9d0f 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -1,7 +1,7 @@ -import { invoke } from "~/electron/core"; -import { type } from "~/electron/os"; import { createResource, Show } from "solid-js"; import { createStore } from "solid-js/store"; +import { invoke } from "~/electron/core"; +import { type } from "~/electron/os"; import { generalSettingsStore } from "~/store"; import { diff --git a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx index 991bb11ce2c..239990c17d6 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx @@ -1,10 +1,10 @@ import { Button } from "@cap/ui-solid"; import { action, useAction, useSubmission } from "@solidjs/router"; +import { createResource, createSignal, For, Show } from "solid-js"; +import toast from "solid-toast"; import { getVersion } from "~/electron/app"; import { type OsType, type as ostype } from "~/electron/os"; import * as shell from "~/electron/shell"; -import { createResource, createSignal, For, Show } from "solid-js"; -import toast from "solid-toast"; import { commands, type SystemDiagnostics } from "~/utils/tauri"; import { apiClient, protectedHeaders } from "~/utils/web-api"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx index 6359dddb82a..36f14cf2755 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx @@ -7,8 +7,6 @@ import { import { type OsType, type } from "~/electron/os"; import "@total-typescript/ts-reset/filter-boolean"; import { Collapsible } from "@kobalte/core/collapsible"; -import { CheckMenuItem, Menu, MenuItem } from "~/electron/menu"; -import { confirm } from "~/electron/dialog"; import { cx } from "cva"; import { createEffect, @@ -25,6 +23,8 @@ import toast from "solid-toast"; import themePreviewAuto from "~/assets/theme-previews/auto.jpg"; import themePreviewDark from "~/assets/theme-previews/dark.jpg"; import themePreviewLight from "~/assets/theme-previews/light.jpg"; +import { confirm } from "~/electron/dialog"; +import { CheckMenuItem, Menu, MenuItem } from "~/electron/menu"; import { Input } from "~/routes/editor/ui"; import { authStore, diff --git a/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx b/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx index a6069288173..44c2b0652d1 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx @@ -1,5 +1,4 @@ import { createEventListener } from "@solid-primitives/event-listener"; -import { type as ostype } from "~/electron/os"; import { batch, createEffect, @@ -12,6 +11,7 @@ import { Switch, } from "solid-js"; import { createStore } from "solid-js/store"; +import { type as ostype } from "~/electron/os"; import { hotkeysStore } from "~/store"; import { diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index c83797e0eb9..be8ee769088 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -6,10 +6,6 @@ import { queryOptions, useQueryClient, } from "@tanstack/solid-query"; -import { Channel, convertFileSrc } from "~/electron/core"; -import { ask, confirm } from "~/electron/dialog"; -import { remove } from "~/electron/fs"; -import * as shell from "~/electron/shell"; import { cx } from "cva"; import { createEffect, @@ -22,6 +18,10 @@ import { } from "solid-js"; import { createStore, produce } from "solid-js/store"; import CapTooltip from "~/components/Tooltip"; +import { Channel, convertFileSrc } from "~/electron/core"; +import { ask, confirm } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import * as shell from "~/electron/shell"; import { Input } from "~/routes/editor/ui"; import { trackEvent } from "~/utils/analytics"; import { createTauriEventListener } from "~/utils/createEventListener"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx b/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx index 7e3e29c64a5..ddc16a8bd20 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx @@ -5,9 +5,6 @@ import { queryOptions, useQueryClient, } from "@tanstack/solid-query"; -import { convertFileSrc } from "~/electron/core"; -import { ask } from "~/electron/dialog"; -import { remove } from "~/electron/fs"; import { createEffect, createMemo, @@ -17,6 +14,9 @@ import { Show, } from "solid-js"; import toast from "solid-toast"; +import { convertFileSrc } from "~/electron/core"; +import { ask } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; import { Input } from "~/routes/editor/ui"; import { createScreenshotShareLinkFromProjectPath, diff --git a/apps/desktop/src/routes/(window-chrome)/update.tsx b/apps/desktop/src/routes/(window-chrome)/update.tsx index de61927714f..fb64c22a09f 100644 --- a/apps/desktop/src/routes/(window-chrome)/update.tsx +++ b/apps/desktop/src/routes/(window-chrome)/update.tsx @@ -1,7 +1,5 @@ import { Button } from "@cap/ui-solid"; import { useNavigate } from "@solidjs/router"; -import { getCurrentWindow, UserAttentionType } from "~/electron/window"; -import { relaunch } from "~/electron/process"; import { createResource, createSignal, @@ -10,6 +8,8 @@ import { Show, Switch, } from "solid-js"; +import { relaunch } from "~/electron/process"; +import { getCurrentWindow, UserAttentionType } from "~/electron/window"; import { commands, events } from "~/utils/tauri"; export default function () { diff --git a/apps/desktop/src/routes/(window-chrome)/upgrade.tsx b/apps/desktop/src/routes/(window-chrome)/upgrade.tsx index f65364d1eea..3fff8018a39 100644 --- a/apps/desktop/src/routes/(window-chrome)/upgrade.tsx +++ b/apps/desktop/src/routes/(window-chrome)/upgrade.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { createMutation, useQueryClient } from "@tanstack/solid-query"; -import { getCurrentWindow, Window } from "~/electron/window"; import { type Accessor, createSignal, Show } from "solid-js"; +import { getCurrentWindow, Window } from "~/electron/window"; import { generalSettingsStore } from "~/store"; import { getProPlanId } from "~/utils/plans"; import { createLicenseQuery } from "~/utils/queries"; diff --git a/apps/desktop/src/routes/camera.tsx b/apps/desktop/src/routes/camera.tsx index 58f8c630791..fd0082073a9 100644 --- a/apps/desktop/src/routes/camera.tsx +++ b/apps/desktop/src/routes/camera.tsx @@ -1,13 +1,4 @@ import { makePersisted } from "@solid-primitives/storage"; -import { listen } from "~/electron/event"; -import { - availableMonitors, - currentMonitor, - getCurrentWindow, - LogicalPosition, - LogicalSize, -} from "~/electron/window"; -import { type } from "~/electron/os"; import { cx } from "cva"; import { type Accessor, @@ -37,6 +28,15 @@ import { getDefaultCameraWindowState, normalizeBackgroundBlurMode, } from "~/components/CameraPreviewChrome"; +import { listen } from "~/electron/event"; +import { type } from "~/electron/os"; +import { + availableMonitors, + currentMonitor, + getCurrentWindow, + LogicalPosition, + LogicalSize, +} from "~/electron/window"; import { generalSettingsStore } from "~/store"; import { createTauriEventListener } from "~/utils/createEventListener"; import { diff --git a/apps/desktop/src/routes/capture-area.tsx b/apps/desktop/src/routes/capture-area.tsx index 014342121c7..ccf68228457 100644 --- a/apps/desktop/src/routes/capture-area.tsx +++ b/apps/desktop/src/routes/capture-area.tsx @@ -1,13 +1,6 @@ import { createEventListener } from "@solid-primitives/event-listener"; import { createScheduled, debounce } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; -import { LogicalPosition } from "~/electron/dpi"; -import { Menu } from "~/electron/menu"; -import { - getCurrentWebviewWindow, - WebviewWindow, -} from "~/electron/webviewWindow"; -import { type as ostype } from "~/electron/os"; import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { Transition } from "solid-transition-group"; @@ -20,6 +13,13 @@ import { type Ratio, } from "~/components/Cropper"; import SelectionHint from "~/components/selection-hint"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu } from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; +import { + getCurrentWebviewWindow, + WebviewWindow, +} from "~/electron/webviewWindow"; import { createOptionsQuery } from "~/utils/queries"; import type { DisplayId } from "~/utils/tauri"; import { emitTo } from "~/utils/tauriSpectaHack"; diff --git a/apps/desktop/src/routes/debug.tsx b/apps/desktop/src/routes/debug.tsx index e0cc57c36cd..539445889cd 100644 --- a/apps/desktop/src/routes/debug.tsx +++ b/apps/desktop/src/routes/debug.tsx @@ -1,9 +1,9 @@ import { useNavigate } from "@solidjs/router"; import { createQuery } from "@tanstack/solid-query"; +import { createSignal, createUniqueId, For, onMount } from "solid-js"; import { getVersion } from "~/electron/app"; import * as dialog from "~/electron/dialog"; import { check } from "~/electron/updater"; -import { createSignal, createUniqueId, For, onMount } from "solid-js"; import { commands } from "~/utils/tauri"; export default function Debug() { diff --git a/apps/desktop/src/routes/editor/AudioLibrary.tsx b/apps/desktop/src/routes/editor/AudioLibrary.tsx index 269409f2425..7b1d0b30445 100644 --- a/apps/desktop/src/routes/editor/AudioLibrary.tsx +++ b/apps/desktop/src/routes/editor/AudioLibrary.tsx @@ -1,4 +1,3 @@ -import { open } from "~/electron/dialog"; import { cx } from "cva"; import { createEffect, @@ -11,6 +10,7 @@ import { Show, } from "solid-js"; import toast from "solid-toast"; +import { open } from "~/electron/dialog"; import { commands, type ImportedAudioTrack } from "~/utils/tauri"; import { AUDIO_IMPORT_EXTENSIONS } from "./audio"; diff --git a/apps/desktop/src/routes/editor/CaptionsTab.tsx b/apps/desktop/src/routes/editor/CaptionsTab.tsx index 3e767e35b4d..a107c8425a0 100644 --- a/apps/desktop/src/routes/editor/CaptionsTab.tsx +++ b/apps/desktop/src/routes/editor/CaptionsTab.tsx @@ -1,7 +1,5 @@ import { Button } from "@cap/ui-solid"; import { Select as KSelect } from "@kobalte/core/select"; -import { appLocalDataDir, join } from "~/electron/path"; -import { exists } from "~/electron/fs"; import { cx } from "cva"; import { createEffect, @@ -17,6 +15,8 @@ import { produce } from "solid-js/store"; import toast from "solid-toast"; import { Toggle } from "~/components/Toggle"; import Tooltip from "~/components/Tooltip"; +import { exists } from "~/electron/fs"; +import { appLocalDataDir, join } from "~/electron/path"; import { CAPTION_STYLE_PRESETS, type CaptionAnimation, diff --git a/apps/desktop/src/routes/editor/ClipsSidebar.tsx b/apps/desktop/src/routes/editor/ClipsSidebar.tsx index 7abeac2121a..7d73e5176b1 100644 --- a/apps/desktop/src/routes/editor/ClipsSidebar.tsx +++ b/apps/desktop/src/routes/editor/ClipsSidebar.tsx @@ -1,13 +1,6 @@ import { Button } from "@cap/ui-solid"; import { createEventListener } from "@solid-primitives/event-listener"; import { useQuery } from "@tanstack/solid-query"; -import { convertFileSrc } from "~/electron/core"; -import { LogicalPosition } from "~/electron/dpi"; -import { Menu, MenuItem } from "~/electron/menu"; -import { appDataDir, join } from "~/electron/path"; -import { getCurrentWindow } from "~/electron/window"; -import { open } from "~/electron/dialog"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { type Component, @@ -26,6 +19,13 @@ import { import { produce, reconcile } from "solid-js/store"; import { Portal } from "solid-js/web"; import toast from "solid-toast"; +import { convertFileSrc } from "~/electron/core"; +import { open } from "~/electron/dialog"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu, MenuItem } from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; +import { appDataDir, join } from "~/electron/path"; +import { getCurrentWindow } from "~/electron/window"; import { createDevicesQuery } from "~/utils/devices"; import { createCameraMutation, diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index b4618bab648..c5766c1a45a 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -13,15 +13,6 @@ import { Tabs as KTabs } from "@kobalte/core/tabs"; import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListenerMap } from "@solid-primitives/event-listener"; import { createWritableMemo } from "@solid-primitives/memo"; -import { convertFileSrc } from "~/electron/core"; -import { appDataDir, resolveResource } from "~/electron/path"; -import { - BaseDirectory, - exists, - readDir, - writeFile, -} from "~/electron/fs"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { batch, @@ -49,6 +40,10 @@ import gradientBg from "~/assets/illustrations/gradient.webp"; import imageBg from "~/assets/illustrations/image.webp"; import transparentBg from "~/assets/illustrations/transparent.webp"; import { Toggle } from "~/components/Toggle"; +import { convertFileSrc } from "~/electron/core"; +import { BaseDirectory, exists, readDir, writeFile } from "~/electron/fs"; +import { type as ostype } from "~/electron/os"; +import { appDataDir, resolveResource } from "~/electron/path"; import { generalSettingsStore } from "~/store"; import { normalizeOpaqueHexColor } from "~/utils/hex-color"; import { diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index 6e54efb8f9f..4664ee299b8 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -5,11 +5,6 @@ import { trackDeep } from "@solid-primitives/deep"; import { debounce, throttle } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation, createQuery, skipToken } from "@tanstack/solid-query"; -import { convertFileSrc } from "~/electron/core"; -import { LogicalPosition } from "~/electron/dpi"; -import { Menu } from "~/electron/menu"; -import { getCurrentWindow } from "~/electron/window"; -import { ask } from "~/electron/dialog"; import { createEffect, createMemo, @@ -38,6 +33,11 @@ import { type Ratio, } from "~/components/Cropper"; import { Toggle } from "~/components/Toggle"; +import { convertFileSrc } from "~/electron/core"; +import { ask } from "~/electron/dialog"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu } from "~/electron/menu"; +import { getCurrentWindow } from "~/electron/window"; import { composeEventHandlers } from "~/utils/composeEventHandlers"; import { createTauriEventListener } from "~/utils/createEventListener"; import { commands, events } from "~/utils/tauri"; diff --git a/apps/desktop/src/routes/editor/EditorErrorScreen.tsx b/apps/desktop/src/routes/editor/EditorErrorScreen.tsx index 41c8d130621..aca4ad6363e 100644 --- a/apps/desktop/src/routes/editor/EditorErrorScreen.tsx +++ b/apps/desktop/src/routes/editor/EditorErrorScreen.tsx @@ -1,9 +1,9 @@ import { Button } from "@cap/ui-solid"; import { createMutation } from "@tanstack/solid-query"; -import { revealItemInDir } from "~/electron/opener"; -import { type as ostype } from "~/electron/os"; import { Show } from "solid-js"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import { revealItemInDir } from "~/electron/opener"; +import { type as ostype } from "~/electron/os"; import { commands } from "~/utils/tauri"; import IconAlertTriangle from "~icons/lucide/alert-triangle"; import IconFolder from "~icons/lucide/folder"; diff --git a/apps/desktop/src/routes/editor/ExportPage.tsx b/apps/desktop/src/routes/editor/ExportPage.tsx index f415e455d02..cf6f2549c2f 100644 --- a/apps/desktop/src/routes/editor/ExportPage.tsx +++ b/apps/desktop/src/routes/editor/ExportPage.tsx @@ -2,11 +2,6 @@ import { Button } from "@cap/ui-solid"; import { debounce } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation } from "@tanstack/solid-query"; -import { Channel } from "~/electron/core"; -import { CheckMenuItem, Menu } from "~/electron/menu"; -import { ask } from "~/electron/dialog"; -import { remove } from "~/electron/fs"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, @@ -25,6 +20,11 @@ import toast from "solid-toast"; import { SignInButton } from "~/components/SignInButton"; import Tooltip from "~/components/Tooltip"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import { Channel } from "~/electron/core"; +import { ask } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import { CheckMenuItem, Menu } from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; import { authStore } from "~/store"; import { trackEvent } from "~/utils/analytics"; import { createSignInMutation } from "~/utils/auth"; diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index dd9216e81d4..ceb57a55b20 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -1,8 +1,4 @@ import { Button } from "@cap/ui-solid"; -import type { UnlistenFn } from "~/electron/event"; -import { ask } from "~/electron/dialog"; -import { revealItemInDir } from "~/electron/opener"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { type ComponentProps, @@ -16,6 +12,10 @@ import { import Tooltip from "~/components/Tooltip"; import CaptionControlsMacOS from "~/components/titlebar/controls/CaptionControlsMacOS"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import { ask } from "~/electron/dialog"; +import type { UnlistenFn } from "~/electron/event"; +import { revealItemInDir } from "~/electron/opener"; +import { type as ostype } from "~/electron/os"; import { trackEvent } from "~/utils/analytics"; import { commands } from "~/utils/tauri"; import { initializeTitlebar } from "~/utils/titlebar-state"; diff --git a/apps/desktop/src/routes/editor/ImportProgress.tsx b/apps/desktop/src/routes/editor/ImportProgress.tsx index cf3537f6d7a..f4ae3b2ba6b 100644 --- a/apps/desktop/src/routes/editor/ImportProgress.tsx +++ b/apps/desktop/src/routes/editor/ImportProgress.tsx @@ -1,6 +1,6 @@ import { Button } from "@cap/ui-solid"; -import { getCurrentWindow } from "~/electron/window"; import { createSignal, Match, onCleanup, onMount, Switch } from "solid-js"; +import { getCurrentWindow } from "~/electron/window"; import { events, type VideoImportProgress as VideoImportProgressEvent, diff --git a/apps/desktop/src/routes/editor/PerformanceOverlay.tsx b/apps/desktop/src/routes/editor/PerformanceOverlay.tsx index 03a17b0a0ab..0ecc4252866 100644 --- a/apps/desktop/src/routes/editor/PerformanceOverlay.tsx +++ b/apps/desktop/src/routes/editor/PerformanceOverlay.tsx @@ -1,4 +1,3 @@ -import { writeText } from "~/electron/clipboard"; import { createEffect, createMemo, @@ -8,6 +7,7 @@ import { Show, } from "solid-js"; import toast from "solid-toast"; +import { writeText } from "~/electron/clipboard"; import { useEditorContext } from "./context"; type PerformanceOverlayProps = { diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index 0782d38aa30..535086045a0 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -2,12 +2,11 @@ import { Select as KSelect } from "@kobalte/core/select"; import { ToggleButton as KToggleButton } from "@kobalte/core/toggle-button"; import { createElementBounds } from "@solid-primitives/bounds"; import { debounce } from "@solid-primitives/scheduled"; -import { Menu } from "~/electron/menu"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, createSignal, onMount, Show } from "solid-js"; - import Tooltip from "~/components/Tooltip"; +import { Menu } from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; import { captionsStore } from "~/store/captions"; import { commands } from "~/utils/tauri"; import AspectRatioSelect from "./AspectRatioSelect"; diff --git a/apps/desktop/src/routes/editor/ShareButton.tsx b/apps/desktop/src/routes/editor/ShareButton.tsx index 922b1f548b7..50ce24b77d0 100644 --- a/apps/desktop/src/routes/editor/ShareButton.tsx +++ b/apps/desktop/src/routes/editor/ShareButton.tsx @@ -1,10 +1,10 @@ import { Button } from "@cap/ui-solid"; import { Select as KSelect } from "@kobalte/core/select"; import { createMutation } from "@tanstack/solid-query"; -import { Channel } from "~/electron/core"; import { createSignal, Show } from "solid-js"; import { createStore, produce, reconcile } from "solid-js/store"; import Tooltip from "~/components/Tooltip"; +import { Channel } from "~/electron/core"; import { createProgressBar } from "~/routes/editor/utils"; import { authStore } from "~/store"; import { exportVideo } from "~/utils/export"; diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 3a3376e0303..7e22bb74e93 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -1,6 +1,5 @@ import { Button } from "@cap/ui-solid"; import { createEventListenerMap } from "@solid-primitives/event-listener"; -import { Menu } from "~/electron/menu"; import { cx } from "cva"; import { Array, Option } from "effect"; import { @@ -15,6 +14,7 @@ import { Switch, } from "solid-js"; import { produce } from "solid-js/store"; +import { Menu } from "~/electron/menu"; import { commands } from "~/utils/tauri"; import { useEditorContext } from "../context"; import { diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 492b33e7e7a..afb0b0fec4f 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1,8 +1,5 @@ import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListener } from "@solid-primitives/event-listener"; -import { LogicalPosition } from "~/electron/dpi"; -import { Menu, MenuItem } from "~/electron/menu"; -import { platform } from "~/electron/os"; import { cx } from "cva"; import { batch, @@ -19,6 +16,9 @@ import { } from "solid-js"; import { produce } from "solid-js/store"; import toast from "solid-toast"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu, MenuItem } from "~/electron/menu"; +import { platform } from "~/electron/os"; import "./styles.css"; diff --git a/apps/desktop/src/routes/editor/TranscriptPage.tsx b/apps/desktop/src/routes/editor/TranscriptPage.tsx index 9047b87c25a..3e4234e84ed 100644 --- a/apps/desktop/src/routes/editor/TranscriptPage.tsx +++ b/apps/desktop/src/routes/editor/TranscriptPage.tsx @@ -1,7 +1,5 @@ import { createEventListener } from "@solid-primitives/event-listener"; import { makePersisted } from "@solid-primitives/storage"; -import { save } from "~/electron/dialog"; -import { writeTextFile } from "~/electron/fs"; import { cx } from "cva"; import { batch, @@ -14,6 +12,8 @@ import { } from "solid-js"; import { produce } from "solid-js/store"; import toast from "solid-toast"; +import { save } from "~/electron/dialog"; +import { writeTextFile } from "~/electron/fs"; import { defaultCaptionSettings } from "~/store/captions"; import { commands } from "~/utils/tauri"; import { diff --git a/apps/desktop/src/routes/editor/captions.ts b/apps/desktop/src/routes/editor/captions.ts index 9a3abb7e3b0..c6ad62c5dfb 100644 --- a/apps/desktop/src/routes/editor/captions.ts +++ b/apps/desktop/src/routes/editor/captions.ts @@ -1,5 +1,5 @@ -import { appLocalDataDir, join } from "~/electron/path"; import { arch, type as osType } from "~/electron/os"; +import { appLocalDataDir, join } from "~/electron/path"; import { defaultCaptionSettings } from "~/store/captions"; import { diff --git a/apps/desktop/src/routes/editor/editor-skeleton.tsx b/apps/desktop/src/routes/editor/editor-skeleton.tsx index fc4eb2692c1..dbbf643b1b8 100644 --- a/apps/desktop/src/routes/editor/editor-skeleton.tsx +++ b/apps/desktop/src/routes/editor/editor-skeleton.tsx @@ -1,6 +1,6 @@ -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import { type as ostype } from "~/electron/os"; const DEFAULT_TIMELINE_HEIGHT = 260; const MIN_PLAYER_HEIGHT = 336; diff --git a/apps/desktop/src/routes/editor/index.tsx b/apps/desktop/src/routes/editor/index.tsx index 2edaac2ac52..7b88cb6ab92 100644 --- a/apps/desktop/src/routes/editor/index.tsx +++ b/apps/desktop/src/routes/editor/index.tsx @@ -1,7 +1,7 @@ -import { Effect, getCurrentWindow } from "~/electron/window"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, onMount, Suspense } from "solid-js"; +import { type as ostype } from "~/electron/os"; +import { Effect, getCurrentWindow } from "~/electron/window"; import { generalSettingsStore } from "~/store"; import { commands } from "~/utils/tauri"; import { Editor } from "./Editor"; diff --git a/apps/desktop/src/routes/editor/utils.ts b/apps/desktop/src/routes/editor/utils.ts index 3280328925c..b2dfd9fdd11 100644 --- a/apps/desktop/src/routes/editor/utils.ts +++ b/apps/desktop/src/routes/editor/utils.ts @@ -12,8 +12,8 @@ export function formatTime(secs: number, fps?: number) { return str; } -import { getCurrentWindow, ProgressBarStatus } from "~/electron/window"; import { createEffect } from "solid-js"; +import { getCurrentWindow, ProgressBarStatus } from "~/electron/window"; export function createProgressBar(progress: () => number | undefined) { const currentWindow = getCurrentWindow(); diff --git a/apps/desktop/src/routes/in-progress-recording.tsx b/apps/desktop/src/routes/in-progress-recording.tsx index cb19116d94e..cc72bc99b9b 100644 --- a/apps/desktop/src/routes/in-progress-recording.tsx +++ b/apps/desktop/src/routes/in-progress-recording.tsx @@ -1,15 +1,5 @@ import { createTimer } from "@solid-primitives/timer"; import { createMutation } from "@tanstack/solid-query"; -import { LogicalPosition } from "~/electron/dpi"; -import { - CheckMenuItem, - Menu, - MenuItem, - PredefinedMenuItem, -} from "~/electron/menu"; -import { getCurrentWindow } from "~/electron/window"; -import * as dialog from "~/electron/dialog"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { type ComponentProps, @@ -23,6 +13,16 @@ import { } from "solid-js"; import { createStore, produce } from "solid-js/store"; import { TransitionGroup } from "solid-transition-group"; +import * as dialog from "~/electron/dialog"; +import { LogicalPosition } from "~/electron/dpi"; +import { + CheckMenuItem, + Menu, + MenuItem, + PredefinedMenuItem, +} from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; +import { getCurrentWindow } from "~/electron/window"; import { authStore } from "~/store"; import { getCameraWindow } from "~/utils/camera-window"; import { createTauriEventListener } from "~/utils/createEventListener"; diff --git a/apps/desktop/src/routes/mode-select.tsx b/apps/desktop/src/routes/mode-select.tsx index 48d34e92fcc..19c8e3fed68 100644 --- a/apps/desktop/src/routes/mode-select.tsx +++ b/apps/desktop/src/routes/mode-select.tsx @@ -1,9 +1,9 @@ -import type { UnlistenFn } from "~/electron/event"; -import { getCurrentWindow, LogicalSize } from "~/electron/window"; -import { type as ostype } from "~/electron/os"; import { onCleanup, onMount } from "solid-js"; import ModeSelect from "~/components/ModeSelect"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import type { UnlistenFn } from "~/electron/event"; +import { type as ostype } from "~/electron/os"; +import { getCurrentWindow, LogicalSize } from "~/electron/window"; import { initializeTitlebar } from "~/utils/titlebar-state"; const ModeSelectWindow = () => { diff --git a/apps/desktop/src/routes/recordings-overlay.tsx b/apps/desktop/src/routes/recordings-overlay.tsx index efa4652cbc1..7bff5879c2f 100644 --- a/apps/desktop/src/routes/recordings-overlay.tsx +++ b/apps/desktop/src/routes/recordings-overlay.tsx @@ -3,7 +3,6 @@ import Tooltip from "@corvu/tooltip"; import { createElementBounds } from "@solid-primitives/bounds"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation, createQuery } from "@tanstack/solid-query"; -import { Channel, convertFileSrc } from "~/electron/core"; import { cx } from "cva"; import { type Accessor, @@ -23,6 +22,7 @@ import { } from "solid-js"; import { createStore, produce, type SetStoreFunction } from "solid-js/store"; import { TransitionGroup } from "solid-transition-group"; +import { Channel, convertFileSrc } from "~/electron/core"; import { authStore } from "~/store"; import { createTauriEventListener } from "~/utils/createEventListener"; import { createExportToFileTask, exportVideo } from "~/utils/export"; diff --git a/apps/desktop/src/routes/screenshot-editor/Editor.tsx b/apps/desktop/src/routes/screenshot-editor/Editor.tsx index 03361cbbfa9..d5da07ed2b4 100644 --- a/apps/desktop/src/routes/screenshot-editor/Editor.tsx +++ b/apps/desktop/src/routes/screenshot-editor/Editor.tsx @@ -1,9 +1,6 @@ import { Button } from "@cap/ui-solid"; import { NumberField } from "@kobalte/core/number-field"; import { makePersisted } from "@solid-primitives/storage"; -import { convertFileSrc } from "~/electron/core"; -import { LogicalPosition } from "~/electron/dpi"; -import { Menu } from "~/electron/menu"; import { createEffect, createSignal, @@ -23,6 +20,9 @@ import { createCropOptionsMenuItems, type Ratio, } from "~/components/Cropper"; +import { convertFileSrc } from "~/electron/core"; +import { LogicalPosition } from "~/electron/dpi"; +import { Menu } from "~/electron/menu"; import { composeEventHandlers } from "~/utils/composeEventHandlers"; import IconCapCircleX from "~icons/cap/circle-x"; import IconLucideMaximize from "~icons/lucide/maximize"; diff --git a/apps/desktop/src/routes/screenshot-editor/Header.tsx b/apps/desktop/src/routes/screenshot-editor/Header.tsx index d0a3069b8f4..ff5e124a1f0 100644 --- a/apps/desktop/src/routes/screenshot-editor/Header.tsx +++ b/apps/desktop/src/routes/screenshot-editor/Header.tsx @@ -1,13 +1,13 @@ import { DropdownMenu } from "@kobalte/core/dropdown-menu"; -import { getCurrentWindow } from "~/electron/window"; -import { ask } from "~/electron/dialog"; -import { remove } from "~/electron/fs"; -import { revealItemInDir } from "~/electron/opener"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, onCleanup, Suspense } from "solid-js"; import CaptionControlsMacOS from "~/components/titlebar/controls/CaptionControlsMacOS"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import { ask } from "~/electron/dialog"; +import { remove } from "~/electron/fs"; +import { revealItemInDir } from "~/electron/opener"; +import { type as ostype } from "~/electron/os"; +import { getCurrentWindow } from "~/electron/window"; import IconCapCrop from "~icons/cap/crop"; import IconCapTrash from "~icons/cap/trash"; import IconLucideCopy from "~icons/lucide/copy"; diff --git a/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx b/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx index 091d76708ed..73cb0160cd8 100644 --- a/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx +++ b/apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx @@ -1,5 +1,5 @@ -import { invoke } from "~/electron/core"; import { createEffect, createMemo, createSignal, For } from "solid-js"; +import { invoke } from "~/electron/core"; import { type ScreenshotProject, useScreenshotEditorContext } from "./context"; type Rect = { diff --git a/apps/desktop/src/routes/screenshot-editor/context.tsx b/apps/desktop/src/routes/screenshot-editor/context.tsx index 96c8ff35138..8335d605a16 100644 --- a/apps/desktop/src/routes/screenshot-editor/context.tsx +++ b/apps/desktop/src/routes/screenshot-editor/context.tsx @@ -2,7 +2,6 @@ import { createContextProvider } from "@solid-primitives/context"; import { trackStore } from "@solid-primitives/deep"; import { debounce, throttle } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; -import { convertFileSrc, invoke } from "~/electron/core"; import { createEffect, createResource, @@ -11,6 +10,7 @@ import { onCleanup, } from "solid-js"; import { createStore, reconcile, unwrap } from "solid-js/store"; +import { convertFileSrc, invoke } from "~/electron/core"; import { createLazySignal, type FrameData } from "~/utils/socket"; import { type Annotation, diff --git a/apps/desktop/src/routes/screenshot-editor/index.tsx b/apps/desktop/src/routes/screenshot-editor/index.tsx index 005164dd3ef..4707fedff18 100644 --- a/apps/desktop/src/routes/screenshot-editor/index.tsx +++ b/apps/desktop/src/routes/screenshot-editor/index.tsx @@ -1,7 +1,7 @@ -import { Effect, getCurrentWindow } from "~/electron/window"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect } from "solid-js"; +import { type as ostype } from "~/electron/os"; +import { Effect, getCurrentWindow } from "~/electron/window"; import { generalSettingsStore } from "~/store"; import { commands } from "~/utils/tauri"; import { ScreenshotEditorProvider } from "./context"; diff --git a/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx b/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx index 659d8bc830a..7791563adcf 100644 --- a/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx +++ b/apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx @@ -1,9 +1,6 @@ import { Popover } from "@kobalte/core/popover"; import { RadioGroup as KRadioGroup } from "@kobalte/core/radio-group"; import { Tabs as KTabs } from "@kobalte/core/tabs"; -import { convertFileSrc, invoke } from "~/electron/core"; -import { appDataDir, resolveResource } from "~/electron/path"; -import { BaseDirectory, writeFile } from "~/electron/fs"; import { batch, createMemo, @@ -12,6 +9,9 @@ import { For, Show, } from "solid-js"; +import { convertFileSrc, invoke } from "~/electron/core"; +import { BaseDirectory, writeFile } from "~/electron/fs"; +import { appDataDir, resolveResource } from "~/electron/path"; import type { BackgroundSource } from "~/utils/tauri"; import IconCapBgBlur from "~icons/cap/bg-blur"; import IconCapCircleX from "~icons/cap/circle-x"; diff --git a/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx b/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx index 9f3f0ad57fd..0085054991d 100644 --- a/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx +++ b/apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx @@ -1,5 +1,5 @@ -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; +import { type as ostype } from "~/electron/os"; import IconCapLogo from "~icons/cap/logo"; function SkeletonPulse(props: { class?: string }) { diff --git a/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts b/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts index 35df707b0cf..500c54104bb 100644 --- a/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts +++ b/apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts @@ -1,8 +1,8 @@ -import { save } from "~/electron/dialog"; -import { writeFile } from "~/electron/fs"; import { createSignal } from "solid-js"; import { unwrap } from "solid-js/store"; import toast from "solid-toast"; +import { save } from "~/electron/dialog"; +import { writeFile } from "~/electron/fs"; import { commands } from "~/utils/tauri"; import { useScreenshotEditorContext } from "./context"; import { diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index 5f32f7513a2..5d191974672 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -5,20 +5,6 @@ import { createElementSize } from "@solid-primitives/resize-observer"; import { makePersisted } from "@solid-primitives/storage"; import { useSearchParams } from "@solidjs/router"; import { createMutation, useQuery } from "@tanstack/solid-query"; -import { - LogicalPosition, - type PhysicalPosition, - type PhysicalSize, -} from "~/electron/dpi"; -import { emit } from "~/electron/event"; -import { - CheckMenuItem, - Menu, - MenuItem, - PredefinedMenuItem, -} from "~/electron/menu"; -import { WebviewWindow } from "~/electron/webviewWindow"; -import { type as ostype } from "~/electron/os"; import { createEffect, createMemo, @@ -58,6 +44,20 @@ import { } from "~/components/Cropper"; import ModeSelect from "~/components/ModeSelect"; import SelectionHint from "~/components/selection-hint"; +import { + LogicalPosition, + type PhysicalPosition, + type PhysicalSize, +} from "~/electron/dpi"; +import { emit } from "~/electron/event"; +import { + CheckMenuItem, + Menu, + MenuItem, + PredefinedMenuItem, +} from "~/electron/menu"; +import { type as ostype } from "~/electron/os"; +import { WebviewWindow } from "~/electron/webviewWindow"; import { authStore, generalSettingsStore, diff --git a/apps/desktop/src/routes/teleprompter.tsx b/apps/desktop/src/routes/teleprompter.tsx index ea77390e73f..41c29bd4d40 100644 --- a/apps/desktop/src/routes/teleprompter.tsx +++ b/apps/desktop/src/routes/teleprompter.tsx @@ -1,6 +1,3 @@ -import type { UnlistenFn } from "~/electron/event"; -import { getCurrentWebviewWindow } from "~/electron/webviewWindow"; -import { type as ostype } from "~/electron/os"; import { cx } from "cva"; import { createEffect, @@ -11,10 +8,12 @@ import { onMount, Show, } from "solid-js"; - import { Toggle } from "~/components/Toggle"; import CaptionControlsMacOS from "~/components/titlebar/controls/CaptionControlsMacOS"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; +import type { UnlistenFn } from "~/electron/event"; +import { type as ostype } from "~/electron/os"; +import { getCurrentWebviewWindow } from "~/electron/webviewWindow"; import { type TeleprompterStore, teleprompterDefaults, diff --git a/apps/desktop/src/routes/window-capture-occluder.tsx b/apps/desktop/src/routes/window-capture-occluder.tsx index 7586785cc02..c638746a157 100644 --- a/apps/desktop/src/routes/window-capture-occluder.tsx +++ b/apps/desktop/src/routes/window-capture-occluder.tsx @@ -1,5 +1,5 @@ -import { getAllWindows } from "~/electron/window"; import { Show, Suspense } from "solid-js"; +import { getAllWindows } from "~/electron/window"; import { isCameraWindowLabel } from "~/utils/camera-window"; import { createCurrentRecordingQuery } from "~/utils/queries"; diff --git a/apps/desktop/src/store.ts b/apps/desktop/src/store.ts index f07f2a729d8..2c05c7b5842 100644 --- a/apps/desktop/src/store.ts +++ b/apps/desktop/src/store.ts @@ -1,6 +1,6 @@ import { createQuery } from "@tanstack/solid-query"; -import { Store } from "~/electron/store"; import { onCleanup } from "solid-js"; +import { Store } from "~/electron/store"; import type { AutomationsStore } from "~/utils/automations"; import { type GeneralSettingsStore, diff --git a/apps/desktop/src/utils/auth.ts b/apps/desktop/src/utils/auth.ts index 5b1ae9c3f34..43eb48acc8c 100644 --- a/apps/desktop/src/utils/auth.ts +++ b/apps/desktop/src/utils/auth.ts @@ -1,11 +1,11 @@ import { createMutation } from "@tanstack/solid-query"; +import { z } from "zod"; +import callbackTemplate from "~/components/callback.template"; import { invoke } from "~/electron/core"; -import { listen } from "~/electron/event"; -import { getCurrentWindow } from "~/electron/window"; import { onOpenUrl } from "~/electron/deep-link"; +import { listen } from "~/electron/event"; import * as shell from "~/electron/shell"; -import { z } from "zod"; -import callbackTemplate from "~/components/callback.template"; +import { getCurrentWindow } from "~/electron/window"; import { authStore, generalSettingsStore } from "~/store"; import { identifyUser, trackEvent } from "./analytics"; import { clientEnv } from "./env"; diff --git a/apps/desktop/src/utils/importMedia.ts b/apps/desktop/src/utils/importMedia.ts index 0c88349842d..75b06778e42 100644 --- a/apps/desktop/src/utils/importMedia.ts +++ b/apps/desktop/src/utils/importMedia.ts @@ -1,6 +1,6 @@ import { invoke } from "~/electron/core"; -import { getCurrentWindow } from "~/electron/window"; import * as dialog from "~/electron/dialog"; +import { getCurrentWindow } from "~/electron/window"; import { commands } from "~/utils/tauri"; const videoExtensions = [ diff --git a/apps/desktop/src/utils/macos-window-material.ts b/apps/desktop/src/utils/macos-window-material.ts index 7c92ee112fb..36d78b1618d 100644 --- a/apps/desktop/src/utils/macos-window-material.ts +++ b/apps/desktop/src/utils/macos-window-material.ts @@ -1,12 +1,12 @@ import { invoke } from "~/electron/core"; import type { UnlistenFn } from "~/electron/event"; +import { type as osType, version as osVersion } from "~/electron/os"; import { Effect, EffectState, type Effects, getCurrentWindow, } from "~/electron/window"; -import { type as osType, version as osVersion } from "~/electron/os"; type MacOSWindowMaterial = "panel" | "settings" | "teleprompter"; diff --git a/apps/desktop/src/utils/recording.ts b/apps/desktop/src/utils/recording.ts index db9e7917d03..5f929c0bede 100644 --- a/apps/desktop/src/utils/recording.ts +++ b/apps/desktop/src/utils/recording.ts @@ -1,5 +1,5 @@ -import { emit } from "~/electron/event"; import * as dialog from "~/electron/dialog"; +import { emit } from "~/electron/event"; import { revealItemInDir } from "~/electron/opener"; import type { createOptionsQuery } from "./queries"; import { commands, type RecordingAction, type RecordingMode } from "./tauri"; diff --git a/apps/desktop/src/utils/teleprompter-window-options.ts b/apps/desktop/src/utils/teleprompter-window-options.ts index aae17a115d0..d8dad23d6c5 100644 --- a/apps/desktop/src/utils/teleprompter-window-options.ts +++ b/apps/desktop/src/utils/teleprompter-window-options.ts @@ -1,6 +1,6 @@ import { LogicalPosition } from "~/electron/dpi"; -import type { WebviewWindow } from "~/electron/webviewWindow"; import type { OsType } from "~/electron/os"; +import type { WebviewWindow } from "~/electron/webviewWindow"; export type TeleprompterWindowOptions = NonNullable< ConstructorParameters[1] diff --git a/apps/desktop/src/utils/teleprompter.ts b/apps/desktop/src/utils/teleprompter.ts index ed7a89d7042..a15164e3c10 100644 --- a/apps/desktop/src/utils/teleprompter.ts +++ b/apps/desktop/src/utils/teleprompter.ts @@ -1,5 +1,5 @@ -import { WebviewWindow } from "~/electron/webviewWindow"; import { type as ostype } from "~/electron/os"; +import { WebviewWindow } from "~/electron/webviewWindow"; import { commands } from "./tauri"; import { getTeleprompterWindowOptions } from "./teleprompter-window-options"; diff --git a/apps/desktop/src/utils/titlebar-state.ts b/apps/desktop/src/utils/titlebar-state.ts index 7ff0eb0318f..ce4425b9266 100644 --- a/apps/desktop/src/utils/titlebar-state.ts +++ b/apps/desktop/src/utils/titlebar-state.ts @@ -1,8 +1,8 @@ -import type { UnlistenFn } from "~/electron/event"; -import { getCurrentWindow } from "~/electron/window"; -import { type as ostype } from "~/electron/os"; import type { JSX } from "solid-js"; import { createStore } from "solid-js/store"; +import type { UnlistenFn } from "~/electron/event"; +import { type as ostype } from "~/electron/os"; +import { getCurrentWindow } from "~/electron/window"; export interface TitlebarState { height: string; diff --git a/apps/desktop/src/utils/web-api.ts b/apps/desktop/src/utils/web-api.ts index 7cef95da703..cdd354ae090 100644 --- a/apps/desktop/src/utils/web-api.ts +++ b/apps/desktop/src/utils/web-api.ts @@ -3,8 +3,8 @@ import { licenseContract, orgCustomDomainContract, } from "@cap/web-api-contract"; -import { fetch } from "~/electron/http"; import { type ApiFetcher, initClient } from "@ts-rest/core"; +import { fetch } from "~/electron/http"; import { authStore, generalSettingsStore } from "~/store"; import { clientEnv } from "./env"; diff --git a/scripts/build-desktop-binaries.mjs b/scripts/build-desktop-binaries.mjs index d2714aa5103..cbaccb36370 100644 --- a/scripts/build-desktop-binaries.mjs +++ b/scripts/build-desktop-binaries.mjs @@ -118,7 +118,10 @@ async function stageWindowsRuntimeLibraries() { const releaseDir = path.join(repoRoot, "target", "release"); for (const name of await fs.readdir(releaseDir)) { if (!name.toLowerCase().endsWith(".dll")) continue; - await fs.copyFile(path.join(releaseDir, name), path.join(binariesDir, name)); + await fs.copyFile( + path.join(releaseDir, name), + path.join(binariesDir, name), + ); } } From ba952c8b55de1766b8ce4750a00a159280e62551 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:09:16 +0100 Subject: [PATCH 06/31] fix: protect published release tags --- .github/workflows/desktop-release.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 86f565dc2dc..00875da55e4 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -43,6 +43,15 @@ jobs: ? `cap-nightly-v${{ inputs.version }}` : `cap-v${{ inputs.version }}`; const sha = "${{ inputs.sha }}"; + + let release; + try { + release = (await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag })).data; + if (!release.draft) throw new Error(`Release '${tag}' is already published: ${release.html_url}`); + } catch (error) { + if (error.status !== 404) throw error; + } + try { await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `tags/${tag}`, sha, force: true }); } catch (error) { @@ -50,10 +59,7 @@ jobs: await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/tags/${tag}`, sha }); } - let release; - try { - release = (await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag })).data; - if (!release.draft) throw new Error(`Release '${tag}' is already published: ${release.html_url}`); + if (release) { release = (await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, @@ -62,8 +68,7 @@ jobs: name: "${{ inputs.version }}", prerelease: "${{ inputs.channel }}" === "nightly", })).data; - } catch (error) { - if (error.status !== 404) throw error; + } else { release = (await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, From c75998b94ac739d43cc840a190ffeab7fd2a75c9 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:33:53 +0100 Subject: [PATCH 07/31] fix: make release tags immutable --- .github/workflows/desktop-release.yml | 37 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 00875da55e4..ea8d8d50d32 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -21,6 +21,10 @@ on: type: string default: "" +concurrency: + group: desktop-release-${{ inputs.channel }}-${{ inputs.version }} + cancel-in-progress: false + env: SENTRY_ORG: cap-s2 SENTRY_PROJECT: cap-desktop @@ -34,7 +38,7 @@ jobs: permissions: contents: write steps: - - name: Create or update release tag and draft + - name: Prepare immutable release tag and draft id: release uses: actions/github-script@v7 with: @@ -43,6 +47,24 @@ jobs: ? `cap-nightly-v${{ inputs.version }}` : `cap-v${{ inputs.version }}`; const sha = "${{ inputs.sha }}"; + const tagRef = `tags/${tag}`; + + let tagExists = true; + try { + const existingTag = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: tagRef, + }); + if (existingTag.data.object.sha !== sha) { + throw new Error( + `Tag '${tag}' already points to ${existingTag.data.object.sha}; refusing to move it to ${sha}`, + ); + } + } catch (error) { + if (error.status === 404) tagExists = false; + else throw error; + } let release; try { @@ -52,12 +74,13 @@ jobs: if (error.status !== 404) throw error; } - try { - await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `tags/${tag}`, sha, force: true }); - } catch (error) { - if (error.status !== 422 && error.status !== 404) throw error; - await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/tags/${tag}`, sha }); - } + if (!tagExists) + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/${tagRef}`, + sha, + }); if (release) { release = (await github.rest.repos.updateRelease({ From 491c2ac27f4b79830b330e1156dedbae06cc2b39 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:33:54 +0100 Subject: [PATCH 08/31] fix: preserve basename suffix semantics --- apps/desktop/src/electron/path.test.ts | 20 ++++++++++++++++++++ apps/desktop/src/electron/path.ts | 11 +++++------ 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/src/electron/path.test.ts diff --git a/apps/desktop/src/electron/path.test.ts b/apps/desktop/src/electron/path.test.ts new file mode 100644 index 00000000000..b3e86a89bfd --- /dev/null +++ b/apps/desktop/src/electron/path.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { basename } from "./path"; + +describe("Electron path basename", () => { + it("keeps the filename when no suffix is provided", async () => { + await expect(basename("/tmp/video.mp4")).resolves.toBe("video.mp4"); + await expect(basename("/tmp/video.mp4", "")).resolves.toBe("video.mp4"); + }); + + it("removes only a matching trailing suffix", async () => { + await expect(basename("/tmp/video.mp4", ".mp4")).resolves.toBe("video"); + await expect(basename("/tmp/.mp4-video", ".mp4")).resolves.toBe( + ".mp4-video", + ); + await expect(basename("C:\\tmp\\video.mp4", ".mov")).resolves.toBe( + "video.mp4", + ); + }); +}); diff --git a/apps/desktop/src/electron/path.ts b/apps/desktop/src/electron/path.ts index bed56c0bdc4..bff11bcd50d 100644 --- a/apps/desktop/src/electron/path.ts +++ b/apps/desktop/src/electron/path.ts @@ -38,13 +38,12 @@ export const dirname = (path: string) => native("path.resolve", { paths: [path, ".."] }); export const extname = (path: string) => Promise.resolve(path.match(/\.[^./\\]+$/)?.[0] ?? ""); -export const basename = (path: string, ext?: string) => - Promise.resolve( - path - .split(/[\\/]/) - .pop() - ?.replace(ext ?? /$^/, "") ?? "", +export const basename = (path: string, ext?: string) => { + const name = path.split(/[\\/]/).pop() ?? ""; + return Promise.resolve( + ext && name.endsWith(ext) ? name.slice(0, -ext.length) : name, ); +}; export const isAbsolute = (path: string) => Promise.resolve(/^(?:[a-zA-Z]:[\\/]|\/)/.test(path)); export const sep = () => From e090512eba48e56b86369859bba2066df36d6150 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:33:54 +0100 Subject: [PATCH 09/31] fix: avoid logging user identifiers --- apps/desktop/src-backend/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/desktop/src-backend/src/lib.rs b/apps/desktop/src-backend/src/lib.rs index cc48574d3a4..edce9dbfe79 100644 --- a/apps/desktop/src-backend/src/lib.rs +++ b/apps/desktop/src-backend/src/lib.rs @@ -4056,10 +4056,7 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result { return Ok(true); } - println!( - "Fetching plan for user {}", - auth.user_id.as_deref().unwrap_or("unknown") - ); + println!("Fetching plan for authenticated user"); let response = app .authed_api_request("/api/desktop/plan", |client, url| client.get(url)) .await From eb048eeea1ca7018a9af0ff79beecd5fdba560c9 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:33:54 +0100 Subject: [PATCH 10/31] style: format Loom storage access --- apps/web/actions/loom.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts index ce2c4af5e7f..e4e8c6d34a1 100644 --- a/apps/web/actions/loom.ts +++ b/apps/web/actions/loom.ts @@ -405,10 +405,7 @@ async function importLoomVideoForOwner({ fetchLoomOEmbed(loomVideoId), ]); - const writableResult = await Storage.getWritableAccessForUser( - ownerId, - orgId, - ) + const writableResult = await Storage.getWritableAccessForUser(ownerId, orgId) .pipe(runPromise) .then( (value) => ({ ok: true as const, value }), From 4d1a993bddd66dbb855cf67dc27975d7005abb65 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:56:09 +0100 Subject: [PATCH 11/31] fix: harden Loom URL handling --- apps/web/__tests__/unit/loom-url.test.ts | 52 ++++++++++++++++++ apps/web/actions/loom.ts | 61 +++++++++++---------- apps/web/lib/loom-browser-download.ts | 69 ++++++++++++------------ apps/web/lib/loom-url.ts | 46 ++++++++++++++++ apps/web/workflows/import-loom-video.ts | 25 +++++++-- 5 files changed, 186 insertions(+), 67 deletions(-) create mode 100644 apps/web/__tests__/unit/loom-url.test.ts create mode 100644 apps/web/lib/loom-url.ts diff --git a/apps/web/__tests__/unit/loom-url.test.ts b/apps/web/__tests__/unit/loom-url.test.ts new file mode 100644 index 00000000000..5052377113e --- /dev/null +++ b/apps/web/__tests__/unit/loom-url.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { + extractLoomVideoId, + isValidLoomVideoId, + normalizeLoomMediaUrl, +} from "@/lib/loom-url"; + +describe("Loom URL security", () => { + it("accepts HTTPS Loom URLs and IDs", () => { + expect( + extractLoomVideoId( + "https://www.loom.com/share/05f424bf8781404091f365c9f5231d86?sid=abc", + ), + ).toBe("05f424bf8781404091f365c9f5231d86"); + expect(extractLoomVideoId("https://loom.com/embed/loom-abc1234567")).toBe( + "loom-abc1234567", + ); + expect(isValidLoomVideoId("loom-abc1234567")).toBe(true); + }); + + it("rejects lookalike hosts and path-injection IDs", () => { + expect( + extractLoomVideoId( + "https://www.loom.com.evil.test/share/loom-abc1234567", + ), + ).toBeNull(); + expect( + extractLoomVideoId("https://evil-loom.com/share/loom-abc1234567"), + ).toBeNull(); + expect( + extractLoomVideoId("http://www.loom.com/share/loom-abc1234567"), + ).toBeNull(); + expect( + extractLoomVideoId("https://www.loom.com/share/loom%2Fevil12345"), + ).toBeNull(); + }); + + it("accepts only HTTPS Loom media URLs", () => { + expect(normalizeLoomMediaUrl("https://cdn.loom.com/video.mp4")).toBe( + "https://cdn.loom.com/video.mp4", + ); + expect( + normalizeLoomMediaUrl( + "https://luna.loom.com/id/video/playlist.mpd?Policy=abc", + ), + ).toBe("https://luna.loom.com/id/video/playlist.mpd?Policy=abc"); + expect( + normalizeLoomMediaUrl("https://cdn.loom.com.evil.test/video.mp4"), + ).toBeNull(); + expect(normalizeLoomMediaUrl("https://127.0.0.1/video.mp4")).toBeNull(); + }); +}); diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts index e4e8c6d34a1..83188e408f9 100644 --- a/apps/web/actions/loom.ts +++ b/apps/web/actions/loom.ts @@ -34,6 +34,12 @@ import { getOrganizationAccess, requireOrganizationAccess, } from "@/actions/organization/authorization"; +import { + extractLoomVideoId, + isValidLoomVideoId, + LOOM_ORIGIN, + normalizeLoomMediaUrl, +} from "@/lib/loom-url"; import { provisionOrganizationInvitee } from "@/lib/organization-provisioning"; import { canManageOrganizationSettings } from "@/lib/permissions/roles"; import { runPromise } from "@/lib/server"; @@ -44,6 +50,7 @@ interface LoomUrlResponse { } type LoomDownloadMode = "direct-download" | "browser-conversion"; +type LoomEndpoint = "raw-url" | "transcoded-url"; interface LoomDownloadResult { success: boolean; @@ -127,33 +134,15 @@ async function createLoomImportRateLimitCheck(userId: User.UserId) { }; } -function extractLoomVideoId(url: string): string | null { - try { - const parsed = new URL(url); - if (!parsed.hostname.includes("loom.com")) { - return null; - } - - const pathParts = parsed.pathname.split("/").filter(Boolean); - const id = pathParts[pathParts.length - 1] ?? null; - - if (!id || id.length < 10) { - return null; - } - - return id.split("?")[0] ?? null; - } catch { - return null; - } -} - async function fetchLoomEndpoint( videoId: string, - endpoint: string, + endpoint: LoomEndpoint, includeBody = true, ): Promise { try { - const options: RequestInit = { method: "POST" }; + if (!isValidLoomVideoId(videoId)) return null; + + const options: RequestInit = { method: "POST", redirect: "error" }; if (includeBody) { options.headers = { "Content-Type": "application/json", @@ -167,8 +156,13 @@ async function fetchLoomEndpoint( }); } + const encodedVideoId = encodeURIComponent(videoId); + const endpointPath = endpoint === "raw-url" ? "raw-url" : "transcoded-url"; const response = await fetch( - `https://www.loom.com/api/campaigns/sessions/${videoId}/${endpoint}`, + new URL( + `/api/campaigns/sessions/${encodedVideoId}/${endpointPath}`, + LOOM_ORIGIN, + ), options, ); @@ -182,7 +176,7 @@ async function fetchLoomEndpoint( } const data: LoomUrlResponse = JSON.parse(text); - return data.url ?? null; + return data.url ? normalizeLoomMediaUrl(data.url) : null; } catch { return null; } @@ -230,7 +224,10 @@ function isDirectMp4Url(url: string): boolean { } async function getLoomDownloadUrl(loomVideoId: string): Promise { - const requestVariants: Array<{ endpoint: string; includeBody: boolean }> = [ + const requestVariants: Array<{ + endpoint: LoomEndpoint; + includeBody: boolean; + }> = [ { endpoint: "transcoded-url", includeBody: true }, { endpoint: "raw-url", includeBody: true }, { endpoint: "transcoded-url", includeBody: false }, @@ -255,10 +252,16 @@ async function fetchLoomOEmbed( loomVideoId: string, ): Promise<{ duration?: number; width?: number; height?: number } | null> { try { - const response = await fetch( - `https://www.loom.com/v1/oembed?url=https://www.loom.com/share/${loomVideoId}`, - { headers: { Accept: "application/json" } }, + const shareUrl = new URL( + `/share/${encodeURIComponent(loomVideoId)}`, + LOOM_ORIGIN, ); + const oembedUrl = new URL("/v1/oembed", LOOM_ORIGIN); + oembedUrl.searchParams.set("url", shareUrl.toString()); + const response = await fetch(oembedUrl, { + redirect: "error", + headers: { Accept: "application/json" }, + }); if (!response.ok) return null; const data = await response.json(); return { @@ -414,7 +417,7 @@ async function importLoomVideoForOwner({ if (!writableResult.ok) { console.error( - `Loom import: failed to resolve storage access for user ${ownerId} in org ${orgId}:`, + "Loom import: failed to resolve storage access", writableResult.error, ); return { diff --git a/apps/web/lib/loom-browser-download.ts b/apps/web/lib/loom-browser-download.ts index 4a5eab7c998..973188949ba 100644 --- a/apps/web/lib/loom-browser-download.ts +++ b/apps/web/lib/loom-browser-download.ts @@ -1,8 +1,16 @@ +import { + extractLoomVideoId, + isValidLoomVideoId, + LOOM_ORIGIN, + normalizeLoomMediaUrl, +} from "@/lib/loom-url"; + type LoomUrlResponse = { url?: string; }; type LoomDownloadMode = "direct-download" | "browser-conversion"; +type LoomEndpoint = "raw-url" | "transcoded-url"; export type LoomBrowserDownloadResult = { success: boolean; @@ -22,27 +30,7 @@ type LoomOEmbedMetadata = { height?: number; }; -const LOOM_API_ROOT = "https://www.loom.com"; - -export function extractLoomVideoId(url: string): string | null { - try { - const parsed = new URL(url); - if (!parsed.hostname.includes("loom.com")) { - return null; - } - - const pathParts = parsed.pathname.split("/").filter(Boolean); - const id = pathParts[pathParts.length - 1] ?? null; - - if (!id || id.length < 10) { - return null; - } - - return id.split("?")[0] ?? null; - } catch { - return null; - } -} +export { extractLoomVideoId } from "@/lib/loom-url"; function createAnonId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { @@ -64,14 +52,17 @@ function isDirectMp4Url(url: string): boolean { async function fetchLoomEndpoint( videoId: string, - endpoint: string, + endpoint: LoomEndpoint, includeBody = true, ): Promise { try { + if (!isValidLoomVideoId(videoId)) return null; + const options: RequestInit = { method: "POST", mode: "cors", referrerPolicy: "no-referrer", + redirect: "error", }; if (includeBody) { options.headers = { @@ -86,8 +77,13 @@ async function fetchLoomEndpoint( }); } + const encodedVideoId = encodeURIComponent(videoId); + const endpointPath = endpoint === "raw-url" ? "raw-url" : "transcoded-url"; const response = await fetch( - `${LOOM_API_ROOT}/api/campaigns/sessions/${videoId}/${endpoint}`, + new URL( + `/api/campaigns/sessions/${encodedVideoId}/${endpointPath}`, + LOOM_ORIGIN, + ), options, ); @@ -101,7 +97,7 @@ async function fetchLoomEndpoint( } const data = JSON.parse(text) as LoomUrlResponse; - return data.url ?? null; + return data.url ? normalizeLoomMediaUrl(data.url) : null; } catch { return null; } @@ -109,7 +105,7 @@ async function fetchLoomEndpoint( async function fetchVideoName(videoId: string): Promise { try { - const response = await fetch(`${LOOM_API_ROOT}/graphql`, { + const response = await fetch(`${LOOM_ORIGIN}/graphql`, { method: "POST", mode: "cors", referrerPolicy: "no-referrer", @@ -144,14 +140,18 @@ async function fetchLoomOEmbed( videoId: string, ): Promise { try { - const response = await fetch( - `${LOOM_API_ROOT}/v1/oembed?url=https://www.loom.com/share/${videoId}`, - { - mode: "cors", - referrerPolicy: "no-referrer", - headers: { Accept: "application/json" }, - }, + const shareUrl = new URL( + `/share/${encodeURIComponent(videoId)}`, + LOOM_ORIGIN, ); + const oembedUrl = new URL("/v1/oembed", LOOM_ORIGIN); + oembedUrl.searchParams.set("url", shareUrl.toString()); + const response = await fetch(oembedUrl, { + mode: "cors", + referrerPolicy: "no-referrer", + redirect: "error", + headers: { Accept: "application/json" }, + }); if (!response.ok) return null; const data = await response.json(); return { @@ -165,7 +165,10 @@ async function fetchLoomOEmbed( } async function getLoomDownloadUrl(loomVideoId: string): Promise { - const requestVariants: Array<{ endpoint: string; includeBody: boolean }> = [ + const requestVariants: Array<{ + endpoint: LoomEndpoint; + includeBody: boolean; + }> = [ { endpoint: "transcoded-url", includeBody: true }, { endpoint: "raw-url", includeBody: true }, { endpoint: "transcoded-url", includeBody: false }, diff --git a/apps/web/lib/loom-url.ts b/apps/web/lib/loom-url.ts new file mode 100644 index 00000000000..34065f7fa66 --- /dev/null +++ b/apps/web/lib/loom-url.ts @@ -0,0 +1,46 @@ +const LOOM_HOSTNAME = "loom.com"; +const LOOM_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{10,128}$/; + +export const LOOM_ORIGIN = "https://www.loom.com"; + +export function isValidLoomVideoId(value: string): boolean { + return LOOM_VIDEO_ID_PATTERN.test(value); +} + +function isLoomHostname(hostname: string): boolean { + const normalized = hostname.toLowerCase().replace(/\.$/, ""); + return ( + normalized === LOOM_HOSTNAME || normalized.endsWith(`.${LOOM_HOSTNAME}`) + ); +} + +function isTrustedLoomUrl(url: URL): boolean { + return ( + url.protocol === "https:" && + (url.port === "" || url.port === "443") && + url.username === "" && + url.password === "" && + isLoomHostname(url.hostname) + ); +} + +export function extractLoomVideoId(value: string): string | null { + try { + const url = new URL(value); + if (!isTrustedLoomUrl(url)) return null; + + const id = url.pathname.split("/").filter(Boolean).at(-1); + return id && isValidLoomVideoId(id) ? id : null; + } catch { + return null; + } +} + +export function normalizeLoomMediaUrl(value: string): string | null { + try { + const url = new URL(value); + return isTrustedLoomUrl(url) ? url.toString() : null; + } catch { + return null; + } +} diff --git a/apps/web/workflows/import-loom-video.ts b/apps/web/workflows/import-loom-video.ts index f68417e8a5d..def06d489a5 100644 --- a/apps/web/workflows/import-loom-video.ts +++ b/apps/web/workflows/import-loom-video.ts @@ -7,6 +7,11 @@ import { Video } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { Effect } from "effect"; import { FatalError } from "workflow"; +import { + isValidLoomVideoId, + LOOM_ORIGIN, + normalizeLoomMediaUrl, +} from "@/lib/loom-url"; import { runWorkflowPromise } from "@/lib/workflow-runtime"; interface ImportLoomPayload { @@ -46,11 +51,13 @@ function isGoogleDriveResumableUrl(url: string): boolean { async function fetchLoomCdnUrl( videoId: string, - endpoint: string, + endpoint: "raw-url" | "transcoded-url", includeBody: boolean, ): Promise { try { - const options: RequestInit = { method: "POST" }; + if (!isValidLoomVideoId(videoId)) return null; + + const options: RequestInit = { method: "POST", redirect: "error" }; if (includeBody) { options.headers = { "Content-Type": "application/json", @@ -64,8 +71,13 @@ async function fetchLoomCdnUrl( }); } + const encodedVideoId = encodeURIComponent(videoId); + const endpointPath = endpoint === "raw-url" ? "raw-url" : "transcoded-url"; const response = await fetch( - `https://www.loom.com/api/campaigns/sessions/${videoId}/${endpoint}`, + new URL( + `/api/campaigns/sessions/${encodedVideoId}/${endpointPath}`, + LOOM_ORIGIN, + ), options, ); @@ -75,14 +87,17 @@ async function fetchLoomCdnUrl( if (!text.trim()) return null; const data = JSON.parse(text) as { url?: string }; - return data.url ?? null; + return data.url ? normalizeLoomMediaUrl(data.url) : null; } catch { return null; } } async function fetchFreshLoomDownloadUrl(loomVideoId: string): Promise { - const requestVariants: Array<{ endpoint: string; includeBody: boolean }> = [ + const requestVariants: Array<{ + endpoint: "raw-url" | "transcoded-url"; + includeBody: boolean; + }> = [ { endpoint: "transcoded-url", includeBody: true }, { endpoint: "raw-url", includeBody: true }, { endpoint: "transcoded-url", includeBody: false }, From 4233b70bc48e6948c44702d2121b989df81bcde6 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:03:07 +0100 Subject: [PATCH 12/31] ci: harden desktop validation --- .github/workflows/ci.yml | 57 ++++++++++++++++++++++------------------ scripts/setup.js | 52 ++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec0d3aa4d95..119a2ac6381 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,8 @@ name: "CI" + +permissions: + contents: read + on: push: branches: @@ -26,7 +30,7 @@ jobs: uses: actions/checkout@v4 - name: Check for changes - uses: dorny/paths-filter@v2 + uses: dorny/paths-filter@v3 id: filter with: filters: | @@ -44,7 +48,7 @@ jobs: - name: Output branch name id: branch run: | - echo "branch=${{ env.BRANCH_NAME }}" >> $GITHUB_OUTPUT + echo "branch=${{ env.BRANCH_NAME }}" >> "$GITHUB_OUTPUT" typecheck: name: Typecheck @@ -144,24 +148,22 @@ jobs: - name: Create .env file in root run: | - echo "VITE_ENVIRONMENT=production" >> .env - echo "CAP_DESKTOP_SENTRY_URL=https://6a3b6a09e6ae976c2ad6fff710e88748@o4506859771527168.ingest.us.sentry.io/4508330917101568" >> .env - echo "NEXT_PUBLIC_WEB_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}" >> .env - echo 'NEXTAUTH_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' >> .env - echo 'VITE_OPENPANEL_CLIENT_ID=${{ secrets.VITE_OPENPANEL_CLIENT_ID }}' >> .env - echo 'VITE_OPENPANEL_API_URL=${{ secrets.VITE_OPENPANEL_API_URL }}' >> .env - echo 'VITE_SERVER_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' >> .env - - cat .env >> $GITHUB_ENV + { + echo "VITE_ENVIRONMENT=production" + echo "CAP_DESKTOP_SENTRY_URL=https://6a3b6a09e6ae976c2ad6fff710e88748@o4506859771527168.ingest.us.sentry.io/4508330917101568" + echo "NEXT_PUBLIC_WEB_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}" + echo 'NEXTAUTH_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' + echo 'VITE_OPENPANEL_CLIENT_ID=${{ secrets.VITE_OPENPANEL_CLIENT_ID }}' + echo 'VITE_OPENPANEL_API_URL=${{ secrets.VITE_OPENPANEL_API_URL }}' + echo 'VITE_SERVER_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' + } >> .env + + cat .env >> "$GITHUB_ENV" - name: Run setup shell: bash run: node scripts/setup.js - - name: Build desktop binaries - shell: bash - run: ./scripts/build-desktop-binaries.sh ${{ matrix.settings.target }} - - name: Run Clippy run: cargo clippy --workspace --all-features --locked -- -D warnings @@ -196,6 +198,10 @@ jobs: env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + # CI validates release-mode compilation and packaging without paying the + # production profile's full-LTO cost on every pull request. + CARGO_PROFILE_RELEASE_LTO: "false" + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16" steps: - name: Checkout repository uses: actions/checkout@v4 @@ -213,20 +219,19 @@ jobs: - name: Create .env file in root run: | - echo "VITE_ENVIRONMENT=production" >> .env - echo "CAP_DESKTOP_SENTRY_URL=https://6a3b6a09e6ae976c2ad6fff710e88748@o4506859771527168.ingest.us.sentry.io/4508330917101568" >> .env - echo "NEXT_PUBLIC_WEB_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}" >> .env - echo 'NEXTAUTH_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' >> .env - echo 'VITE_OPENPANEL_CLIENT_ID=${{ secrets.VITE_OPENPANEL_CLIENT_ID }}' >> .env - echo 'VITE_OPENPANEL_API_URL=${{ secrets.VITE_OPENPANEL_API_URL }}' >> .env - echo 'VITE_SERVER_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' >> .env + { + echo "VITE_ENVIRONMENT=production" + echo "CAP_DESKTOP_SENTRY_URL=https://6a3b6a09e6ae976c2ad6fff710e88748@o4506859771527168.ingest.us.sentry.io/4508330917101568" + echo "NEXT_PUBLIC_WEB_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}" + echo 'NEXTAUTH_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' + echo 'VITE_OPENPANEL_CLIENT_ID=${{ secrets.VITE_OPENPANEL_CLIENT_ID }}' + echo 'VITE_OPENPANEL_API_URL=${{ secrets.VITE_OPENPANEL_API_URL }}' + echo 'VITE_SERVER_URL=${{ secrets.NEXT_PUBLIC_WEB_URL }}' + } >> .env - name: Copy .env to apps/desktop run: cp .env apps/desktop/.env - - name: Output .env file - run: cat apps/desktop/.env - - name: Run cap-setup shell: bash run: pnpm -w cap-setup @@ -283,7 +288,7 @@ jobs: save-cache: true - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 24 diff --git a/scripts/setup.js b/scripts/setup.js index feec22ca063..f1f570238ba 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -28,6 +28,26 @@ function cargoConfigPath(value) { return value.replaceAll("\\", "/"); } +async function fetchBytes(url, attempts = 3) { + let lastError; + + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + const response = await fetch(url); + if (!response.ok) + throw new Error(`Download failed with HTTP ${response.status}`); + return Buffer.from(await response.arrayBuffer()); + } catch (error) { + lastError = error; + if (attempt === attempts) break; + console.warn(`Download attempt ${attempt} failed; retrying...`); + await new Promise((resolve) => setTimeout(resolve, attempt * 2_000)); + } + } + + throw lastError; +} + async function main() { await fs.mkdir(targetDir, { recursive: true }); @@ -66,10 +86,10 @@ async function main() { if (!(await fileExists(nativeDepsTarPath))) { console.log(`Downloading ${nativeDepsTar}`); - const nativeDepsBytes = await fetch(`${NATIVE_DEPS_URL}/${nativeDepsTar}`) - .then((r) => r.blob()) - .then((b) => b.arrayBuffer()); - await fs.writeFile(nativeDepsTarPath, Buffer.from(nativeDepsBytes)); + const nativeDepsBytes = await fetchBytes( + `${NATIVE_DEPS_URL}/${nativeDepsTar}`, + ); + await fs.writeFile(nativeDepsTarPath, nativeDepsBytes); console.log("Downloaded native deps"); downloadedNativeDeps = true; } else console.log(`Using cached ${nativeDepsTar}`); @@ -143,10 +163,8 @@ async function main() { const ffmpegZip = `ffmpeg-${FFMPEG_VERSION}.zip`; const ffmpegZipPath = path.join(targetDir, ffmpegZip); if (!(await fileExists(ffmpegZipPath))) { - const ffmpegZipBytes = await fetch(FFMPEG_ZIP_URL) - .then((r) => r.blob()) - .then((b) => b.arrayBuffer()); - await fs.writeFile(ffmpegZipPath, Buffer.from(ffmpegZipBytes)); + const ffmpegZipBytes = await fetchBytes(FFMPEG_ZIP_URL); + await fs.writeFile(ffmpegZipPath, ffmpegZipBytes); console.log(`Downloaded ${ffmpegZip}`); downloadedFfmpeg = true; } else console.log(`Using cached ${ffmpegZip}`); @@ -237,10 +255,8 @@ async function main() { let downloadedNativeDeps = false; if (!(await fileExists(nativeDepsTarPath))) { console.log(`Downloading ${nativeDepsTar}`); - const bytes = await fetch(`${NATIVE_DEPS_URL}/${nativeDepsTar}`) - .then((r) => r.blob()) - .then((b) => b.arrayBuffer()); - await fs.writeFile(nativeDepsTarPath, Buffer.from(bytes)); + const bytes = await fetchBytes(`${NATIVE_DEPS_URL}/${nativeDepsTar}`); + await fs.writeFile(nativeDepsTarPath, bytes); console.log("Downloaded native deps"); downloadedNativeDeps = true; } else console.log(`Using cached ${nativeDepsTar}`); @@ -385,10 +401,8 @@ async function setupMacOSOnnxRuntime() { if (!(await fileExists(archivePath))) { console.log(`Downloading ${asset.name}`); - const bytes = await fetch(url) - .then((r) => r.blob()) - .then((b) => b.arrayBuffer()); - await fs.writeFile(archivePath, Buffer.from(bytes)); + const bytes = await fetchBytes(url); + await fs.writeFile(archivePath, bytes); console.log(`Downloaded ${asset.name}`); } else console.log(`Using cached ${asset.name}`); @@ -439,10 +453,8 @@ async function setupWindowsOnnxRuntime() { if (!(await fileExists(archivePath))) { console.log(`Downloading ${asset.name}`); - const bytes = await fetch(url) - .then((r) => r.blob()) - .then((b) => b.arrayBuffer()); - await fs.writeFile(archivePath, Buffer.from(bytes)); + const bytes = await fetchBytes(url); + await fs.writeFile(archivePath, bytes); console.log(`Downloaded ${asset.name}`); } else console.log(`Using cached ${asset.name}`); From 9db0c2b5fbb11fccd62fbbb0168bcc1a582dd62f Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:09:22 +0100 Subject: [PATCH 13/31] fix: revalidate draft before asset uploads --- .github/workflows/desktop-release.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index ea8d8d50d32..bb79cb05d44 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -307,6 +307,23 @@ jobs: Get-ChildItem $dist -Filter *.blockmap | Remove-Item -Force + - name: Verify release is still a draft + uses: actions/github-script@v7 + env: + RELEASE_TAG: ${{ needs.draft.outputs.tag_name }} + with: + script: | + const release = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: process.env.RELEASE_TAG, + }); + if (!release.data.draft) { + throw new Error( + `Release '${process.env.RELEASE_TAG}' was published while assets were building; refusing to mutate it`, + ); + } + - name: Upload release assets uses: softprops/action-gh-release@v2 with: From 2aebf16a8eee4b81c39f91d8781e6487bcd225c8 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:30:12 +0100 Subject: [PATCH 14/31] fix: stage release assets before promotion --- .github/workflows/desktop-release.yml | 261 ++++++++++++++---- package.json | 3 +- pnpm-lock.yaml | 3 + scripts/prepare-electron-release-assets.mjs | 139 ++++++++++ .../prepare-electron-release-assets.test.mjs | 98 +++++++ 5 files changed, 447 insertions(+), 57 deletions(-) create mode 100644 scripts/prepare-electron-release-assets.mjs create mode 100644 scripts/prepare-electron-release-assets.test.mjs diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index bb79cb05d44..efb5328ca31 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -34,20 +34,43 @@ jobs: runs-on: ubuntu-latest outputs: tag_name: ${{ steps.release.outputs.tag_name }} - release_url: ${{ steps.release.outputs.release_url }} + staging_tag: ${{ steps.release.outputs.staging_tag }} + staging_release_id: ${{ steps.release.outputs.staging_release_id }} permissions: contents: write steps: - name: Prepare immutable release tag and draft id: release uses: actions/github-script@v7 + env: + RELEASE_CHANNEL: ${{ inputs.channel }} + RELEASE_SHA: ${{ inputs.sha }} + RELEASE_VERSION: ${{ inputs.version }} with: script: | - const tag = "${{ inputs.channel }}" === "nightly" - ? `cap-nightly-v${{ inputs.version }}` - : `cap-v${{ inputs.version }}`; - const sha = "${{ inputs.sha }}"; + const channel = process.env.RELEASE_CHANNEL; + const sha = process.env.RELEASE_SHA; + const version = process.env.RELEASE_VERSION; + if (!/^[0-9a-f]{40}$/.test(sha)) + throw new Error(`Invalid release SHA '${sha}'`); + const versionPattern = channel === "nightly" + ? /^\d+\.\d+\.\d+-nightly\.\d{12}$/ + : channel === "stable" + ? /^\d+\.\d+\.\d+$/ + : null; + if (!versionPattern || !versionPattern.test(version)) + throw new Error(`Invalid ${channel} release version '${version}'`); + + const tag = channel === "nightly" + ? `cap-nightly-v${version}` + : `cap-v${version}`; + const stagingTag = `${tag}-staging`; const tagRef = `tags/${tag}`; + const releases = await github.paginate(github.rest.repos.listReleases, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }); let tagExists = true; try { @@ -66,12 +89,13 @@ jobs: else throw error; } - let release; - try { - release = (await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag })).data; - if (!release.draft) throw new Error(`Release '${tag}' is already published: ${release.html_url}`); - } catch (error) { - if (error.status !== 404) throw error; + const existingRelease = releases.find( + (release) => release.tag_name === tag, + ); + if (existingRelease) { + throw new Error( + `Release '${tag}' already exists (${existingRelease.draft ? "draft" : "published"}): ${existingRelease.html_url}`, + ); } if (!tagExists) @@ -82,34 +106,65 @@ jobs: sha, }); - if (release) { - release = (await github.rest.repos.updateRelease({ + let stagingRelease = releases.find( + (release) => release.tag_name === stagingTag, + ); + + if (stagingRelease) { + if (!stagingRelease.draft) { + throw new Error( + `Staging release '${stagingTag}' is published; refusing to mutate it`, + ); + } + if (stagingRelease.target_commitish !== sha) { + throw new Error( + `Staging release '${stagingTag}' targets ${stagingRelease.target_commitish}; expected ${sha}`, + ); + } + for (const asset of await github.paginate( + github.rest.repos.listReleaseAssets, + { + owner: context.repo.owner, + repo: context.repo.repo, + release_id: stagingRelease.id, + per_page: 100, + }, + )) { + await github.rest.repos.deleteReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: asset.id, + }); + } + stagingRelease = (await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, - release_id: release.id, + release_id: stagingRelease.id, target_commitish: sha, - name: "${{ inputs.version }}", - prerelease: "${{ inputs.channel }}" === "nightly", + name: version, + draft: true, + prerelease: channel === "nightly", })).data; } else { - release = (await github.rest.repos.createRelease({ + stagingRelease = (await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, - tag_name: tag, + tag_name: stagingTag, target_commitish: sha, - name: "${{ inputs.version }}", + name: version, draft: true, - prerelease: "${{ inputs.channel }}" === "nightly", + prerelease: channel === "nightly", generate_release_notes: true, })).data; } core.setOutput("tag_name", tag); - core.setOutput("release_url", release.html_url); + core.setOutput("staging_tag", stagingTag); + core.setOutput("staging_release_id", stagingRelease.id); build: needs: draft permissions: - contents: write + contents: read actions: read strategy: fail-fast: false @@ -307,30 +362,13 @@ jobs: Get-ChildItem $dist -Filter *.blockmap | Remove-Item -Force - - name: Verify release is still a draft - uses: actions/github-script@v7 - env: - RELEASE_TAG: ${{ needs.draft.outputs.tag_name }} - with: - script: | - const release = await github.rest.repos.getReleaseByTag({ - owner: context.repo.owner, - repo: context.repo.repo, - tag: process.env.RELEASE_TAG, - }); - if (!release.data.draft) { - throw new Error( - `Release '${process.env.RELEASE_TAG}' was published while assets were building; refusing to mutate it`, - ); - } - - - name: Upload release assets - uses: softprops/action-gh-release@v2 + - name: Stage release assets + uses: actions/upload-artifact@v4 with: - tag_name: ${{ needs.draft.outputs.tag_name }} - draft: true - prerelease: ${{ inputs.channel == 'nightly' }} - files: | + name: release-assets-${{ matrix.settings.target }} + if-no-files-found: error + retention-days: 1 + path: | apps/desktop/dist-electron/*.dmg apps/desktop/dist-electron/*.zip apps/desktop/dist-electron/*.exe @@ -357,25 +395,136 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + actions: read id-token: write steps: - - name: Publish nightly release - if: ${{ inputs.auto_publish }} + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha }} + + - uses: ./.github/actions/setup-js + + - name: Download staged platform assets + uses: actions/download-artifact@v4 + with: + pattern: release-assets-* + path: staged-release-assets + + - name: Assemble release assets + run: node scripts/prepare-electron-release-assets.mjs staged-release-assets release-assets + + - name: Upload complete staging release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.draft.outputs.staging_tag }} + draft: true + prerelease: ${{ inputs.channel == 'nightly' }} + fail_on_unmatched_files: true + files: release-assets/* + + - name: Promote complete staging release + id: promote uses: actions/github-script@v7 + env: + AUTO_PUBLISH: ${{ inputs.auto_publish }} + FINAL_TAG: ${{ needs.draft.outputs.tag_name }} + RELEASE_CHANNEL: ${{ inputs.channel }} + RELEASE_SHA: ${{ inputs.sha }} + RELEASE_VERSION: ${{ inputs.version }} + STAGING_RELEASE_ID: ${{ needs.draft.outputs.staging_release_id }} + STAGING_TAG: ${{ needs.draft.outputs.staging_tag }} with: script: | - const release = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag: "${{ needs.draft.outputs.tag_name }}" }); - await github.rest.repos.updateRelease({ + const path = require("node:path"); + const releaseId = Number(process.env.STAGING_RELEASE_ID); + const stagingRelease = (await github.rest.repos.getRelease({ owner: context.repo.owner, repo: context.repo.repo, - release_id: release.data.id, - draft: false, - prerelease: "${{ inputs.channel }}" === "nightly", - }); + release_id: releaseId, + })).data; + if (!stagingRelease.draft || stagingRelease.tag_name !== process.env.STAGING_TAG) { + throw new Error( + `Staging release '${process.env.STAGING_TAG}' is no longer an unpublished draft`, + ); + } + + const globber = await glob.create("release-assets/*"); + const expectedAssets = []; + for await (const file of globber.globGenerator()) { + expectedAssets.push(path.basename(file)); + } + expectedAssets.sort(); + const uploadedAssetRecords = await github.paginate( + github.rest.repos.listReleaseAssets, + { + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + per_page: 100, + }, + ); + const incompleteAsset = uploadedAssetRecords.find( + (asset) => asset.state !== "uploaded" || asset.size <= 0, + ); + if (incompleteAsset) { + throw new Error( + `Staging asset '${incompleteAsset.name}' is not completely uploaded`, + ); + } + const uploadedAssets = uploadedAssetRecords + .map((asset) => asset.name) + .sort(); + if (JSON.stringify(uploadedAssets) !== JSON.stringify(expectedAssets)) { + throw new Error( + `Staging release assets do not match the assembled set: expected ${expectedAssets.join(", ")}; got ${uploadedAssets.join(", ")}`, + ); + } + + const existingRelease = (await github.paginate( + github.rest.repos.listReleases, + { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }, + )).find((release) => release.tag_name === process.env.FINAL_TAG); + if (existingRelease) { + throw new Error( + `Final release '${process.env.FINAL_TAG}' already exists: ${existingRelease.html_url}`, + ); + } + + const release = (await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + tag_name: process.env.FINAL_TAG, + target_commitish: process.env.RELEASE_SHA, + name: process.env.RELEASE_VERSION, + draft: process.env.AUTO_PUBLISH !== "true", + prerelease: process.env.RELEASE_CHANNEL === "nightly", + })).data; + core.setOutput("release_url", release.html_url); + + try { + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${process.env.STAGING_TAG}`, + }); + } catch (error) { + core.warning( + `Final release is complete, but staging tag cleanup failed: ${error.message}`, + ); + } - name: Notify Discord that the stable draft is ready if: ${{ inputs.channel == 'stable' && inputs.interactionId != '' }} uses: actions/github-script@v7 + env: + INTERACTION_ID: ${{ inputs.interactionId }} + RELEASE_URL: ${{ steps.promote.outputs.release_url }} + RELEASE_VERSION: ${{ inputs.version }} with: script: | const token = await core.getIDToken("cap-discord-bot"); @@ -384,9 +533,9 @@ jobs: headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ type: "release-done", - interactionId: "${{ inputs.interactionId }}", - version: "${{ inputs.version }}", - releaseUrl: "${{ needs.draft.outputs.release_url }}", + interactionId: process.env.INTERACTION_ID, + version: process.env.RELEASE_VERSION, + releaseUrl: process.env.RELEASE_URL, }), }); if (!response.ok) throw new Error(await response.text()); diff --git a/package.json b/package.json index b2e7b1b5400..80a1cadde50 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "mysql2": "^3.15.2", "tsdown": "^0.15.6", "turbo": "^2.3.4", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "yaml": "^2.9.0" }, "pnpm": { "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a34e0357ce3..beb10d9ebbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.9.3 + yaml: + specifier: ^2.9.0 + version: 2.9.0 apps/chrome-extension: dependencies: diff --git a/scripts/prepare-electron-release-assets.mjs b/scripts/prepare-electron-release-assets.mjs new file mode 100644 index 00000000000..9d3a20ab9c3 --- /dev/null +++ b/scripts/prepare-electron-release-assets.mjs @@ -0,0 +1,139 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { pathToFileURL } from "node:url"; +import { parse, stringify } from "yaml"; + +const TARGETS = [ + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", +]; +const RELEASE_ASSET_PATTERN = /\.(?:deb|dmg|exe|yml|zip)$/i; +const MAC_MANIFEST_PATTERN = /-mac\.yml$/i; +const REQUIRED_PATTERNS = new Map([ + ["x86_64-apple-darwin", [/\.dmg$/i, /\.zip$/i, MAC_MANIFEST_PATTERN]], + ["aarch64-apple-darwin", [/\.dmg$/i, /\.zip$/i, MAC_MANIFEST_PATTERN]], + ["x86_64-pc-windows-msvc", [/\.exe$/i, /\.yml$/i]], + ["x86_64-unknown-linux-gnu", [/\.deb$/i, /\.yml$/i]], +]); + +function parseManifest(contents, source) { + const manifest = parse(contents); + if (!manifest || typeof manifest !== "object") + throw new Error(`${source} is not a YAML object`); + if (typeof manifest.version !== "string" || !manifest.version) + throw new Error(`${source} has no version`); + if (!Array.isArray(manifest.files) || manifest.files.length === 0) + throw new Error(`${source} has no update files`); + for (const file of manifest.files) { + if ( + !file || + typeof file !== "object" || + typeof file.url !== "string" || + typeof file.sha512 !== "string" + ) + throw new Error(`${source} contains an invalid update file`); + } + return manifest; +} + +export function mergeMacManifests(x64Contents, arm64Contents) { + const x64 = parseManifest(x64Contents, "x64 macOS manifest"); + const arm64 = parseManifest(arm64Contents, "arm64 macOS manifest"); + if (x64.version !== arm64.version) + throw new Error( + `macOS manifest versions differ: ${x64.version} vs ${arm64.version}`, + ); + + const files = [...x64.files, ...arm64.files]; + const urls = new Set(files.map((file) => file.url)); + if (urls.size !== files.length) + throw new Error("macOS manifests contain duplicate update URLs"); + if (![...urls].some((url) => url.includes("arm64"))) + throw new Error("arm64 macOS manifest has no arm64 artifact"); + if (![...urls].some((url) => !url.includes("arm64"))) + throw new Error("x64 macOS manifest has no x64 artifact"); + + return stringify( + { + ...x64, + files, + }, + { lineWidth: 0 }, + ); +} + +export async function prepareReleaseAssets(inputDir, outputDir) { + await fs.rm(outputDir, { recursive: true, force: true }); + await fs.mkdir(outputDir, { recursive: true }); + + const copiedNames = new Set(); + const macManifests = new Map(); + for (const target of TARGETS) { + const artifactDir = path.join(inputDir, `release-assets-${target}`); + const entries = await fs.readdir(artifactDir, { withFileTypes: true }); + const assets = entries.filter( + (entry) => entry.isFile() && RELEASE_ASSET_PATTERN.test(entry.name), + ); + if (assets.length === 0) + throw new Error(`No release assets found for ${target}`); + for (const requiredPattern of REQUIRED_PATTERNS.get(target) ?? []) { + if (!assets.some((asset) => requiredPattern.test(asset.name))) + throw new Error( + `Release assets for ${target} are missing ${requiredPattern}`, + ); + } + + for (const asset of assets) { + const source = path.join(artifactDir, asset.name); + if ((await fs.stat(source)).size === 0) + throw new Error(`Release asset is empty: ${asset.name}`); + if ( + target.endsWith("apple-darwin") && + MAC_MANIFEST_PATTERN.test(asset.name) + ) { + macManifests.set(target, { name: asset.name, source }); + continue; + } + if (copiedNames.has(asset.name)) + throw new Error(`Duplicate release asset name: ${asset.name}`); + copiedNames.add(asset.name); + await fs.copyFile(source, path.join(outputDir, asset.name)); + } + } + + const x64Manifest = macManifests.get("x86_64-apple-darwin"); + const arm64Manifest = macManifests.get("aarch64-apple-darwin"); + if (!x64Manifest || !arm64Manifest) + throw new Error("Both macOS updater manifests are required"); + if (x64Manifest.name !== arm64Manifest.name) + throw new Error( + `macOS manifest names differ: ${x64Manifest.name} vs ${arm64Manifest.name}`, + ); + + const mergedManifest = mergeMacManifests( + await fs.readFile(x64Manifest.source, "utf8"), + await fs.readFile(arm64Manifest.source, "utf8"), + ); + await fs.writeFile( + path.join(outputDir, x64Manifest.name), + mergedManifest, + "utf8", + ); + + return (await fs.readdir(outputDir)).sort(); +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + const [, , inputDir, outputDir] = process.argv; + if (!inputDir || !outputDir) + throw new Error( + "Usage: node scripts/prepare-electron-release-assets.mjs ", + ); + const assets = await prepareReleaseAssets(inputDir, outputDir); + console.log(`Prepared ${assets.length} release assets: ${assets.join(", ")}`); +} diff --git a/scripts/prepare-electron-release-assets.test.mjs b/scripts/prepare-electron-release-assets.test.mjs new file mode 100644 index 00000000000..16c26f2098d --- /dev/null +++ b/scripts/prepare-electron-release-assets.test.mjs @@ -0,0 +1,98 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import * as path from "node:path"; +import { afterEach, test } from "node:test"; +import { parse, stringify } from "yaml"; +import { + mergeMacManifests, + prepareReleaseAssets, +} from "./prepare-electron-release-assets.mjs"; + +const tempDirs = []; + +afterEach(async () => { + await Promise.all( + tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })), + ); +}); + +function manifest(url, sha512) { + return stringify({ + version: "1.2.3", + files: [{ url, sha512, size: 42 }], + path: url, + sha512, + releaseDate: "2026-08-13T00:00:00.000Z", + }); +} + +test("merges macOS updater files while retaining x64 legacy metadata", () => { + const merged = parse( + mergeMacManifests( + manifest("Cap-1.2.3-mac.zip", "x64-hash"), + manifest("Cap-1.2.3-arm64-mac.zip", "arm64-hash"), + ), + ); + + assert.equal(merged.path, "Cap-1.2.3-mac.zip"); + assert.equal(merged.sha512, "x64-hash"); + assert.deepEqual( + merged.files.map((file) => file.url), + ["Cap-1.2.3-mac.zip", "Cap-1.2.3-arm64-mac.zip"], + ); +}); + +test("assembles unique platform assets and one merged macOS manifest", async () => { + const root = await mkdtemp(path.join(tmpdir(), "cap-release-assets-")); + tempDirs.push(root); + const input = path.join(root, "input"); + const output = path.join(root, "output"); + const fixtures = { + "x86_64-apple-darwin": { + "Cap-1.2.3.dmg": "dmg-x64", + "Cap-1.2.3-mac.zip": "zip-x64", + "latest-mac.yml": manifest("Cap-1.2.3-mac.zip", "x64-hash"), + }, + "aarch64-apple-darwin": { + "Cap-1.2.3-arm64.dmg": "dmg-arm64", + "Cap-1.2.3-arm64-mac.zip": "zip-arm64", + "latest-mac.yml": manifest("Cap-1.2.3-arm64-mac.zip", "arm64-hash"), + }, + "x86_64-pc-windows-msvc": { + "Cap-Setup-1.2.3.exe": "exe", + "latest.yml": "version: 1.2.3\n", + }, + "x86_64-unknown-linux-gnu": { + "Cap-1.2.3.deb": "deb", + "latest-linux.yml": "version: 1.2.3\n", + }, + }; + + for (const [target, files] of Object.entries(fixtures)) { + const dir = path.join(input, `release-assets-${target}`); + await mkdir(dir, { recursive: true }); + await Promise.all( + Object.entries(files).map(([name, contents]) => + writeFile(path.join(dir, name), contents), + ), + ); + } + + const assets = await prepareReleaseAssets(input, output); + assert.deepEqual(assets, [ + "Cap-1.2.3-arm64-mac.zip", + "Cap-1.2.3-arm64.dmg", + "Cap-1.2.3-mac.zip", + "Cap-1.2.3.deb", + "Cap-1.2.3.dmg", + "Cap-Setup-1.2.3.exe", + "latest-linux.yml", + "latest-mac.yml", + "latest.yml", + ]); + const merged = parse( + await readFile(path.join(output, "latest-mac.yml"), "utf8"), + ); + assert.equal(merged.files.length, 2); +}); From 13a1d43d83feac03dc19aa2791adb51b64c33a87 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:32:45 +0100 Subject: [PATCH 15/31] style: organize release asset imports --- scripts/prepare-electron-release-assets.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-electron-release-assets.test.mjs b/scripts/prepare-electron-release-assets.test.mjs index 16c26f2098d..6e1ed8b8601 100644 --- a/scripts/prepare-electron-release-assets.test.mjs +++ b/scripts/prepare-electron-release-assets.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import * as path from "node:path"; import { afterEach, test } from "node:test"; From 5736e494b0fd07e697baba075915aef5182769de Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:37:03 +0100 Subject: [PATCH 16/31] fix: create release tag on promotion --- .github/workflows/desktop-release.yml | 39 ++++++++++++++++++--------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index efb5328ca31..f2022d8ec75 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -39,7 +39,7 @@ jobs: permissions: contents: write steps: - - name: Prepare immutable release tag and draft + - name: Prepare immutable release staging draft id: release uses: actions/github-script@v7 env: @@ -72,7 +72,6 @@ jobs: per_page: 100, }); - let tagExists = true; try { const existingTag = await github.rest.git.getRef({ owner: context.repo.owner, @@ -85,8 +84,7 @@ jobs: ); } } catch (error) { - if (error.status === 404) tagExists = false; - else throw error; + if (error.status !== 404) throw error; } const existingRelease = releases.find( @@ -98,14 +96,6 @@ jobs: ); } - if (!tagExists) - await github.rest.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `refs/${tagRef}`, - sha, - }); - let stagingRelease = releases.find( (release) => release.tag_name === stagingTag, ); @@ -494,6 +484,21 @@ jobs: ); } + try { + const existingTag = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${process.env.FINAL_TAG}`, + }); + if (existingTag.data.object.sha !== process.env.RELEASE_SHA) { + throw new Error( + `Final tag '${process.env.FINAL_TAG}' points to ${existingTag.data.object.sha}; expected ${process.env.RELEASE_SHA}`, + ); + } + } catch (error) { + if (error.status !== 404) throw error; + } + const release = (await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, @@ -504,6 +509,16 @@ jobs: draft: process.env.AUTO_PUBLISH !== "true", prerelease: process.env.RELEASE_CHANNEL === "nightly", })).data; + const finalTag = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${process.env.FINAL_TAG}`, + }); + if (finalTag.data.object.sha !== process.env.RELEASE_SHA) { + throw new Error( + `Promoted tag '${process.env.FINAL_TAG}' points to ${finalTag.data.object.sha}; expected ${process.env.RELEASE_SHA}`, + ); + } core.setOutput("release_url", release.html_url); try { From 98acc87a4d651a9cacf80d524fc73808cfb112fd Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:29:26 +0100 Subject: [PATCH 17/31] perf: shrink Electron package footprint --- apps/desktop/electron-builder.yml | 13 +++++++++ scripts/build-desktop-binaries.mjs | 45 +++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml index 22ae072808d..babb1ef7718 100644 --- a/apps/desktop/electron-builder.yml +++ b/apps/desktop/electron-builder.yml @@ -2,6 +2,9 @@ appId: so.cap.desktop productName: Cap asar: true generateUpdatesFilesForAllChannels: true +electronLanguages: + - en-US + - en-GB extraMetadata: main: .electron-dist/main.cjs directories: @@ -9,11 +12,21 @@ directories: files: - .electron-dist/** - .output/public/** + - '!.output/public/_server/**' + - '!.output/public/**/*.br' + - '!.output/public/**/*.gz' - package.json - '!node_modules/**/*' extraResources: - from: src-backend/assets to: assets + filter: + - '**/*' + - '!dmg-background.png' + - '!nsis-header.bmp' + - '!nsis-sidebar.bmp' + - '!wix-banner.bmp' + - '!wix-dialog.bmp' - from: src-backend/icons to: tray-icons filter: diff --git a/scripts/build-desktop-binaries.mjs b/scripts/build-desktop-binaries.mjs index cbaccb36370..23a0de38670 100644 --- a/scripts/build-desktop-binaries.mjs +++ b/scripts/build-desktop-binaries.mjs @@ -61,6 +61,43 @@ async function isUpToDate(destPath, sourcePaths) { return true; } +async function unlinkIfExists(targetPath) { + await fs.unlink(targetPath).catch((error) => { + if (error.code !== "ENOENT") throw error; + }); +} + +async function stageBuiltBinaries(src, dests, target) { + await fs.copyFile(src, dests[0]); + console.log(`Copied ${src} -> ${dests[0]}`); + + for (const dest of dests.slice(1)) { + if (target.includes("windows")) { + await fs.copyFile(src, dest); + console.log(`Copied ${src} -> ${dest}`); + continue; + } + + await unlinkIfExists(dest); + const linkTarget = path.basename(dests[0]); + await fs.symlink(linkTarget, dest); + console.log(`Linked ${dest} -> ${linkTarget}`); + } +} + +async function canonicalizeDuplicateBinaries(dests, target) { + if (target.includes("windows") || dests.length < 2) return; + + const linkTarget = path.basename(dests[0]); + for (const dest of dests.slice(1)) { + const existingTarget = await fs.readlink(dest).catch(() => null); + if (existingTarget === linkTarget) continue; + await unlinkIfExists(dest); + await fs.symlink(linkTarget, dest); + console.log(`Linked ${dest} -> ${linkTarget}`); + } +} + async function main() { const target = process.argv[2] || process.env.RUST_TARGET_TRIPLE || detectHostTriple(); @@ -77,6 +114,8 @@ async function main() { path.join(repoRoot, "apps", "desktop", "src-backend"), path.join(repoRoot, "crates", "desktop-runtime"), path.join(repoRoot, "crates", "desktop-runtime-macros"), + path.join(repoRoot, "crates", "editor"), + path.join(repoRoot, "crates", "rendering"), path.join(repoRoot, "Cargo.lock"), ], }, @@ -144,6 +183,7 @@ async function buildSidecar(sidecar, target, ext) { ) ).every(Boolean) ) { + await canonicalizeDuplicateBinaries(dests, target); console.log( `${sidecar.destBinaries.join(", ")} desktop binaries up to date`, ); @@ -167,10 +207,7 @@ async function buildSidecar(sidecar, target, ext) { process.exit(1); } - for (const dest of dests) { - await fs.copyFile(src, dest); - console.log(`Copied ${src} -> ${dest}`); - } + await stageBuiltBinaries(src, dests, target); } main().catch((err) => { From 32d5b610c7b07b521cf9291658dcdd61e7a7aa6c Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:29:29 +0100 Subject: [PATCH 18/31] perf: reduce Electron host overhead --- apps/desktop/electron/backend.cjs | 5 +- apps/desktop/electron/main.cjs | 10 ++- apps/desktop/electron/native.cjs | 25 ++++-- apps/desktop/electron/preload.cjs | 23 ++++-- apps/desktop/electron/windows.cjs | 101 ++++++++++++++++++----- apps/desktop/src/electron/bridge.ts | 3 +- apps/desktop/src/electron/event.ts | 5 +- crates/desktop-runtime-macros/src/lib.rs | 1 + crates/desktop-runtime/src/lib.rs | 40 ++++++++- crates/desktop-runtime/src/protocol.rs | 1 + 10 files changed, 173 insertions(+), 41 deletions(-) diff --git a/apps/desktop/electron/backend.cjs b/apps/desktop/electron/backend.cjs index 18b6ecad292..6091ba6a6b9 100644 --- a/apps/desktop/electron/backend.cjs +++ b/apps/desktop/electron/backend.cjs @@ -142,7 +142,10 @@ class RustBackend { } const header = Buffer.allocUnsafe(4); header.writeUInt32BE(payload.length); - this.socket.write(Buffer.concat([header, payload])); + this.socket.cork(); + this.socket.write(header); + this.socket.write(payload); + this.socket.uncork(); return true; } diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 3c9420ec869..86d41cbd47f 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -186,6 +186,12 @@ async function start() { payload: request.payload ?? null, }); }); + ipcMain.on("cap:event-subscribe", (_event, request) => { + windowManager.subscribeToEvent(request.windowLabel, request.event); + }); + ipcMain.on("cap:event-unsubscribe", (_event, request) => { + windowManager.unsubscribeFromEvent(request.windowLabel, request.event); + }); ipcMain.on("cap:window-drop", (_event, request) => { windowManager.sendWindowEvent(request.windowLabel, { type: "dragDrop", @@ -193,8 +199,8 @@ async function start() { }); }); globalThis.__capMenuSelection = (id) => { - for (const window of windowManager.windows.values()) { - window.webContents.send("cap:event", { + for (const label of windowManager.windows.keys()) { + windowManager.sendEvent(label, { type: "event", event: `menu:${id}`, payload: null, diff --git a/apps/desktop/electron/native.cjs b/apps/desktop/electron/native.cjs index 52d5be56b52..d47371a897d 100644 --- a/apps/desktop/electron/native.cjs +++ b/apps/desktop/electron/native.cjs @@ -14,7 +14,6 @@ const { const fs = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); -const { autoUpdater } = require("electron-updater"); class NativeBridge { constructor(windowManager) { @@ -26,6 +25,13 @@ class NativeBridge { this.trayMode = "studio"; this.trayRecording = false; this.updateChannel = "stable"; + this.autoUpdater = null; + } + + getAutoUpdater() { + if (this.autoUpdater) return this.autoUpdater; + + const { autoUpdater } = require("electron-updater"); autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = true; autoUpdater.on("download-progress", (progress) => { @@ -45,6 +51,8 @@ class NativeBridge { payload: { version: info.version, installed: true }, }); }); + this.autoUpdater = autoUpdater; + return autoUpdater; } async invoke(windowLabel, operation, payload = {}) { @@ -466,17 +474,21 @@ class NativeBridge { configureUpdater(channel = "stable") { this.updateChannel = channel; - this.setUpdaterFeed(channel); - if (this.updateTimer) clearInterval(this.updateTimer); + if (this.updateTimer) { + clearInterval(this.updateTimer); + this.updateTimer = null; + } if (!app.isPackaged || channel !== "nightly") return; this.updateTimer = setInterval( () => void this.checkForUpdates(channel).catch(console.error), 2 * 60 * 60 * 1000, ); + this.updateTimer.unref?.(); } setUpdaterFeed(channel) { - if (!app.isPackaged) return; + if (!app.isPackaged) return null; + const autoUpdater = this.getAutoUpdater(); autoUpdater.channel = channel === "nightly" ? "nightly" : "latest"; const baseUrl = process.env.CAP_ELECTRON_UPDATE_URL; if (baseUrl) @@ -490,11 +502,12 @@ class NativeBridge { owner: "CapSoftware", repo: "Cap", }); + return autoUpdater; } async checkForUpdates(channel = this.updateChannel) { if (!app.isPackaged) return null; - this.setUpdaterFeed(channel); + const autoUpdater = this.setUpdaterFeed(channel); const result = await autoUpdater.checkForUpdates(); if (!result?.updateInfo || result.updateInfo.version === app.getVersion()) return null; @@ -511,7 +524,7 @@ class NativeBridge { async downloadAndInstall(channel = this.updateChannel) { const update = await this.checkForUpdates(channel); if (!update) throw new Error("No update available"); - await autoUpdater.downloadUpdate(); + await this.getAutoUpdater().downloadUpdate(); return { version: update.version, installed: true }; } } diff --git a/apps/desktop/electron/preload.cjs b/apps/desktop/electron/preload.cjs index 87db185fb16..0803c66f76e 100644 --- a/apps/desktop/electron/preload.cjs +++ b/apps/desktop/electron/preload.cjs @@ -17,11 +17,13 @@ const initialization = initArgument ? JSON.parse(Buffer.from(normalizedInitialization, "base64").toString("utf8")) : null; -const eventListeners = new Set(); +const eventListeners = new Map(); const channelListeners = new Set(); ipcRenderer.on("cap:event", (_event, message) => { - for (const listener of eventListeners) listener(message); + const listeners = eventListeners.get(message.event); + if (!listeners) return; + for (const listener of listeners) listener(message); }); ipcRenderer.on("cap:channel", (_event, message) => { for (const listener of channelListeners) listener(message); @@ -60,9 +62,20 @@ contextBridge.exposeInMainWorld("capElectron", { ipcRenderer.invoke("cap:native", { windowLabel, operation, payload }), emit: (event, payload) => ipcRenderer.send("cap:emit", { windowLabel, event, payload }), - onEvent: (listener) => { - eventListeners.add(listener); - return () => eventListeners.delete(listener); + onEvent: (event, listener) => { + let listeners = eventListeners.get(event); + if (!listeners) { + listeners = new Set(); + eventListeners.set(event, listeners); + ipcRenderer.send("cap:event-subscribe", { windowLabel, event }); + } + listeners.add(listener); + return () => { + listeners.delete(listener); + if (listeners.size > 0) return; + eventListeners.delete(event); + ipcRenderer.send("cap:event-unsubscribe", { windowLabel, event }); + }; }, onChannel: (listener) => { channelListeners.add(listener); diff --git a/apps/desktop/electron/windows.cjs b/apps/desktop/electron/windows.cjs index 8f767a812ca..2fe34947126 100644 --- a/apps/desktop/electron/windows.cjs +++ b/apps/desktop/electron/windows.cjs @@ -1,5 +1,8 @@ const { BrowserWindow, nativeTheme, screen } = require("electron"); +const CURSOR_POLL_INTERVAL_MS = 50; +const CURSOR_POLL_IDLE_MS = 250; + class WindowManager { constructor({ backend, devUrl, publicDir, preloadPath }) { this.backend = backend; @@ -7,15 +10,15 @@ class WindowManager { this.publicDir = publicDir; this.preloadPath = preloadPath; this.windows = new Map(); + this.eventSubscriptions = new Map(); this.windowMetadata = new WeakMap(); this.allowedToClose = new WeakSet(); this.allowAllClose = false; backend.onMessage((message) => this.receive(message)); this.nativeThemeListener = () => { const theme = nativeTheme.shouldUseDarkColors ? "dark" : "light"; - for (const [label, window] of this.windows) { - if (window.isDestroyed()) continue; - window.webContents.send("cap:event", { + for (const label of this.windows.keys()) { + this.sendEvent(label, { type: "event", event: "tauri://theme-changed", payload: theme, @@ -25,20 +28,8 @@ class WindowManager { }; nativeTheme.on("updated", this.nativeThemeListener); this.lastCursorPosition = null; - this.cursorTimer = setInterval(() => { - const position = this.dipPointToPhysical(screen.getCursorScreenPoint()); - if ( - this.lastCursorPosition?.x === position.x && - this.lastCursorPosition?.y === position.y - ) - return; - this.lastCursorPosition = position; - this.backend.send({ - type: "cursorPosition", - x: position.x, - y: position.y, - }); - }, 50); + this.cursorTimer = null; + this.cursorPollingUntil = 0; } create(options) { @@ -86,6 +77,7 @@ class WindowManager { }); this.windowMetadata.set(window, { decorated: chrome.frame, + ignoreCursorEvents: null, loaded: false, pendingShow: options.visible === true, pendingFocus: options.focus === true, @@ -186,6 +178,7 @@ class WindowManager { }); window.on("closed", () => { this.windows.delete(label); + this.eventSubscriptions.delete(label); this.sendWindowEvent(label, { type: "destroyed" }); }); window.webContents.on("did-finish-load", update); @@ -208,7 +201,7 @@ class WindowManager { if (!window || window.isDestroyed()) return; const rendererEvent = rendererWindowEvent(event, window); if (rendererEvent) { - window.webContents.send("cap:event", { + this.sendEvent(label, { type: "event", event: rendererEvent.name, payload: rendererEvent.payload, @@ -217,6 +210,30 @@ class WindowManager { } } + subscribeToEvent(label, event) { + if (typeof label !== "string" || typeof event !== "string") return; + let subscriptions = this.eventSubscriptions.get(label); + if (!subscriptions) { + subscriptions = new Set(); + this.eventSubscriptions.set(label, subscriptions); + } + subscriptions.add(event); + } + + unsubscribeFromEvent(label, event) { + const subscriptions = this.eventSubscriptions.get(label); + if (!subscriptions) return; + subscriptions.delete(event); + if (subscriptions.size === 0) this.eventSubscriptions.delete(label); + } + + sendEvent(label, message) { + if (!this.eventSubscriptions.get(label)?.has(message.event)) return; + const window = this.windows.get(label); + if (!window || window.isDestroyed()) return; + window.webContents.send("cap:event", message); + } + receive(message) { if (message.type === "createWindow") this.create(message.options); if (message.type === "desktopOperation") { @@ -225,16 +242,16 @@ class WindowManager { this.operation(window, message.operation); } if (message.type === "event") { - for (const [label, window] of this.windows) { + for (const label of this.windows.keys()) { if (!message.target || message.target === label) { - window.webContents.send("cap:event", message); + this.sendEvent(label, message); } } } if (message.type === "channel") { - for (const window of this.windows.values()) { + const window = this.windows.get(message.target); + if (window && !window.isDestroyed()) window.webContents.send("cap:channel", message); - } } } @@ -320,7 +337,10 @@ class WindowManager { window.setSkipTaskbar(operation.skip); break; case "setIgnoreCursorEvents": + this.ensureCursorPolling(); + if (metadata.ignoreCursorEvents === operation.ignore) break; window.setIgnoreMouseEvents(operation.ignore, { forward: true }); + metadata.ignoreCursorEvents = operation.ignore; break; case "setOpacity": window.setOpacity(operation.opacity); @@ -358,6 +378,41 @@ class WindowManager { return this.dipPointToPhysical(screen.getCursorScreenPoint()); } + ensureCursorPolling() { + this.cursorPollingUntil = Date.now() + CURSOR_POLL_IDLE_MS; + this.sendCursorPosition(); + if (this.cursorTimer) return; + + this.cursorTimer = setInterval(() => { + if (Date.now() >= this.cursorPollingUntil) { + this.stopCursorPolling(); + return; + } + this.sendCursorPosition(); + }, CURSOR_POLL_INTERVAL_MS); + this.cursorTimer.unref?.(); + } + + sendCursorPosition() { + const position = this.cursorPosition(); + if ( + this.lastCursorPosition?.x === position.x && + this.lastCursorPosition?.y === position.y + ) + return; + this.lastCursorPosition = position; + this.backend.send({ + type: "cursorPosition", + x: position.x, + y: position.y, + }); + } + + stopCursorPolling() { + if (this.cursorTimer) clearInterval(this.cursorTimer); + this.cursorTimer = null; + } + windowState(window) { const dipBounds = window.getBounds(); const display = screen.getDisplayMatching(dipBounds); @@ -467,6 +522,8 @@ class WindowManager { beginQuit() { this.allowAllClose = true; + this.stopCursorPolling(); + nativeTheme.removeListener("updated", this.nativeThemeListener); } } diff --git a/apps/desktop/src/electron/bridge.ts b/apps/desktop/src/electron/bridge.ts index 3cc77df5ea7..94cac8e86f7 100644 --- a/apps/desktop/src/electron/bridge.ts +++ b/apps/desktop/src/electron/bridge.ts @@ -5,7 +5,7 @@ export interface CapElectronBridge { invoke(command: string, args?: Record): Promise; native(operation: string, payload?: Record): Promise; emit(event: string, payload?: unknown): void; - onEvent(listener: (message: BridgeEvent) => void): () => void; + onEvent(event: string, listener: (message: BridgeEvent) => void): () => void; onChannel(listener: (message: BridgeChannelMessage) => void): () => void; } @@ -18,6 +18,7 @@ export interface BridgeEvent { export interface BridgeChannelMessage { type: "channel"; + target: string; channelId: number; index: number; message?: unknown; diff --git a/apps/desktop/src/electron/event.ts b/apps/desktop/src/electron/event.ts index 49374b0274f..c18a02e451c 100644 --- a/apps/desktop/src/electron/event.ts +++ b/apps/desktop/src/electron/event.ts @@ -17,9 +17,8 @@ export async function listen( handler: EventCallback, ): Promise { const id = nextEventId++; - return bridge().onEvent((message) => { - if (message.event === event) - handler({ event, id, payload: message.payload as T }); + return bridge().onEvent(event, (message) => { + handler({ event, id, payload: message.payload as T }); }); } diff --git a/crates/desktop-runtime-macros/src/lib.rs b/crates/desktop-runtime-macros/src/lib.rs index 21470931faf..d7424798cb1 100644 --- a/crates/desktop-runtime-macros/src/lib.rs +++ b/crates/desktop-runtime-macros/src/lib.rs @@ -88,6 +88,7 @@ fn expand_command(function: ItemFn) -> proc_macro2::TokenStream { let #variable = ::cap_desktop_runtime::Channel::<#inner>::from_value( context.channel_sender(), ::cap_desktop_runtime::take_argument(&mut arguments, #key)?, + context.window_label(), )?; }); arguments.push(quote! { #variable }); diff --git a/crates/desktop-runtime/src/lib.rs b/crates/desktop-runtime/src/lib.rs index fa508afe5a0..b028bde45a4 100644 --- a/crates/desktop-runtime/src/lib.rs +++ b/crates/desktop-runtime/src/lib.rs @@ -277,6 +277,10 @@ impl CommandContext { &self.app } + pub fn window_label(&self) -> &str { + &self.window_label + } + pub fn window(&self) -> Window { Window::new(self.app.clone(), self.window_label.clone()) } @@ -1027,6 +1031,7 @@ impl Window { #[derive(Clone)] pub struct Channel { id: u64, + target: String, next_index: Arc, sender: Option>, local_sender: Option Result<()> + Send + Sync>>, @@ -1037,6 +1042,7 @@ impl Channel { pub fn new(sender: impl Fn(T) -> Result<()> + Send + Sync + 'static) -> Self { Self { id: 0, + target: String::new(), next_index: Arc::new(AtomicU64::new(0)), sender: None, local_sender: Some(Arc::new(sender)), @@ -1044,7 +1050,11 @@ impl Channel { } } - pub fn from_value(sender: mpsc::UnboundedSender, value: Value) -> Result { + pub fn from_value( + sender: mpsc::UnboundedSender, + value: Value, + target: impl Into, + ) -> Result { let marker = value .as_str() .and_then(|value| value.strip_prefix("__CHANNEL__:")) @@ -1054,6 +1064,7 @@ impl Channel { .map_err(|error| format!("invalid desktop channel id: {error}"))?; Ok(Self { id, + target: target.into(), next_index: Arc::new(AtomicU64::new(0)), sender: Some(sender), local_sender: None, @@ -1073,6 +1084,7 @@ impl Channel { .as_ref() .ok_or_else(|| "desktop channel sender is unavailable".to_string())? .send(ChannelMessage { + target: self.target.clone(), channel_id: self.id, index: self.next_index.fetch_add(1, Ordering::Relaxed), message: Some(message), @@ -1088,6 +1100,7 @@ impl Drop for Channel { return; }; let _ = sender.send(ChannelMessage { + target: self.target.clone(), channel_id: self.id, index: self.next_index.load(Ordering::Relaxed), message: None, @@ -1331,4 +1344,29 @@ mod tests { .unwrap(); assert_eq!(project_path, PathBuf::from("/tmp/a.cap")); } + + #[test] + fn channel_messages_are_bound_to_the_invoking_window() { + let (sender, mut receiver) = mpsc::unbounded_channel(); + let channel = + Channel::::from_value(sender, serde_json::json!("__CHANNEL__:7"), "editor-42") + .unwrap(); + + channel.send(123).unwrap(); + drop(channel); + + let message = receiver.try_recv().unwrap(); + assert_eq!(message.target, "editor-42"); + assert_eq!(message.channel_id, 7); + assert_eq!(message.index, 0); + assert_eq!(message.message, Some(serde_json::json!(123))); + assert!(!message.end); + + let end = receiver.try_recv().unwrap(); + assert_eq!(end.target, "editor-42"); + assert_eq!(end.channel_id, 7); + assert_eq!(end.index, 1); + assert_eq!(end.message, None); + assert!(end.end); + } } diff --git a/crates/desktop-runtime/src/protocol.rs b/crates/desktop-runtime/src/protocol.rs index 0ecfb18d737..f6267ddef6f 100644 --- a/crates/desktop-runtime/src/protocol.rs +++ b/crates/desktop-runtime/src/protocol.rs @@ -94,6 +94,7 @@ pub enum CommandResponse { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChannelMessage { + pub target: String, pub channel_id: u64, pub index: u64, pub message: Option, From ad0ffb8f12a747869bb96d2fd80f2e7839480245 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:29:32 +0100 Subject: [PATCH 19/31] perf: defer nonessential renderer work --- apps/desktop/src/app.tsx | 98 ++++++++++++++++++++--------- apps/desktop/src/utils/analytics.ts | 2 - 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx index f2920eb3f2f..9624b4f79d2 100644 --- a/apps/desktop/src/app.tsx +++ b/apps/desktop/src/app.tsx @@ -8,7 +8,6 @@ import { onMount, Suspense, } from "solid-js"; -import { Toaster } from "solid-toast"; import { message } from "~/electron/dialog"; import { getCurrentWebviewWindow, @@ -20,13 +19,15 @@ import "unfonts.css"; import "./styles/theme.css"; import { CapErrorBoundary } from "./components/CapErrorBoundary"; -import WindowChromeLayout from "./routes/(window-chrome)"; -import SettingsLayout from "./routes/(window-chrome)/settings"; import { authStore, generalSettingsStore } from "./store"; -import { identifyUser, initAnonymousUser } from "./utils/analytics"; import { type AppTheme, commands } from "./utils/tauri"; import titlebar from "./utils/titlebar-state"; +const WindowChromeLayout = lazy(() => import("./routes/(window-chrome)")); +const SettingsLayout = lazy(() => import("./routes/(window-chrome)/settings")); +const GlobalToaster = lazy(() => + import("solid-toast").then(({ Toaster }) => ({ default: Toaster })), +); const NewMainPage = lazy(() => import("./routes/(window-chrome)/new-main")); const SettingsGeneralPage = lazy( () => import("./routes/(window-chrome)/settings/general"), @@ -123,38 +124,39 @@ export default function App() { function Inner() { const currentWindow = getCurrentWebviewWindow(); - createThemeListener(currentWindow); + if (location.pathname !== "/camera") createThemeListener(currentWindow); onMount(() => { - initAnonymousUser(); - // OpenPanel keeps profileId in memory only (PostHog persisted it), so - // sign-in-time identify alone loses attribution after an app restart. - void authStore.get().then((auth) => { - if (auth?.user_id) identifyUser(auth.user_id); - }); + if (shouldInitializeAnalytics(currentWindow.label)) { + void initializeAnalytics(); + } prewarmFontCaches(); }); return ( <> - + {shouldRenderGlobalToaster(location.pathname) && ( + + + + )} { @@ -265,6 +267,44 @@ function Inner() { ); } +function shouldRenderGlobalToaster(pathname: string) { + switch (pathname) { + case "/camera": + case "/capture-area": + case "/debug": + case "/in-progress-recording": + case "/mode-select": + case "/notifications": + case "/recordings-overlay": + case "/teleprompter": + case "/window-capture-occluder": + return false; + default: + return true; + } +} + +function shouldInitializeAnalytics(label: string) { + return ( + label === "main" || label === "settings" || label.startsWith("editor-") + ); +} + +async function initializeAnalytics() { + try { + const { identifyUser, initAnonymousUser } = await import( + "./utils/analytics" + ); + initAnonymousUser(); + // OpenPanel keeps profileId in memory only (PostHog persisted it), so + // sign-in-time identify alone loses attribution after an app restart. + const auth = await authStore.get(); + if (auth?.user_id) identifyUser(auth.user_id); + } catch (error) { + console.error("Failed to initialize analytics:", error); + } +} + // WebKit resolves the emoji fallback chain lazily on first glyph paint, which // can jank the first list/text render containing emoji (e.g. recording // titles). Drawing once to an offscreen canvas at idle warms the per-process diff --git a/apps/desktop/src/utils/analytics.ts b/apps/desktop/src/utils/analytics.ts index ff1087e1541..e99806f95bf 100644 --- a/apps/desktop/src/utils/analytics.ts +++ b/apps/desktop/src/utils/analytics.ts @@ -130,5 +130,3 @@ export function trackEvent( } }); } - -void isTelemetryEnabled(); From f11a0ffca02034a4fc1a4e9da908e62a522cfd36 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:29:36 +0100 Subject: [PATCH 20/31] perf: bound editor playback memory --- crates/editor/src/playback.rs | 298 ++++++++++++++++-- crates/rendering/src/decoder/avassetreader.rs | 50 ++- 2 files changed, 314 insertions(+), 34 deletions(-) diff --git a/crates/editor/src/playback.rs b/crates/editor/src/playback.rs index bb496afd878..fa92bb89ffb 100644 --- a/crates/editor/src/playback.rs +++ b/crates/editor/src/playback.rs @@ -2,8 +2,8 @@ use cap_project::{ ClipOffsets, ClipTransitionType, ProjectConfiguration, TimelineFrameMapping, XY, }; use cap_rendering::{ - DecodedSegmentFrames, PrecomputedCursorTimeline, ProjectUniforms, RecordingSegmentDecoders, - RenderVideoConstants, ZoomTransformTimeline, + DecodedFrame, DecodedSegmentFrames, PixelFormat, PrecomputedCursorTimeline, ProjectUniforms, + RecordingSegmentDecoders, RenderVideoConstants, ZoomTransformTimeline, spring_mass_damper::SpringMassDamperSimulationConfig, }; use futures::stream::{FuturesUnordered, StreamExt}; @@ -11,7 +11,11 @@ use lru::LruCache; use std::{ collections::{HashSet, VecDeque}, num::NonZeroUsize, - sync::{Arc, RwLock, mpsc as std_mpsc}, + sync::{ + Arc, RwLock, + atomic::{AtomicUsize, Ordering}, + mpsc as std_mpsc, + }, time::{Duration, Instant}, }; use tokio::sync::watch; @@ -27,11 +31,14 @@ use crate::{ }, }; -const PREFETCH_BUFFER_SIZE: usize = 90; +const INITIAL_PREFETCH_BUFFER_SIZE: usize = 10; +const MAX_PREFETCH_BUFFER_SIZE: usize = 90; +const PREFETCH_CHANNEL_SIZE: usize = 4; +const MAX_PREFETCH_BUFFER_BYTES: usize = 256 * 1024 * 1024; const PARALLEL_DECODE_TASKS: usize = 4; const INITIAL_PARALLEL_DECODE_TASKS: usize = 4; -const MAX_PREFETCH_AHEAD: u32 = 90; const FRAME_CACHE_SIZE: usize = 90; +const MAX_FRAME_CACHE_BYTES: usize = 128 * 1024 * 1024; const RAMP_UP_FRAME_COUNT: u32 = 15; #[cfg(target_os = "windows")] @@ -136,6 +143,15 @@ struct PrefetchedTransition { } impl PrefetchedFrame { + fn byte_len(&self) -> usize { + decoded_segment_frames_byte_len(&self.segment_frames).saturating_add( + self.transition + .as_ref() + .map(|transition| decoded_segment_frames_byte_len(&transition.segment_frames)) + .unwrap_or(0), + ) + } + fn into_cached(self) -> CachedFrame { ( Arc::new(self.segment_frames), @@ -155,14 +171,78 @@ impl PrefetchedFrame { type CachedTransition = (Arc, u32, ClipTransitionType, f32); type CachedFrame = (Arc, u32, Option); +fn decoded_frame_byte_len(frame: &DecodedFrame) -> usize { + if !frame.data().is_empty() { + return frame.data().len(); + } + + let pixels = (frame.width() as usize).saturating_mul(frame.height() as usize); + match frame.format() { + PixelFormat::Rgba => pixels.saturating_mul(4), + PixelFormat::Nv12 | PixelFormat::Yuv420p => pixels.saturating_mul(3) / 2, + } +} + +fn decoded_segment_frames_byte_len(frames: &DecodedSegmentFrames) -> usize { + frames + .screen_frame + .as_ref() + .map(decoded_frame_byte_len) + .unwrap_or(0) + .saturating_add( + frames + .camera_frame + .as_ref() + .map(decoded_frame_byte_len) + .unwrap_or(0), + ) +} + +fn cached_frame_byte_len(frame: &CachedFrame) -> usize { + decoded_segment_frames_byte_len(&frame.0).saturating_add( + frame + .2 + .as_ref() + .map(|transition| decoded_segment_frames_byte_len(&transition.0)) + .unwrap_or(0), + ) +} + +fn prefetch_frame_limit(frame_bytes: usize) -> usize { + if frame_bytes == 0 { + return MAX_PREFETCH_BUFFER_SIZE; + } + + (MAX_PREFETCH_BUFFER_BYTES / frame_bytes) + .clamp(INITIAL_PREFETCH_BUFFER_SIZE, MAX_PREFETCH_BUFFER_SIZE) +} + +fn observe_prefetched_frame( + frame: &PrefetchedFrame, + largest_frame_bytes: &mut usize, + frame_limit: &AtomicUsize, +) { + let frame_bytes = frame.byte_len(); + if frame_bytes <= *largest_frame_bytes { + return; + } + + *largest_frame_bytes = frame_bytes; + frame_limit.store(prefetch_frame_limit(frame_bytes), Ordering::Relaxed); +} + struct FrameCache { cache: LruCache, + cache_bytes: usize, + max_bytes: usize, } impl FrameCache { - fn new(capacity: usize) -> Self { + fn new(capacity: usize, max_bytes: usize) -> Self { Self { cache: LruCache::new(NonZeroUsize::new(capacity).unwrap()), + cache_bytes: 0, + max_bytes, } } @@ -194,8 +274,24 @@ impl FrameCache { segment_index: u32, transition: Option, ) { - self.cache - .put(frame_number, (segment_frames, segment_index, transition)); + let frame = (segment_frames, segment_index, transition); + let frame_bytes = cached_frame_byte_len(&frame); + + if let Some((_, replaced)) = self.cache.push(frame_number, frame) { + self.cache_bytes = self + .cache_bytes + .saturating_sub(cached_frame_byte_len(&replaced)); + } + self.cache_bytes = self.cache_bytes.saturating_add(frame_bytes); + + while self.cache.len() > 1 && self.cache_bytes > self.max_bytes { + let Some((_, evicted)) = self.cache.pop_lru() else { + break; + }; + self.cache_bytes = self + .cache_bytes + .saturating_sub(cached_frame_byte_len(&evicted)); + } } fn evict_far_from(&mut self, current_frame: u32, max_distance: u32) { @@ -212,7 +308,11 @@ impl FrameCache { .collect(); for key in keys_to_remove { - self.cache.pop(&key); + if let Some(evicted) = self.cache.pop(&key) { + self.cache_bytes = self + .cache_bytes + .saturating_sub(cached_frame_byte_len(&evicted)); + } } } } @@ -362,9 +462,11 @@ impl Playback { event_rx, }; - let (prefetch_tx, prefetch_rx) = std_mpsc::channel::(); + let (prefetch_tx, prefetch_rx) = + std_mpsc::sync_channel::(PREFETCH_CHANNEL_SIZE); let (frame_request_tx, mut frame_request_rx) = watch::channel(self.start_frame_number); let (playback_position_tx, playback_position_rx) = watch::channel(self.start_frame_number); + let prefetch_frame_limit = Arc::new(AtomicUsize::new(INITIAL_PREFETCH_BUFFER_SIZE)); let output_size = ProjectUniforms::get_output_size( &self.render_constants.options, @@ -376,9 +478,10 @@ impl Playback { let in_flight_frames: Arc>> = Arc::new(RwLock::new(HashSet::new())); let prefetch_in_flight = in_flight_frames.clone(); + let producer_prefetch_frame_limit = Arc::clone(&prefetch_frame_limit); let _main_in_flight = in_flight_frames; - let prefetch_stop_rx = stop_rx.clone(); + let mut prefetch_stop_rx = stop_rx.clone(); let mut prefetch_project = self.project.clone(); let prefetch_segment_medias = self.segment_medias.clone(); let (prefetch_duration, has_timeline) = self @@ -404,12 +507,28 @@ impl Playback { let mut in_flight: FuturesUnordered = FuturesUnordered::new(); let mut frames_decoded: u32 = 0; let mut cached_project = prefetch_project.borrow().clone(); + let mut pending_frame: Option = None; loop { if *prefetch_stop_rx.borrow() { break; } + if let Some(frame) = pending_frame.take() { + match prefetch_tx.try_send(frame) { + Ok(()) => {} + Err(std_mpsc::TrySendError::Full(frame)) => { + pending_frame = Some(frame); + tokio::select! { + _ = prefetch_stop_rx.changed() => {} + _ = tokio::time::sleep(Duration::from_millis(1)) => {} + } + continue; + } + Err(std_mpsc::TrySendError::Disconnected(_)) => break, + } + } + if prefetch_project.has_changed().unwrap_or(false) { cached_project = prefetch_project.borrow_and_update().clone(); } @@ -432,14 +551,19 @@ impl Playback { in_flight_guard.clear(); } - if is_backward_seek || seek_distance > MAX_PREFETCH_AHEAD / 2 { + let reset_distance = + producer_prefetch_frame_limit.load(Ordering::Relaxed).max(2) as u32 / 2; + if is_backward_seek || seek_distance > reset_distance { in_flight = FuturesUnordered::new(); } } } let current_playback_frame = *playback_position_rx.borrow(); - let max_prefetch_ahead = MAX_PREFETCH_AHEAD; + let max_prefetch_ahead = producer_prefetch_frame_limit + .load(Ordering::Relaxed) + .clamp(INITIAL_PREFETCH_BUFFER_SIZE, MAX_PREFETCH_BUFFER_SIZE) + as u32; let max_prefetch_frame = current_playback_frame + max_prefetch_ahead; let initial_parallel_decode_tasks = INITIAL_PARALLEL_DECODE_TASKS; @@ -525,12 +649,19 @@ impl Playback { frames_decoded = frames_decoded.saturating_add(1); if let Some(segment_frames) = result { - let _ = prefetch_tx.send(PrefetchedFrame { + let frame = PrefetchedFrame { frame_number: frame_num, segment_frames, segment_index, transition, - }); + }; + match prefetch_tx.try_send(frame) { + Ok(()) => {} + Err(std_mpsc::TrySendError::Full(frame)) => { + pending_frame = Some(frame); + } + Err(std_mpsc::TrySendError::Disconnected(_)) => break, + } } else if frames_decoded <= 5 { warn!( frame = frame_num, @@ -576,8 +707,9 @@ impl Playback { let frame_duration = Duration::from_secs_f64(1.0 / fps_f64); let mut frame_number = self.start_frame_number; let mut prefetch_buffer: VecDeque = - VecDeque::with_capacity(PREFETCH_BUFFER_SIZE); - let mut frame_cache = FrameCache::new(FRAME_CACHE_SIZE); + VecDeque::with_capacity(MAX_PREFETCH_BUFFER_SIZE + PREFETCH_CHANNEL_SIZE); + let mut frame_cache = FrameCache::new(FRAME_CACHE_SIZE, MAX_FRAME_CACHE_BYTES); + let mut largest_prefetched_frame_bytes = 0usize; let mut total_frames_rendered = 0u64; let mut total_frames_skipped = 0u64; @@ -603,7 +735,9 @@ impl Playback { prefetch_buffer .iter() .any(|p| p.frame_number == frame_number) - || prefetch_buffer.len() >= warmup_target_frames + || prefetch_buffer.len() + >= warmup_target_frames + .min(prefetch_frame_limit.load(Ordering::Relaxed)) || first_time.elapsed() > warmup_after_first_timeout } else { false @@ -625,15 +759,27 @@ impl Playback { match prefetch_rx.recv_timeout(Duration::from_millis(50)) { Ok(prefetched) => { if prefetched.frame_number >= frame_number { + observe_prefetched_frame( + &prefetched, + &mut largest_prefetched_frame_bytes, + &prefetch_frame_limit, + ); prefetch_buffer.push_back(prefetched); if first_frame_time.is_none() { first_frame_time = Some(Instant::now()); } } - while prefetch_buffer.len() < warmup_target_frames { + while prefetch_buffer.len() + < warmup_target_frames.min(prefetch_frame_limit.load(Ordering::Relaxed)) + { match prefetch_rx.try_recv() { Ok(p) => { if p.frame_number >= frame_number { + observe_prefetched_frame( + &p, + &mut largest_prefetched_frame_bytes, + &prefetch_frame_limit, + ); prefetch_buffer.push_back(p); } } @@ -838,10 +984,17 @@ impl Playback { } } - while prefetch_buffer.len() < warmup_target_frames { + while prefetch_buffer.len() + < warmup_target_frames.min(prefetch_frame_limit.load(Ordering::Relaxed)) + { match prefetch_rx.try_recv() { Ok(prefetched) => { if prefetched.frame_number >= frame_number { + observe_prefetched_frame( + &prefetched, + &mut largest_prefetched_frame_bytes, + &prefetch_frame_limit, + ); prefetch_buffer.push_back(prefetched); } } @@ -923,7 +1076,10 @@ impl Playback { { prefetch_buffer.pop_front(); } - frame_cache.evict_far_from(frame_number, MAX_PREFETCH_AHEAD); + frame_cache.evict_far_from( + frame_number, + prefetch_frame_limit.load(Ordering::Relaxed) as u32, + ); let _ = frame_request_tx.send(frame_number); let _ = playback_position_tx.send(frame_number); if let Some(telemetry) = &self.telemetry { @@ -947,11 +1103,18 @@ impl Playback { prefetch_buffer.retain(|p| p.frame_number >= frame_number); let drain_budget = 16usize; let mut drained = 0usize; - while prefetch_buffer.len() < PREFETCH_BUFFER_SIZE && drained < drain_budget { + while prefetch_buffer.len() < prefetch_frame_limit.load(Ordering::Relaxed) + && drained < drain_budget + { match prefetch_rx.try_recv() { Ok(prefetched) => { drained += 1; if prefetched.frame_number >= frame_number { + observe_prefetched_frame( + &prefetched, + &mut largest_prefetched_frame_bytes, + &prefetch_frame_limit, + ); prefetch_buffer.push_back(prefetched); } } @@ -1006,6 +1169,11 @@ impl Playback { match prefetched_opt { Some(prefetched) => { + observe_prefetched_frame( + &prefetched, + &mut largest_prefetched_frame_bytes, + &prefetch_frame_limit, + ); if prefetched.frame_number == frame_number { frame_source = PlaybackFrameSource::PrefetchWaitExact; Some(prefetched.into_cached()) @@ -1084,10 +1252,18 @@ impl Playback { // regular drain budget of 16 per iteration can miss frames that arrived // between the drain and the buffer check. This protects both the // jump-to-min path and the +1 fallback path below. - while prefetch_buffer.len() < PREFETCH_BUFFER_SIZE { + let late_drain_limit = prefetch_frame_limit + .load(Ordering::Relaxed) + .saturating_add(PREFETCH_CHANNEL_SIZE); + while prefetch_buffer.len() < late_drain_limit { match prefetch_rx.try_recv() { Ok(p) => { if p.frame_number >= frame_number { + observe_prefetched_frame( + &p, + &mut largest_prefetched_frame_bytes, + &prefetch_frame_limit, + ); prefetch_buffer.push_back(p); } } @@ -1327,7 +1503,10 @@ impl Playback { { prefetch_buffer.pop_front(); } - frame_cache.evict_far_from(frame_number, MAX_PREFETCH_AHEAD); + frame_cache.evict_far_from( + frame_number, + prefetch_frame_limit.load(Ordering::Relaxed) as u32, + ); let _ = frame_request_tx.send(frame_number); let _ = playback_position_tx.send(frame_number); if let Some(telemetry) = &self.telemetry { @@ -1366,24 +1545,81 @@ impl Playback { } } +impl PlaybackHandle { + pub fn stop(&self) { + self.stop_tx.send(true).ok(); + } + + pub async fn receive_event(&mut self) -> watch::Ref<'_, PlaybackEvent> { + self.event_rx.changed().await.ok(); + self.event_rx.borrow_and_update() + } +} + #[cfg(test)] mod tests { use super::*; + fn decoded_frames(bytes: usize) -> DecodedSegmentFrames { + DecodedSegmentFrames { + screen_frame: Some(DecodedFrame::new(vec![0; bytes], 1, 1)), + camera_frame: None, + segment_time: 0.0, + recording_time: 0.0, + segment_has_camera: false, + } + } + #[test] fn timeline_music_enables_audio_playback_without_recorded_audio() { assert!(has_playback_audio(&[], true)); assert!(!has_playback_audio(&[], false)); } -} -impl PlaybackHandle { - pub fn stop(&self) { - self.stop_tx.send(true).ok(); + #[test] + fn prefetch_limit_tracks_decoded_frame_memory() { + assert_eq!(prefetch_frame_limit(0), MAX_PREFETCH_BUFFER_SIZE); + assert_eq!( + prefetch_frame_limit(4 * 1024 * 1024), + MAX_PREFETCH_BUFFER_BYTES / (4 * 1024 * 1024) + ); + assert_eq!( + prefetch_frame_limit(MAX_PREFETCH_BUFFER_BYTES), + INITIAL_PREFETCH_BUFFER_SIZE + ); } - pub async fn receive_event(&mut self) -> watch::Ref<'_, PlaybackEvent> { - self.event_rx.changed().await.ok(); - self.event_rx.borrow_and_update() + #[test] + fn frame_cache_evicts_oldest_frames_to_stay_within_byte_budget() { + let mut cache = FrameCache::new(10, 15); + cache.insert(1, Arc::new(decoded_frames(10)), 0, None); + cache.insert(2, Arc::new(decoded_frames(10)), 0, None); + + assert_eq!(cache.cache.len(), 1); + assert_eq!(cache.cache_bytes, 10); + assert!(cache.get(1).is_none()); + assert!(cache.get(2).is_some()); + } + + #[test] + fn frame_cache_keeps_one_oversized_frame() { + let mut cache = FrameCache::new(10, 4); + cache.insert(1, Arc::new(decoded_frames(10)), 0, None); + + assert_eq!(cache.cache.len(), 1); + assert_eq!(cache.cache_bytes, 10); + assert!(cache.get(1).is_some()); + } + + #[test] + fn frame_cache_accounts_for_count_based_eviction() { + let mut cache = FrameCache::new(2, usize::MAX); + cache.insert(1, Arc::new(decoded_frames(10)), 0, None); + cache.insert(2, Arc::new(decoded_frames(10)), 0, None); + cache.insert(3, Arc::new(decoded_frames(10)), 0, None); + + assert_eq!(cache.cache.len(), 2); + assert_eq!(cache.cache_bytes, 20); + assert!(cache.get(1).is_none()); } } diff --git a/crates/rendering/src/decoder/avassetreader.rs b/crates/rendering/src/decoder/avassetreader.rs index 1d121e2712c..20e24901bc1 100644 --- a/crates/rendering/src/decoder/avassetreader.rs +++ b/crates/rendering/src/decoder/avassetreader.rs @@ -25,6 +25,19 @@ use super::{ const MAX_RELAXED_FALLBACK_DISTANCE: u32 = 8; const SCRUB_REUSE_THRESHOLD_SECS: f32 = 0.5; const DECODER_REQUEST_CLUSTER_GAP_FRAMES: u32 = FRAME_CACHE_SIZE as u32 / 2; +// A frame-count-only limit can retain more than 1 GiB for high-resolution +// recordings. Keep the existing temporal cap, but also bound the raw decoded +// frame data so playback remains viable on memory-constrained devices. +const MAX_FRAME_CACHE_BYTES: usize = 256 * 1024 * 1024; + +fn frame_cache_needs_eviction( + current_frames: usize, + current_bytes: usize, + next_frame_bytes: usize, +) -> bool { + current_frames >= FRAME_CACHE_SIZE + || current_bytes.saturating_add(next_frame_bytes) > MAX_FRAME_CACHE_BYTES +} #[derive(Clone)] struct FrameData { @@ -250,6 +263,10 @@ impl CachedFrame { } } + fn byte_len(&self) -> usize { + self.0.frame_data.data.len() + } + fn data(&self) -> &ProcessedFrame { &self.0 } @@ -577,6 +594,7 @@ impl AVAssetReaderDecoder { ready_tx.send(Ok(init_result)).ok(); let mut cache = BTreeMap::::new(); + let mut cache_bytes = 0usize; #[allow(unused)] let mut last_active_frame = None::; @@ -765,6 +783,10 @@ impl AVAssetReaderDecoder { if was_reset { *last_sent_frame.borrow_mut() = None; cache.retain(|&f, _| f >= cache_min && f <= cache_max); + cache_bytes = cache + .values() + .map(CachedFrame::byte_len) + .fold(0usize, usize::saturating_add); } last_active_frame = Some(requested_frame); @@ -842,7 +864,13 @@ impl AVAssetReaderDecoder { let too_small_for_cache_bounds = current_frame < cache_min; if !too_small_for_cache_bounds { - if cache.len() >= FRAME_CACHE_SIZE { + let frame_bytes = cache_frame.byte_len(); + if let Some(replaced) = cache.remove(¤t_frame) { + cache_bytes = cache_bytes.saturating_sub(replaced.byte_len()); + } + while !cache.is_empty() + && frame_cache_needs_eviction(cache.len(), cache_bytes, frame_bytes) + { if let Some(last_active) = &last_active_frame { let frame_to_remove = if requested_frame > *last_active { *cache.keys().next().unwrap() @@ -853,12 +881,16 @@ impl AVAssetReaderDecoder { let max = *cache.keys().max().unwrap(); if current_frame > max { min } else { max } }; - cache.remove(&frame_to_remove); + if let Some(removed) = cache.remove(&frame_to_remove) { + cache_bytes = cache_bytes.saturating_sub(removed.byte_len()); + } } else { - cache.clear() + cache.clear(); + cache_bytes = 0; } } + cache_bytes = cache_bytes.saturating_add(frame_bytes); cache.insert(current_frame, cache_frame.clone()); let mut remaining_requests = Vec::with_capacity(pending_requests.len()); @@ -1120,6 +1152,18 @@ impl AVAssetReaderDecoder { mod tests { use super::*; + #[test] + fn frame_cache_enforces_frame_and_byte_limits() { + assert!(!frame_cache_needs_eviction( + FRAME_CACHE_SIZE - 1, + MAX_FRAME_CACHE_BYTES - 1, + 1, + )); + assert!(frame_cache_needs_eviction(FRAME_CACHE_SIZE, 0, 1)); + assert!(frame_cache_needs_eviction(1, MAX_FRAME_CACHE_BYTES, 1,)); + assert!(frame_cache_needs_eviction(1, usize::MAX, 1)); + } + #[test] fn test_decoder_health_new() { let health = DecoderHealth::new(); From 679a64f0f7f5f79133e7b97bff6545726430835a Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:37:36 +0100 Subject: [PATCH 21/31] fix: scope release staging to commits --- .github/workflows/desktop-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index f2022d8ec75..bfbc723b0c6 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -64,7 +64,7 @@ jobs: const tag = channel === "nightly" ? `cap-nightly-v${version}` : `cap-v${version}`; - const stagingTag = `${tag}-staging`; + const stagingTag = `${tag}-staging-${sha}`; const tagRef = `tags/${tag}`; const releases = await github.paginate(github.rest.repos.listReleases, { owner: context.repo.owner, From e739e3206aef9de4a73e7ba300b6b1ef5fabd55b Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:02:49 +0100 Subject: [PATCH 22/31] perf: retain decoded frames on the GPU --- crates/rendering/src/decoder/avassetreader.rs | 174 ++++++------ crates/rendering/src/decoder/mod.rs | 256 +++++++++++++++++- crates/rendering/src/layers/camera.rs | 60 +++- crates/rendering/src/layers/display.rs | 64 ++++- crates/rendering/src/yuv_converter.rs | 25 +- 5 files changed, 467 insertions(+), 112 deletions(-) diff --git a/crates/rendering/src/decoder/avassetreader.rs b/crates/rendering/src/decoder/avassetreader.rs index 20e24901bc1..fac9e4c0dd2 100644 --- a/crates/rendering/src/decoder/avassetreader.rs +++ b/crates/rendering/src/decoder/avassetreader.rs @@ -39,49 +39,15 @@ fn frame_cache_needs_eviction( || current_bytes.saturating_add(next_frame_bytes) > MAX_FRAME_CACHE_BYTES } -#[derive(Clone)] -struct FrameData { - data: Arc>, - y_stride: u32, - uv_stride: u32, -} - #[derive(Clone)] struct ProcessedFrame { number: u32, - width: u32, - height: u32, - format: PixelFormat, - frame_data: FrameData, + frame: DecodedFrame, } impl ProcessedFrame { fn to_decoded_frame(&self) -> DecodedFrame { - let FrameData { - data, - y_stride, - uv_stride, - } = &self.frame_data; - - match self.format { - PixelFormat::Rgba => { - DecodedFrame::new_with_arc(Arc::clone(data), self.width, self.height) - } - PixelFormat::Nv12 => DecodedFrame::new_nv12_with_arc( - Arc::clone(data), - self.width, - self.height, - *y_stride, - *uv_stride, - ), - PixelFormat::Yuv420p => DecodedFrame::new_yuv420p_with_arc( - Arc::clone(data), - self.width, - self.height, - *y_stride, - *uv_stride, - ), - } + self.frame.clone() } } @@ -232,39 +198,41 @@ impl CachedFrame { | format::Pixel::RGBA | format::Pixel::BGRA | format::Pixel::YUV420P => { - let mut img = image_buf; - let (data, fmt, y_str, uv_str) = processor.extract_raw(&mut img); - Self(ProcessedFrame { - number, - width, - height, - format: fmt, - frame_data: FrameData { - data: Arc::new(data), - y_stride: y_str, - uv_stride: uv_str, - }, - }) + let frame = if pixel_format == format::Pixel::NV12 && image_buf.io_surf().is_some() + { + let y_stride = image_buf.plane_bytes_per_row(0) as u32; + let uv_stride = image_buf.plane_bytes_per_row(1) as u32; + DecodedFrame::new_nv12_with_image_buf( + width, height, y_stride, uv_stride, image_buf, + ) + } else { + let mut image_buf = image_buf; + let (data, fmt, y_str, uv_str) = processor.extract_raw(&mut image_buf); + let data = Arc::new(data); + match fmt { + PixelFormat::Nv12 => { + DecodedFrame::new_nv12_with_arc(data, width, height, y_str, uv_str) + } + PixelFormat::Rgba => DecodedFrame::new_with_arc(data, width, height), + PixelFormat::Yuv420p => { + DecodedFrame::new_yuv420p_with_arc(data, width, height, y_str, uv_str) + } + } + }; + Self(ProcessedFrame { number, frame }) } _ => { let black_frame = vec![0u8; (width * height * 4) as usize]; Self(ProcessedFrame { number, - width, - height, - format: PixelFormat::Rgba, - frame_data: FrameData { - data: Arc::new(black_frame), - y_stride: width * 4, - uv_stride: 0, - }, + frame: DecodedFrame::new(black_frame, width, height), }) } } } fn byte_len(&self) -> usize { - self.0.frame_data.data.len() + self.0.frame.byte_len() } fn data(&self) -> &ProcessedFrame { @@ -477,34 +445,10 @@ impl AVAssetReaderDecoder { keyframe_index_arc.clone(), )?; - let mut decoders = vec![primary_instance]; - - let initial_positions = pool_manager.positions(); - for pos in initial_positions.iter().skip(1) { - let start_time = pos.position_secs; - match DecoderInstance::new( - path.clone(), - tokio_handle.clone(), - start_time, - keyframe_index_arc.clone(), - ) { - Ok(instance) => { - decoders.push(instance); - tracing::info!( - position_secs = start_time, - decoder_index = decoders.len() - 1, - "Created additional decoder instance for multi-position pool" - ); - } - Err(e) => { - tracing::warn!( - position_secs = start_time, - error = %e, - "Failed to create additional decoder instance, continuing with fewer decoders" - ); - } - } - } + // Linear playback only needs one reader. Additional readers retain + // AVFoundation state and decoded buffers, so create them only when a + // non-sequential request would otherwise reposition an active reader. + let decoders = vec![primary_instance]; let total_frames_estimate = (duration_secs * fps as f64).ceil() as u32; @@ -543,6 +487,46 @@ impl AVAssetReaderDecoder { let decoder_idx = best_id.min(decoder_count.saturating_sub(1)); + let max_decoder_count = self + .pool_manager + .optimal_pool_size() + .min(self.pool_manager.positions().len()); + if should_grow_decoder_pool(is_scrubbing, needs_reset, decoder_count, max_decoder_count) { + let (path, tokio_handle, keyframe_index) = { + let config = self.pool_manager.config(); + ( + config.path.clone(), + config.tokio_handle.clone(), + config.keyframe_index.clone(), + ) + }; + + match DecoderInstance::new(path, tokio_handle, requested_time, keyframe_index) { + Ok(instance) => { + let decoder_idx = self.decoders.len(); + let position = instance.current_position(); + self.decoders.push(instance); + self.pool_manager + .update_decoder_position(decoder_idx, position); + self.active_decoder_idx = decoder_idx; + tracing::info!( + decoder_index = decoder_idx, + requested_time = requested_time, + decoder_count = self.decoders.len(), + "Created decoder instance on demand for non-sequential access" + ); + return (decoder_idx, true); + } + Err(error) => { + tracing::warn!( + requested_time = requested_time, + error = %error, + "Failed to create decoder on demand; reusing an existing decoder" + ); + } + } + } + if needs_reset && decoder_idx < self.decoders.len() { self.decoders[decoder_idx].reset(requested_time); self.pool_manager.update_decoder_position( @@ -1148,6 +1132,15 @@ impl AVAssetReaderDecoder { } } +fn should_grow_decoder_pool( + is_scrubbing: bool, + needs_reset: bool, + decoder_count: usize, + max_decoder_count: usize, +) -> bool { + is_scrubbing && needs_reset && decoder_count < max_decoder_count +} + #[cfg(test)] mod tests { use super::*; @@ -1164,6 +1157,15 @@ mod tests { assert!(frame_cache_needs_eviction(1, usize::MAX, 1)); } + #[test] + fn decoder_pool_grows_only_for_unserved_non_sequential_access() { + assert!(should_grow_decoder_pool(true, true, 1, 5)); + assert!(!should_grow_decoder_pool(false, true, 1, 5)); + assert!(!should_grow_decoder_pool(true, false, 1, 5)); + assert!(!should_grow_decoder_pool(true, true, 5, 5)); + assert!(!should_grow_decoder_pool(true, true, 1, 1)); + } + #[test] fn test_decoder_health_new() { let health = DecoderHealth::new(); diff --git a/crates/rendering/src/decoder/mod.rs b/crates/rendering/src/decoder/mod.rs index 8cf323adc8e..13a2064761f 100644 --- a/crates/rendering/src/decoder/mod.rs +++ b/crates/rendering/src/decoder/mod.rs @@ -2,12 +2,17 @@ use ::ffmpeg::Rational; use std::{ fmt, path::PathBuf, - sync::{Arc, mpsc}, + sync::{Arc, Weak, mpsc}, time::Duration, }; use tokio::sync::oneshot; use tracing::info; +#[cfg(target_os = "macos")] +use cidre::{arc::R, cv}; +#[cfg(target_os = "macos")] +use std::sync::{Mutex, OnceLock}; + #[cfg(target_os = "macos")] mod avassetreader; mod ffmpeg; @@ -150,10 +155,107 @@ pub struct DecodedFrame { format: PixelFormat, y_stride: u32, uv_stride: u32, + #[cfg(target_os = "macos")] + image_buf_backing: Option>, #[cfg(target_os = "windows")] d3d11_texture_backing: Option>, } +#[cfg(target_os = "macos")] +struct SendableImageBuf { + image_buf: Mutex>>, + raw_data: OnceLock>, +} + +#[cfg(target_os = "macos")] +// SAFETY: all CVPixelBuffer access is serialized by `image_buf`, and the Core +// Foundation object remains retained for the lifetime of this wrapper. +unsafe impl Send for SendableImageBuf {} +#[cfg(target_os = "macos")] +// SAFETY: see the `Send` implementation above. +unsafe impl Sync for SendableImageBuf {} + +#[cfg(target_os = "macos")] +impl SendableImageBuf { + fn new(image_buf: R) -> Self { + Self { + image_buf: Mutex::new(Some(image_buf)), + raw_data: OnceLock::new(), + } + } + + fn with_image_buf(&self, f: impl FnOnce(&cv::ImageBuf) -> T) -> Option { + let image_buf = self + .image_buf + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + image_buf.as_deref().map(f) + } + + fn raw_data(&self) -> &[u8] { + self.raw_data.get_or_init(|| { + let mut image_buf_guard = self + .image_buf + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let Some(image_buf) = image_buf_guard.as_mut() else { + return Vec::new(); + }; + + unsafe { + image_buf + .lock_base_addr(cv::pixel_buffer::LockFlags::READ_ONLY) + .result() + .unwrap(); + } + + let y_size = image_buf.plane_bytes_per_row(0) * image_buf.plane_height(0); + let uv_size = image_buf.plane_bytes_per_row(1) * image_buf.plane_height(1); + let y_plane = + unsafe { std::slice::from_raw_parts(image_buf.plane_base_address(0), y_size) }; + let uv_plane = + unsafe { std::slice::from_raw_parts(image_buf.plane_base_address(1), uv_size) }; + + let mut data = Vec::with_capacity(y_size + uv_size); + data.extend_from_slice(y_plane); + data.extend_from_slice(uv_plane); + + unsafe { + image_buf.unlock_lock_base_addr(cv::pixel_buffer::LockFlags::READ_ONLY); + } + + // CPU consumers no longer need the IOSurface after its planes have + // been copied. Releasing it keeps cache accounting at one frame + // instead of retaining both the CVPixelBuffer and its materialized + // bytes for the lifetime of the cache entry. + *image_buf_guard = None; + + data + }) + } +} + +#[derive(Clone)] +pub(crate) struct DecodedFrameStorageIdentity { + data: Weak>, + width: u32, + height: u32, + format: PixelFormat, + y_stride: u32, + uv_stride: u32, +} + +impl DecodedFrameStorageIdentity { + pub(crate) fn matches(&self, other: &Self) -> bool { + self.data.ptr_eq(&other.data) + && self.width == other.width + && self.height == other.height + && self.format == other.format + && self.y_stride == other.y_stride + && self.uv_stride == other.uv_stride + } +} + impl fmt::Debug for DecodedFrame { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DecodedFrame") @@ -176,6 +278,8 @@ impl DecodedFrame { format: PixelFormat::Rgba, y_stride: width * 4, uv_stride: 0, + #[cfg(target_os = "macos")] + image_buf_backing: None, #[cfg(target_os = "windows")] d3d11_texture_backing: None, } @@ -189,6 +293,8 @@ impl DecodedFrame { format: PixelFormat::Rgba, y_stride: width * 4, uv_stride: 0, + #[cfg(target_os = "macos")] + image_buf_backing: None, #[cfg(target_os = "windows")] d3d11_texture_backing: None, } @@ -202,6 +308,8 @@ impl DecodedFrame { format: PixelFormat::Nv12, y_stride, uv_stride, + #[cfg(target_os = "macos")] + image_buf_backing: None, #[cfg(target_os = "windows")] d3d11_texture_backing: None, } @@ -221,11 +329,32 @@ impl DecodedFrame { format: PixelFormat::Nv12, y_stride, uv_stride, + #[cfg(target_os = "macos")] + image_buf_backing: None, #[cfg(target_os = "windows")] d3d11_texture_backing: None, } } + #[cfg(target_os = "macos")] + pub(crate) fn new_nv12_with_image_buf( + width: u32, + height: u32, + y_stride: u32, + uv_stride: u32, + image_buf: R, + ) -> Self { + Self { + data: Arc::new(Vec::new()), + width, + height, + format: PixelFormat::Nv12, + y_stride, + uv_stride, + image_buf_backing: Some(Arc::new(SendableImageBuf::new(image_buf))), + } + } + pub fn new_yuv420p( data: Vec, width: u32, @@ -240,6 +369,8 @@ impl DecodedFrame { format: PixelFormat::Yuv420p, y_stride, uv_stride, + #[cfg(target_os = "macos")] + image_buf_backing: None, #[cfg(target_os = "windows")] d3d11_texture_backing: None, } @@ -259,6 +390,8 @@ impl DecodedFrame { format: PixelFormat::Yuv420p, y_stride, uv_stride, + #[cfg(target_os = "macos")] + image_buf_backing: None, #[cfg(target_os = "windows")] d3d11_texture_backing: None, } @@ -273,6 +406,8 @@ impl DecodedFrame { format: PixelFormat::Nv12, y_stride: width, uv_stride: width, + #[cfg(target_os = "macos")] + image_buf_backing: None, d3d11_texture_backing: Some(Arc::new(SendableD3D11Texture::new(texture))), } } @@ -291,6 +426,8 @@ impl DecodedFrame { format: PixelFormat::Nv12, y_stride: width, uv_stride: width, + #[cfg(target_os = "macos")] + image_buf_backing: None, d3d11_texture_backing: Some(Arc::new(SendableD3D11Texture::new_with_handle( texture, shared_handle, @@ -314,6 +451,8 @@ impl DecodedFrame { format: PixelFormat::Nv12, y_stride: width, uv_stride: width, + #[cfg(target_os = "macos")] + image_buf_backing: None, d3d11_texture_backing: Some(Arc::new(SendableD3D11Texture::new_with_yuv_handles( texture, shared_handle, @@ -351,9 +490,23 @@ impl DecodedFrame { } pub fn data(&self) -> &[u8] { + #[cfg(target_os = "macos")] + if self.data.is_empty() + && let Some(backing) = &self.image_buf_backing + { + return backing.raw_data(); + } + &self.data } + #[cfg(target_os = "macos")] + pub(crate) fn with_image_buf(&self, f: impl FnOnce(&cv::ImageBuf) -> T) -> Option { + self.image_buf_backing + .as_ref() + .and_then(|backing| backing.with_image_buf(f)) + } + pub fn width(&self) -> u32 { self.width } @@ -366,6 +519,34 @@ impl DecodedFrame { self.format } + pub fn byte_len(&self) -> usize { + if !self.data.is_empty() { + return self.data.len(); + } + + let y_bytes = (self.y_stride as usize).saturating_mul(self.height as usize); + let uv_bytes = (self.uv_stride as usize).saturating_mul((self.height / 2) as usize); + + match self.format { + PixelFormat::Rgba => (self.width as usize) + .saturating_mul(self.height as usize) + .saturating_mul(4), + PixelFormat::Nv12 => y_bytes.saturating_add(uv_bytes), + PixelFormat::Yuv420p => y_bytes.saturating_add(uv_bytes.saturating_mul(2)), + } + } + + pub(crate) fn storage_identity(&self) -> DecodedFrameStorageIdentity { + DecodedFrameStorageIdentity { + data: Arc::downgrade(&self.data), + width: self.width, + height: self.height, + format: self.format, + y_stride: self.y_stride, + uv_stride: self.uv_stride, + } + } + pub fn y_plane(&self) -> Option<&[u8]> { match self.format { PixelFormat::Nv12 | PixelFormat::Yuv420p => { @@ -373,7 +554,7 @@ impl DecodedFrame { .y_stride .checked_mul(self.height) .and_then(|v| usize::try_from(v).ok())?; - self.data.get(..y_size) + self.data().get(..y_size) } PixelFormat::Rgba => None, } @@ -386,7 +567,7 @@ impl DecodedFrame { .y_stride .checked_mul(self.height) .and_then(|v| usize::try_from(v).ok())?; - self.data.get(y_size..) + self.data().get(y_size..) } PixelFormat::Yuv420p | PixelFormat::Rgba => None, } @@ -404,7 +585,7 @@ impl DecodedFrame { .checked_mul(self.height / 2) .and_then(|v| usize::try_from(v).ok())?; let u_end = y_size.checked_add(u_size)?; - self.data.get(y_size..u_end) + self.data().get(y_size..u_end) } _ => None, } @@ -422,7 +603,7 @@ impl DecodedFrame { .checked_mul(self.height / 2) .and_then(|v| usize::try_from(v).ok())?; let v_start = y_size.checked_add(u_size)?; - self.data.get(v_start..) + self.data().get(v_start..) } _ => None, } @@ -896,6 +1077,71 @@ pub async fn spawn_decoder( mod tests { use super::*; + #[test] + fn decoded_frame_storage_identity_matches_clones() { + let frame = DecodedFrame::new_nv12(vec![0; 6], 2, 2, 2, 2); + let cloned = frame.clone(); + + assert!(frame.storage_identity().matches(&cloned.storage_identity())); + } + + #[test] + fn decoded_frame_storage_identity_rejects_distinct_or_reinterpreted_frames() { + let data = Arc::new(vec![0; 16]); + let original = DecodedFrame::new_with_arc(Arc::clone(&data), 2, 2); + let reinterpreted = DecodedFrame::new_with_arc(Arc::clone(&data), 1, 4); + let distinct = DecodedFrame::new(vec![0; 16], 2, 2); + + assert!( + !original + .storage_identity() + .matches(&reinterpreted.storage_identity()) + ); + assert!( + !original + .storage_identity() + .matches(&distinct.storage_identity()) + ); + } + + #[cfg(target_os = "macos")] + #[test] + fn iosurface_backed_nv12_materializes_raw_planes_only_on_demand() { + let mut image_buf = cv::ImageBuf::new(4, 2, cv::PixelFormat::_420V, None).unwrap(); + + unsafe { + image_buf + .lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) + .result() + .unwrap(); + } + + let y_stride = image_buf.plane_bytes_per_row(0); + let uv_stride = image_buf.plane_bytes_per_row(1); + let y_size = y_stride * image_buf.plane_height(0); + let uv_size = uv_stride * image_buf.plane_height(1); + unsafe { + std::ptr::write_bytes(image_buf.plane_base_address(0).cast_mut(), 0x11, y_size); + std::ptr::write_bytes(image_buf.plane_base_address(1).cast_mut(), 0x22, uv_size); + image_buf.unlock_lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT); + } + + let frame = DecodedFrame::new_nv12_with_image_buf( + 4, + 2, + y_stride as u32, + uv_stride as u32, + image_buf, + ); + + assert_eq!(frame.byte_len(), y_size + uv_size); + assert!(frame.with_image_buf(|_| ()).is_some()); + assert_eq!(frame.y_plane().unwrap(), vec![0x11; y_size]); + assert_eq!(frame.uv_plane().unwrap(), vec![0x22; uv_size]); + assert_eq!(frame.data().len(), y_size + uv_size); + assert!(frame.with_image_buf(|_| ()).is_none()); + } + #[test] fn pts_to_frame_maps_real_presentation_time_to_index() { let tb = Rational::new(1, 600); diff --git a/crates/rendering/src/layers/camera.rs b/crates/rendering/src/layers/camera.rs index e2be7f5dd55..4558de2f914 100644 --- a/crates/rendering/src/layers/camera.rs +++ b/crates/rendering/src/layers/camera.rs @@ -5,6 +5,7 @@ use wgpu::util::DeviceExt; use crate::{ CompositeVideoFrameUniforms, DecodedFrame, PixelFormat, composite_frame::CompositeVideoFramePipeline, + decoder::DecodedFrameStorageIdentity, yuv_converter::{YuvConverterPipelines, YuvToRgbaConverter}, }; @@ -17,6 +18,7 @@ pub struct CameraLayer { pipeline: Arc, hidden: bool, last_recording_time: Option, + last_frame_storage: Option, yuv_converter: YuvToRgbaConverter, blur_bind_group: Option, blur_active: bool, @@ -75,6 +77,7 @@ impl CameraLayer { pipeline: composite_pipeline, hidden: false, last_recording_time: None, + last_frame_storage: None, yuv_converter, blur_bind_group: None, blur_active: false, @@ -105,12 +108,16 @@ impl CameraLayer { return; }; - let frame_data_bytes = camera_frame.data(); let format = camera_frame.format(); + let frame_storage = camera_frame.storage_identity(); let is_same_frame = self - .last_recording_time - .is_some_and(|last| (last - recording_time).abs() < 0.001); + .last_frame_storage + .as_ref() + .is_some_and(|last| last.matches(&frame_storage)) + || self + .last_recording_time + .is_some_and(|last| (last - recording_time).abs() < 0.001); if !is_same_frame { let next_texture = 1 - self.current_texture; @@ -136,6 +143,7 @@ impl CameraLayer { match format { PixelFormat::Rgba => { + let frame_data_bytes = camera_frame.data(); let src_bytes_per_row = frame_size.x * 4; queue.write_texture( @@ -198,6 +206,7 @@ impl CameraLayer { frame_size, ); self.last_recording_time = Some(recording_time); + self.last_frame_storage = Some(frame_storage); self.current_texture = next_texture; return; } @@ -212,7 +221,20 @@ impl CameraLayer { } } - if let (Some(y_data), Some(uv_data)) = + #[cfg(target_os = "macos")] + let iosurface_converted = camera_frame + .with_image_buf(|image_buf| { + self.yuv_converter + .convert_nv12_from_iosurface(device, queue, image_buf) + .is_ok() + }) + .unwrap_or(false); + #[cfg(not(target_os = "macos"))] + let iosurface_converted = false; + + if iosurface_converted && self.yuv_converter.output_texture().is_some() { + self.copy_from_yuv_output(device, queue, next_texture, frame_size); + } else if let (Some(y_data), Some(uv_data)) = (camera_frame.y_plane(), camera_frame.uv_plane()) && self .yuv_converter @@ -266,7 +288,10 @@ impl CameraLayer { } self.last_recording_time = Some(recording_time); + self.last_frame_storage = Some(frame_storage); self.current_texture = next_texture; + } else { + self.last_recording_time = Some(recording_time); } } @@ -360,10 +385,15 @@ impl CameraLayer { }; let format = camera_frame.format(); + let frame_storage = camera_frame.storage_identity(); let is_same_frame = self - .last_recording_time - .is_some_and(|last| (last - recording_time).abs() < 0.001); + .last_frame_storage + .as_ref() + .is_some_and(|last| last.matches(&frame_storage)) + || self + .last_recording_time + .is_some_and(|last| (last - recording_time).abs() < 0.001); if !is_same_frame { let next_texture = 1 - self.current_texture; @@ -422,7 +452,20 @@ impl CameraLayer { return; } - if let (Some(y_data), Some(uv_data)) = + #[cfg(target_os = "macos")] + let iosurface_converted = camera_frame + .with_image_buf(|image_buf| { + self.yuv_converter + .convert_nv12_from_iosurface_to_encoder(device, encoder, image_buf) + .is_ok() + }) + .unwrap_or(false); + #[cfg(not(target_os = "macos"))] + let iosurface_converted = false; + + if iosurface_converted && self.yuv_converter.output_texture().is_some() { + self.copy_from_yuv_output_to_encoder(encoder, next_texture, frame_size); + } else if let (Some(y_data), Some(uv_data)) = (camera_frame.y_plane(), camera_frame.uv_plane()) && self .yuv_converter @@ -478,7 +521,10 @@ impl CameraLayer { } self.last_recording_time = Some(recording_time); + self.last_frame_storage = Some(frame_storage); self.current_texture = next_texture; + } else { + self.last_recording_time = Some(recording_time); } } diff --git a/crates/rendering/src/layers/display.rs b/crates/rendering/src/layers/display.rs index d67a5dbd55e..d4de70fa377 100644 --- a/crates/rendering/src/layers/display.rs +++ b/crates/rendering/src/layers/display.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use crate::{ DecodedSegmentFrames, PixelFormat, composite_frame::{CompositeVideoFramePipeline, CompositeVideoFrameUniforms}, + decoder::DecodedFrameStorageIdentity, yuv_converter::{YuvConverterPipelines, YuvToRgbaConverter}, }; @@ -68,6 +69,7 @@ pub struct DisplayLayer { pipeline: std::sync::Arc, bind_groups: [Option; 2], last_recording_time: Option, + last_frame_storage: Option, yuv_converter: YuvToRgbaConverter, pending_copy: Option, prefer_cpu_conversion: bool, @@ -120,6 +122,7 @@ impl DisplayLayer { pipeline: composite_pipeline, bind_groups: [bind_group_0, bind_group_1], last_recording_time: None, + last_frame_storage: None, yuv_converter, pending_copy: None, prefer_cpu_conversion, @@ -194,17 +197,21 @@ impl DisplayLayer { return (false, frame_size.x, frame_size.y); }; - let frame_data = screen_frame.data(); let actual_width = screen_frame.width(); let actual_height = screen_frame.height(); let source_size = XY::new(actual_width, actual_height); let uniforms = uniforms_for_source_frame(uniforms, frame_size, source_size); let format = screen_frame.format(); let current_recording_time = segment_frames.recording_time; + let frame_storage = screen_frame.storage_identity(); let skipped = self - .last_recording_time - .is_some_and(|last| (last - current_recording_time).abs() < 0.001); + .last_frame_storage + .as_ref() + .is_some_and(|last| last.matches(&frame_storage)) + || self + .last_recording_time + .is_some_and(|last| (last - current_recording_time).abs() < 0.001); let mut frame_uploaded = false; @@ -232,6 +239,7 @@ impl DisplayLayer { frame_uploaded = match format { PixelFormat::Rgba => { + let frame_data = screen_frame.data(); let src_bytes_per_row = source_size.x * 4; queue.write_texture( @@ -258,7 +266,22 @@ impl DisplayLayer { PixelFormat::Nv12 => { #[cfg(target_os = "macos")] if !self.prefer_cpu_conversion { - if let (Some(y_data), Some(uv_data)) = + let iosurface_converted = screen_frame + .with_image_buf(|image_buf| { + self.yuv_converter + .convert_nv12_from_iosurface(device, queue, image_buf) + .is_ok() + }) + .unwrap_or(false); + + if iosurface_converted && self.yuv_converter.output_texture().is_some() { + self.pending_copy = Some(PendingTextureCopy { + width: source_size.x, + height: source_size.y, + dst_texture_index: next_texture, + }); + true + } else if let (Some(y_data), Some(uv_data)) = (screen_frame.y_plane(), screen_frame.uv_plane()) { let y_stride = screen_frame.y_stride(); @@ -523,6 +546,7 @@ impl DisplayLayer { if frame_uploaded { self.last_recording_time = Some(current_recording_time); + self.last_frame_storage = Some(frame_storage); self.current_texture = next_texture; self.has_valid_frame = true; } else { @@ -569,10 +593,15 @@ impl DisplayLayer { let uniforms = uniforms_for_source_frame(uniforms, frame_size, source_size); let format = screen_frame.format(); let current_recording_time = segment_frames.recording_time; + let frame_storage = screen_frame.storage_identity(); let skipped = self - .last_recording_time - .is_some_and(|last| (last - current_recording_time).abs() < 0.001); + .last_frame_storage + .as_ref() + .is_some_and(|last| last.matches(&frame_storage)) + || self + .last_recording_time + .is_some_and(|last| (last - current_recording_time).abs() < 0.001); let mut frame_uploaded = false; @@ -748,7 +777,27 @@ impl DisplayLayer { #[cfg(not(target_os = "windows"))] if !self.prefer_cpu_conversion { - if let (Some(y_data), Some(uv_data)) = + #[cfg(target_os = "macos")] + let iosurface_converted = screen_frame + .with_image_buf(|image_buf| { + self.yuv_converter + .convert_nv12_from_iosurface_to_encoder( + device, encoder, image_buf, + ) + .is_ok() + }) + .unwrap_or(false); + #[cfg(not(target_os = "macos"))] + let iosurface_converted = false; + + if iosurface_converted && self.yuv_converter.output_texture().is_some() { + self.pending_copy = Some(PendingTextureCopy { + width: source_size.x, + height: source_size.y, + dst_texture_index: next_texture, + }); + true + } else if let (Some(y_data), Some(uv_data)) = (screen_frame.y_plane(), screen_frame.uv_plane()) { let y_stride = screen_frame.y_stride(); @@ -876,6 +925,7 @@ impl DisplayLayer { if frame_uploaded { self.last_recording_time = Some(current_recording_time); + self.last_frame_storage = Some(frame_storage); self.current_texture = next_texture; self.has_valid_frame = true; } else { diff --git a/crates/rendering/src/yuv_converter.rs b/crates/rendering/src/yuv_converter.rs index c1750dbbfbf..969e21208ee 100644 --- a/crates/rendering/src/yuv_converter.rs +++ b/crates/rendering/src/yuv_converter.rs @@ -868,6 +868,23 @@ impl YuvToRgbaConverter { device: &wgpu::Device, queue: &wgpu::Queue, image_buf: &cv::ImageBuf, + ) -> Result<&wgpu::TextureView, YuvConversionError> { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("NV12 IOSurface Conversion Encoder"), + }); + + self.convert_nv12_from_iosurface_to_encoder(device, &mut encoder, image_buf)?; + queue.submit(std::iter::once(encoder.finish())); + + Ok(self.current_output_view()) + } + + #[cfg(target_os = "macos")] + pub fn convert_nv12_from_iosurface_to_encoder( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + image_buf: &cv::ImageBuf, ) -> Result<&wgpu::TextureView, YuvConversionError> { if self.iosurface_cache.is_none() { return Err(IOSurfaceTextureError::NoMetalDevice.into()); @@ -929,13 +946,9 @@ impl YuvToRgbaConverter { ], }); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("NV12 IOSurface Conversion Encoder"), - }); - { let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("NV12 IOSurface Conversion Pass"), + label: Some("NV12 IOSurface Conversion Pass (Batched)"), ..Default::default() }); compute_pass.set_pipeline(&self.pipelines.nv12_pipeline); @@ -943,8 +956,6 @@ impl YuvToRgbaConverter { compute_pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1); } - queue.submit(std::iter::once(encoder.finish())); - Ok(self.current_output_view()) } From c9aa93e830c17c28805cbf8f43c5297941b6deaa Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:03:00 +0100 Subject: [PATCH 23/31] perf: reduce Electron editor resource usage --- apps/desktop/src-backend/src/editor_window.rs | 31 ++- apps/desktop/src-backend/src/frame_ws.rs | 154 ++++++++++++++- apps/desktop/src-backend/src/lib.rs | 4 +- apps/desktop/src/utils/socket.ts | 17 ++ crates/editor/src/editor.rs | 178 ++++++++++++++++-- crates/editor/src/editor_instance.rs | 43 ++++- crates/editor/src/playback.rs | 21 +-- crates/rendering/src/frame_pipeline.rs | 32 ++-- crates/rendering/src/lib.rs | 6 + 9 files changed, 425 insertions(+), 61 deletions(-) diff --git a/apps/desktop/src-backend/src/editor_window.rs b/apps/desktop/src-backend/src/editor_window.rs index 1330bc1eef5..2f21e6864ff 100644 --- a/apps/desktop/src-backend/src/editor_window.rs +++ b/apps/desktop/src-backend/src/editor_window.rs @@ -8,7 +8,7 @@ use cap_rendering::GpuOutputFormat; use crate::{ FrameLayoutEvent, create_editor_instance_impl, - frame_ws::{WSFrame, WSFrameFormat, create_watch_frame_ws}, + frame_ws::{WSFrame, WSFrameFormat, create_watch_frame_ws_with_output_capability}, }; /// Forwards rendered frames to the preview websocket and mirrors each frame's @@ -72,10 +72,21 @@ pub struct PendingEditorInstances(Arc>>) async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult { let (frame_tx, frame_rx) = watch::channel(None); - - let (ws_port, ws_shutdown_token) = create_watch_frame_ws(frame_rx, Default::default()).await; - let (inner, render_frame_event_id) = - create_editor_instance_impl(&app, path, make_frame_callback(app.clone(), frame_tx)).await?; + let nv12_output_available = Arc::new(std::sync::atomic::AtomicBool::new(false)); + + let (ws_port, ws_shutdown_token) = create_watch_frame_ws_with_output_capability( + frame_rx, + Default::default(), + nv12_output_available.clone(), + ) + .await; + let (inner, render_frame_event_id) = create_editor_instance_impl( + &app, + path, + make_frame_callback(app.clone(), frame_tx), + nv12_output_available, + ) + .await?; Ok(Arc::new(EditorInstance { inner, @@ -360,14 +371,20 @@ impl EditorInstances { } let (frame_tx, frame_rx) = watch::channel(None); + let nv12_output_available = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let (ws_port, ws_shutdown_token) = - create_watch_frame_ws(frame_rx, Default::default()).await; + let (ws_port, ws_shutdown_token) = create_watch_frame_ws_with_output_capability( + frame_rx, + Default::default(), + nv12_output_available.clone(), + ) + .await; let app_handle = window.app_handle().clone(); let (inner, render_frame_event_id) = create_editor_instance_impl( window.app_handle(), path, make_frame_callback(app_handle.clone(), frame_tx), + nv12_output_available, ) .await?; diff --git a/apps/desktop/src-backend/src/frame_ws.rs b/apps/desktop/src-backend/src/frame_ws.rs index 4c78a129620..aba472cd5f3 100644 --- a/apps/desktop/src-backend/src/frame_ws.rs +++ b/apps/desktop/src-backend/src/frame_ws.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; use std::time::Instant; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::{broadcast, watch}; @@ -143,6 +143,68 @@ struct WsFrameStatsWindow { max_created_to_sent_ns: u64, } +#[derive(Default)] +struct OutputCapabilityCounts { + subscribers: usize, + nv12_capable_subscribers: usize, +} + +struct SubscriberCapabilityGuard { + counts: Arc>, + nv12_output_available: Arc, + nv12_capable: bool, +} + +impl SubscriberCapabilityGuard { + fn new( + counts: Arc>, + nv12_output_available: Arc, + ) -> Self { + let guard = Self { + counts, + nv12_output_available, + nv12_capable: false, + }; + guard.update_counts(|counts| counts.subscribers += 1); + guard + } + + fn update_counts(&self, update: impl FnOnce(&mut OutputCapabilityCounts)) { + let mut counts = self.counts.lock().unwrap_or_else(|e| e.into_inner()); + update(&mut counts); + self.nv12_output_available.store( + counts.subscribers > 0 && counts.nv12_capable_subscribers == counts.subscribers, + Ordering::Release, + ); + } + + fn set_nv12_capable(&mut self, capable: bool) { + if capable == self.nv12_capable { + return; + } + self.update_counts(|counts| { + if capable { + counts.nv12_capable_subscribers += 1; + } else { + counts.nv12_capable_subscribers = counts.nv12_capable_subscribers.saturating_sub(1); + } + }); + self.nv12_capable = capable; + } +} + +impl Drop for SubscriberCapabilityGuard { + fn drop(&mut self) { + let nv12_capable = self.nv12_capable; + self.update_counts(|counts| { + counts.subscribers = counts.subscribers.saturating_sub(1); + if nv12_capable { + counts.nv12_capable_subscribers = counts.nv12_capable_subscribers.saturating_sub(1); + } + }); + } +} + struct SubscriberCountGuard { subscribers: Arc, instant_subscribers: Option>, @@ -175,7 +237,15 @@ pub async fn create_watch_frame_ws( frame_rx: watch::Receiver>>, subscribers: Arc, ) -> (u16, CancellationToken) { - create_watch_frame_ws_inner(frame_rx, subscribers, None).await + create_watch_frame_ws_inner(frame_rx, subscribers, None, None).await +} + +pub async fn create_watch_frame_ws_with_output_capability( + frame_rx: watch::Receiver>>, + subscribers: Arc, + nv12_output_available: Arc, +) -> (u16, CancellationToken) { + create_watch_frame_ws_inner(frame_rx, subscribers, None, Some(nv12_output_available)).await } pub async fn create_watch_frame_ws_with_instant_tracking( @@ -183,13 +253,14 @@ pub async fn create_watch_frame_ws_with_instant_tracking( subscribers: Arc, instant_subscribers: Arc, ) -> (u16, CancellationToken) { - create_watch_frame_ws_inner(frame_rx, subscribers, Some(instant_subscribers)).await + create_watch_frame_ws_inner(frame_rx, subscribers, Some(instant_subscribers), None).await } async fn create_watch_frame_ws_inner( frame_rx: watch::Receiver>>, subscribers: Arc, instant_subscribers: Option>, + nv12_output_available: Option>, ) -> (u16, CancellationToken) { use axum::{ extract::{ @@ -200,20 +271,41 @@ async fn create_watch_frame_ws_inner( routing::get, }; + let output_capability_counts = nv12_output_available + .as_ref() + .map(|_| Arc::new(Mutex::new(OutputCapabilityCounts::default()))); + type RouterState = ( watch::Receiver>>, Arc, Option>, + Option>, + Option>>, ); #[axum::debug_handler] async fn ws_handler( ws: WebSocketUpgrade, Query(query): Query, - State((state, subscribers, instant_subscribers)): State, + State(( + state, + subscribers, + instant_subscribers, + nv12_output_available, + output_capability_counts, + )): State, ) -> impl IntoResponse { let instant_subscribers = query.instant.then_some(instant_subscribers).flatten(); - ws.on_upgrade(move |socket| handle_socket(socket, state, subscribers, instant_subscribers)) + ws.on_upgrade(move |socket| { + handle_socket( + socket, + state, + subscribers, + instant_subscribers, + nv12_output_available, + output_capability_counts, + ) + }) } async fn handle_socket( @@ -221,6 +313,8 @@ async fn create_watch_frame_ws_inner( mut camera_rx: watch::Receiver>>, subscribers: Arc, instant_subscribers: Option>, + nv12_output_available: Option>, + output_capability_counts: Option>>, ) { tracing::info!("Socket connection established"); let now = std::time::Instant::now(); @@ -234,6 +328,9 @@ async fn create_watch_frame_ws_inner( subscribers, instant_subscribers, }; + let mut subscriber_capability = nv12_output_available + .zip(output_capability_counts) + .map(|(available, counts)| SubscriberCapabilityGuard::new(counts, available)); { let packed = { @@ -274,6 +371,19 @@ async fn create_watch_frame_ws_inner( tracing::info!("WebSocket closed"); break; } + Some(Ok(Message::Text(message))) => { + if let Some(subscriber_capability) = &mut subscriber_capability { + match message.as_str() { + "cap-preview:nv12-webgpu" => { + subscriber_capability.set_nv12_capable(true); + } + "cap-preview:rgba" => { + subscriber_capability.set_nv12_capable(false); + } + _ => {} + } + } + } Some(Ok(_)) => {} Some(Err(e)) => { if is_normal_socket_disconnect(&e) { @@ -357,6 +467,8 @@ async fn create_watch_frame_ws_inner( frame_rx, subscribers, instant_subscribers, + nv12_output_available, + output_capability_counts, )); let cancel_token = CancellationToken::new(); @@ -562,4 +674,34 @@ mod tests { assert_eq!(subscribers.load(Ordering::Acquire), 0); assert_eq!(instant_subscribers.load(Ordering::Acquire), 0); } + + #[test] + fn last_subscriber_resets_output_capability() { + let nv12_output_available = Arc::new(AtomicBool::new(true)); + let counts = Arc::new(Mutex::new(OutputCapabilityCounts::default())); + let mut subscriber = SubscriberCapabilityGuard::new(counts, nv12_output_available.clone()); + subscriber.set_nv12_capable(true); + + drop(subscriber); + + assert!(!nv12_output_available.load(Ordering::Acquire)); + } + + #[test] + fn nv12_requires_every_active_subscriber_to_be_capable() { + let counts = Arc::new(Mutex::new(OutputCapabilityCounts::default())); + let nv12_output_available = Arc::new(AtomicBool::new(false)); + let mut capable = + SubscriberCapabilityGuard::new(counts.clone(), nv12_output_available.clone()); + let mut fallback = SubscriberCapabilityGuard::new(counts, nv12_output_available.clone()); + + capable.set_nv12_capable(true); + assert!(!nv12_output_available.load(Ordering::Acquire)); + fallback.set_nv12_capable(true); + assert!(nv12_output_available.load(Ordering::Acquire)); + fallback.set_nv12_capable(false); + assert!(!nv12_output_available.load(Ordering::Acquire)); + drop(fallback); + assert!(nv12_output_available.load(Ordering::Acquire)); + } } diff --git a/apps/desktop/src-backend/src/lib.rs b/apps/desktop/src-backend/src/lib.rs index edce9dbfe79..8cdf40f56a0 100644 --- a/apps/desktop/src-backend/src/lib.rs +++ b/apps/desktop/src-backend/src/lib.rs @@ -6465,6 +6465,7 @@ async fn create_editor_instance_impl( app: &AppHandle, path: PathBuf, frame_cb: cap_editor::EditorFrameCallback, + nv12_output_available: Arc, ) -> Result<(Arc, cap_desktop_runtime::EventId), String> { let app = app.clone(); @@ -6485,13 +6486,14 @@ async fn create_editor_instance_impl( let instance = { let app = app.clone(); - EditorInstance::new( + EditorInstance::new_with_output_capability( path, move |state| { let _ = EditorStateChanged::new(state).emit(&app); }, frame_cb, shared_device, + nv12_output_available, ) .await? }; diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts index 1ce9ac75c81..98bc1c5375f 100644 --- a/apps/desktop/src/utils/socket.ts +++ b/apps/desktop/src/utils/socket.ts @@ -323,6 +323,7 @@ export function createImageDataWS( let mainThreadWebGPU: WebGPURenderer | null = null; let mainThreadWebGPUInitializing = false; + let nv12WebGPUAvailable = false; let pendingNv12Frame: { buffer: ArrayBuffer; receivedAt: number } | null = null; let pendingRgbaFrame: { buffer: ArrayBuffer; receivedAt: number } | null = @@ -449,6 +450,13 @@ export function createImageDataWS( setIsConnected(false); } + function sendOutputCapability() { + if (ws.readyState !== WebSocket.OPEN) return; + ws.send( + nv12WebGPUAvailable ? "cap-preview:nv12-webgpu" : "cap-preview:rgba", + ); + } + function renderPendingNv12Frame() { if (!pendingNv12Frame || !mainThreadWebGPU || !directCanvas) return; @@ -806,6 +814,8 @@ export function createImageDataWS( const isNewCanvas = directCanvas !== canvas; if (isNewCanvas && directCanvas) { + nv12WebGPUAvailable = false; + sendOutputCapability(); if (mainThreadWebGPU) { disposeWebGPU(mainThreadWebGPU); mainThreadWebGPU = null; @@ -838,7 +848,9 @@ export function createImageDataWS( } mainThreadWebGPU = renderer; + nv12WebGPUAvailable = true; mainThreadWebGPUInitializing = false; + sendOutputCapability(); if (pendingNv12Frame && directCanvas) { renderPendingNv12Frame(); } @@ -854,6 +866,8 @@ export function createImageDataWS( } mainThreadWebGPUInitializing = false; + nv12WebGPUAvailable = false; + sendOutputCapability(); console.error("[Socket] Main thread WebGPU init failed:", e); directCtx = directCanvas?.getContext("2d", { alpha: false }) ?? null; @@ -867,6 +881,8 @@ export function createImageDataWS( }); } else { mainThreadWebGPUInitializing = false; + nv12WebGPUAvailable = false; + sendOutputCapability(); directCtx = directCanvas?.getContext("2d", { alpha: false }) ?? null; if (pendingNv12Frame && directCanvas && directCtx) { renderPendingFrameCanvas2D(); @@ -1082,6 +1098,7 @@ export function createImageDataWS( ws.addEventListener("open", () => { setIsConnected(true); + sendOutputCapability(); }); ws.addEventListener("close", () => { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3211341374..565ab8e7569 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,10 +1,14 @@ -use std::sync::Arc; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; use std::time::Instant; -use cap_project::{ClipTransitionType, CursorEvents, ProjectConfiguration}; +use cap_project::{BackgroundSource, ClipTransitionType, CursorEvents, ProjectConfiguration}; use cap_rendering::{ DecodedSegmentFrames, FrameLayout, FrameRenderStageTimings, FrameRenderer, Nv12RenderedFrame, - ProjectUniforms, RenderVideoConstants, RenderedFrame, RendererLayers, TransitionRenderInput, + ProjectUniforms, RenderVideoConstants, RenderedFrame, RendererLayers, RenderingError, + TransitionRenderInput, clean_background_path, }; use tokio::sync::{mpsc, oneshot}; @@ -51,12 +55,17 @@ pub type RendererLayersReceiver = oneshot::Receiver; pub type EditorFrameCallback = Box; +fn should_use_nv12_output(is_software_adapter: bool, nv12_output_available: bool) -> bool { + nv12_output_available && !is_software_adapter +} + pub struct Renderer { rx: mpsc::Receiver, frame_cb: EditorFrameCallback, render_constants: Arc, layers_rx: RendererLayersReceiver, telemetry: Option, + nv12_output_available: Arc, } pub struct RendererHandle { @@ -68,6 +77,20 @@ pub fn start_renderer_layers_creation( render_constants: &Arc, project: &ProjectConfiguration, ) -> RendererLayersReceiver { + if let BackgroundSource::Image { path } | BackgroundSource::Wallpaper { path } = + &project.background.source + && let Some(path) = path.as_deref().and_then(clean_background_path) + && let Ok(runtime) = tokio::runtime::Handle::try_current() + { + let constants = render_constants.clone(); + drop(runtime.spawn(async move { + constants + .background_textures + .ensure(&constants.device, &constants.queue, &path) + .await; + })); + } + let (layers_tx, layers_rx) = oneshot::channel(); let constants = render_constants.clone(); let use_svg = project.cursor.use_svg; @@ -111,6 +134,22 @@ impl Renderer { frame_cb: EditorFrameCallback, layers_rx: RendererLayersReceiver, telemetry: Option, + ) -> Result { + Self::spawn_with_output_capability( + render_constants, + frame_cb, + layers_rx, + telemetry, + Arc::new(AtomicBool::new(false)), + ) + } + + pub fn spawn_with_output_capability( + render_constants: Arc, + frame_cb: EditorFrameCallback, + layers_rx: RendererLayersReceiver, + telemetry: Option, + nv12_output_available: Arc, ) -> Result { let (tx, rx) = mpsc::channel(64); @@ -120,6 +159,7 @@ impl Renderer { render_constants, layers_rx, telemetry: telemetry.clone(), + nv12_output_available, }; tokio::spawn(this.run()); @@ -134,9 +174,11 @@ impl Renderer { render_constants, layers_rx, telemetry, + nv12_output_available, } = self; let mut frame_renderer = FrameRenderer::new(&render_constants); + let mut previous_nv12_output = false; let mut layers = match layers_rx.await { Ok(layers) => layers, @@ -313,9 +355,28 @@ impl Renderer { let queue_wait = current.queued_at.elapsed(); let drain_duration = queue_drain_start.elapsed(); + // Stay on the universally supported RGBA path until the webview + // confirms that its WebGPU renderer initialized successfully. + let use_nv12_output = should_use_nv12_output( + render_constants.is_software_adapter, + nv12_output_available.load(Ordering::Acquire), + ); + if use_nv12_output != previous_nv12_output { + if use_nv12_output { + let _ = frame_renderer.flush_pipeline().await; + frame_renderer.release_rgba_readback(); + } else { + let _ = frame_renderer.flush_pipeline_nv12().await; + } + previous_nv12_output = use_nv12_output; + } let flush_start = Instant::now(); if drained_count > 0 { - let _ = frame_renderer.flush_pipeline().await; + if use_nv12_output { + let _ = frame_renderer.flush_pipeline_nv12().await; + } else { + let _ = frame_renderer.flush_pipeline().await; + } } let flush_duration = if drained_count > 0 { flush_start.elapsed() @@ -326,17 +387,75 @@ impl Renderer { let render_start = Instant::now(); let input_frame_number = current.input.uniforms().frame_number; let frame_layout = current.input.uniforms().frame_layout(); - let render_result = match current.input { - PendingRenderInput::Single(input) => { - frame_renderer - .render_immediate_with_timings( - input.segment_frames, - input.uniforms, - &input.cursor, - true, - &mut layers, + let render_result: Result< + (EditorFrameOutput, FrameRenderStageTimings), + RenderingError, + > = match current.input { + PendingRenderInput::Single(input) if use_nv12_output => frame_renderer + .render_immediate_nv12( + input.segment_frames, + input.uniforms, + &input.cursor, + true, + &mut layers, + ) + .await + .map(|frame| { + ( + EditorFrameOutput::Nv12(frame), + FrameRenderStageTimings::default(), ) - .await + }), + PendingRenderInput::Single(input) => frame_renderer + .render_immediate_with_timings( + input.segment_frames, + input.uniforms, + &input.cursor, + true, + &mut layers, + ) + .await + .map(|(frame, timings)| (EditorFrameOutput::Rgba(frame), timings)), + PendingRenderInput::Transition { + outgoing, + incoming, + kind, + progress, + } if use_nv12_output => { + async { + let frame = frame_renderer + .render_transition_nv12( + TransitionRenderInput { + segment_frames: outgoing.segment_frames, + uniforms: outgoing.uniforms, + cursor: &outgoing.cursor, + render_display: true, + }, + TransitionRenderInput { + segment_frames: incoming.segment_frames, + uniforms: incoming.uniforms, + cursor: &incoming.cursor, + render_display: true, + }, + kind, + progress, + &mut layers, + ) + .await?; + let frame = if let Some(frame) = frame { + frame + } else { + frame_renderer + .flush_pipeline_nv12() + .await + .unwrap_or(Err(RenderingError::BufferMapWaitingFailed))? + }; + Ok(( + EditorFrameOutput::Nv12(frame), + FrameRenderStageTimings::default(), + )) + } + .await } PendingRenderInput::Transition { outgoing, @@ -362,15 +481,26 @@ impl Renderer { &mut layers, ) .await - .map(|frame| (frame, FrameRenderStageTimings::default())), + .map(|frame| { + ( + EditorFrameOutput::Rgba(frame), + FrameRenderStageTimings::default(), + ) + }), }; match render_result { Ok((frame, render_stage_timings)) => { let render_duration = render_start.elapsed(); - let frame_number = frame.frame_number; - let output_format = PlaybackRenderOutputFormat::Rgba; + let (frame_number, output_format) = match &frame { + EditorFrameOutput::Nv12(frame) => { + (frame.frame_number, PlaybackRenderOutputFormat::Nv12) + } + EditorFrameOutput::Rgba(frame) => { + (frame.frame_number, PlaybackRenderOutputFormat::Rgba) + } + }; let callback_start = Instant::now(); - (frame_cb)(EditorFrameOutput::Rgba(frame), frame_layout); + (frame_cb)(frame, frame_layout); let callback_duration = callback_start.elapsed(); if let Some(telemetry) = &telemetry { telemetry.emit(PlaybackTelemetryEvent::RendererFrame { @@ -610,3 +740,15 @@ impl RendererHandle { let _ = rx.await; } } + +#[cfg(test)] +mod tests { + use super::should_use_nv12_output; + + #[test] + fn nv12_output_requires_hardware_and_a_capable_consumer() { + assert!(should_use_nv12_output(false, true)); + assert!(!should_use_nv12_output(true, true)); + assert!(!should_use_nv12_output(false, false)); + } +} diff --git a/crates/editor/src/editor_instance.rs b/crates/editor/src/editor_instance.rs index 5214531101a..f333741e8fe 100644 --- a/crates/editor/src/editor_instance.rs +++ b/crates/editor/src/editor_instance.rs @@ -115,12 +115,30 @@ impl EditorInstance { frame_cb: editor::EditorFrameCallback, shared_device: Option, ) -> Result, String> { - Self::new_with_audio_output( + Self::new_with_output_capability( + project_path, + on_state_change, + frame_cb, + shared_device, + Arc::new(AtomicBool::new(false)), + ) + .await + } + + pub async fn new_with_output_capability( + project_path: PathBuf, + on_state_change: impl Fn(&EditorState) + Send + Sync + 'static, + frame_cb: editor::EditorFrameCallback, + shared_device: Option, + nv12_output_available: Arc, + ) -> Result, String> { + Self::new_inner( project_path, on_state_change, frame_cb, shared_device, Arc::new(crate::AudioOutput::new()), + nv12_output_available, ) .await } @@ -134,6 +152,25 @@ impl EditorInstance { frame_cb: editor::EditorFrameCallback, shared_device: Option, audio_output: Arc, + ) -> Result, String> { + Self::new_inner( + project_path, + on_state_change, + frame_cb, + shared_device, + audio_output, + Arc::new(AtomicBool::new(false)), + ) + .await + } + + async fn new_inner( + project_path: PathBuf, + on_state_change: impl Fn(&EditorState) + Send + Sync + 'static, + frame_cb: editor::EditorFrameCallback, + shared_device: Option, + audio_output: Arc, + nv12_output_available: Arc, ) -> Result, String> { if !project_path.exists() { return Err(format!("Video path {} not found!", project_path.display())); @@ -337,10 +374,12 @@ impl EditorInstance { .map_err(|e| format!("Segment setup task failed: {e}"))??; let layers_rx = editor::finish_renderer_layers_creation(layers_rx).await; - let renderer = Arc::new(editor::Renderer::spawn( + let renderer = Arc::new(editor::Renderer::spawn_with_output_capability( render_constants.clone(), frame_cb, layers_rx, + None, + nv12_output_available, )?); let (preview_tx, preview_rx) = watch::channel(None); diff --git a/crates/editor/src/playback.rs b/crates/editor/src/playback.rs index fa92bb89ffb..67b521fec16 100644 --- a/crates/editor/src/playback.rs +++ b/crates/editor/src/playback.rs @@ -2,7 +2,7 @@ use cap_project::{ ClipOffsets, ClipTransitionType, ProjectConfiguration, TimelineFrameMapping, XY, }; use cap_rendering::{ - DecodedFrame, DecodedSegmentFrames, PixelFormat, PrecomputedCursorTimeline, ProjectUniforms, + DecodedFrame, DecodedSegmentFrames, PrecomputedCursorTimeline, ProjectUniforms, RecordingSegmentDecoders, RenderVideoConstants, ZoomTransformTimeline, spring_mass_damper::SpringMassDamperSimulationConfig, }; @@ -34,11 +34,14 @@ use crate::{ const INITIAL_PREFETCH_BUFFER_SIZE: usize = 10; const MAX_PREFETCH_BUFFER_SIZE: usize = 90; const PREFETCH_CHANNEL_SIZE: usize = 4; -const MAX_PREFETCH_BUFFER_BYTES: usize = 256 * 1024 * 1024; +const MAX_PREFETCH_BUFFER_BYTES: usize = 128 * 1024 * 1024; const PARALLEL_DECODE_TASKS: usize = 4; const INITIAL_PARALLEL_DECODE_TASKS: usize = 4; -const FRAME_CACHE_SIZE: usize = 90; -const MAX_FRAME_CACHE_BYTES: usize = 128 * 1024 * 1024; +// A Playback instance only advances its output frame number. Retaining the +// preceding 90 output frames therefore consumed memory without producing +// cache hits; keep only the current frame for a possible same-frame retry. +const FRAME_CACHE_SIZE: usize = 1; +const MAX_FRAME_CACHE_BYTES: usize = 16 * 1024 * 1024; const RAMP_UP_FRAME_COUNT: u32 = 15; #[cfg(target_os = "windows")] @@ -172,15 +175,7 @@ type CachedTransition = (Arc, u32, ClipTransitionType, f32 type CachedFrame = (Arc, u32, Option); fn decoded_frame_byte_len(frame: &DecodedFrame) -> usize { - if !frame.data().is_empty() { - return frame.data().len(); - } - - let pixels = (frame.width() as usize).saturating_mul(frame.height() as usize); - match frame.format() { - PixelFormat::Rgba => pixels.saturating_mul(4), - PixelFormat::Nv12 | PixelFormat::Yuv420p => pixels.saturating_mul(3) / 2, - } + frame.byte_len() } fn decoded_segment_frames_byte_len(frames: &DecodedSegmentFrames) -> usize { diff --git a/crates/rendering/src/frame_pipeline.rs b/crates/rendering/src/frame_pipeline.rs index 1e4b660f25b..58ed57139f4 100644 --- a/crates/rendering/src/frame_pipeline.rs +++ b/crates/rendering/src/frame_pipeline.rs @@ -790,17 +790,13 @@ impl PipelinedGpuReadback { pub fn take_pending(&mut self) -> Option { self.pending.take() } - - pub fn has_pending(&self) -> bool { - self.pending.is_some() - } } pub struct RenderSession { pub textures: (wgpu::Texture, wgpu::Texture), texture_views: (wgpu::TextureView, wgpu::TextureView), pub current_is_left: bool, - pub pipelined_readback: PipelinedGpuReadback, + pipelined_readback: Option, texture_width: u32, texture_height: u32, } @@ -827,8 +823,6 @@ impl RenderSession { }; let textures = (make_texture(), make_texture()); - let padded = padded_bytes_per_row((width, height)); - let initial_buffer_size = (padded * height) as u64; Self { current_is_left: true, @@ -837,7 +831,9 @@ impl RenderSession { textures.1.create_view(&Default::default()), ), textures, - pipelined_readback: PipelinedGpuReadback::new(device, initial_buffer_size), + // NV12 output has its own compact readback pool. Allocate the much + // larger RGBA staging buffers only if an RGBA consumer is used. + pipelined_readback: None, texture_width: width, texture_height: height, } @@ -884,6 +880,10 @@ impl RenderSession { self.texture_height = height; } + pub fn release_rgba_readback(&mut self) { + self.pipelined_readback = None; + } + pub fn current_texture(&self) -> &wgpu::Texture { if self.current_is_left { &self.textures.0 @@ -994,9 +994,14 @@ pub async fn finish_encoder_timed( encoder: wgpu::CommandEncoder, ) -> Result<(Option, FinishEncoderTimings), RenderingError> { let mut timings = FinishEncoderTimings::default(); + let initial_buffer_size = + (padded_bytes_per_row(uniforms.output_size) * uniforms.output_size.1) as u64; + let readback = session + .pipelined_readback + .get_or_insert_with(|| PipelinedGpuReadback::new(device, initial_buffer_size)); let wait_start = Instant::now(); - let previous_frame = if let Some(prev) = session.pipelined_readback.take_pending() { + let previous_frame = if let Some(prev) = readback.take_pending() { Some(prev.wait(device).await?) } else { None @@ -1004,7 +1009,7 @@ pub async fn finish_encoder_timed( timings.wait_previous_duration = wait_start.elapsed(); let resize_start = Instant::now(); - session.pipelined_readback.perform_resize_if_needed(device); + readback.perform_resize_if_needed(device); timings.resize_duration = resize_start.elapsed(); let texture = if session.current_is_left { @@ -1014,9 +1019,7 @@ pub async fn finish_encoder_timed( }; let submit_start = Instant::now(); - session - .pipelined_readback - .submit_readback(device, queue, texture, uniforms, encoder)?; + readback.submit_readback(device, queue, texture, uniforms, encoder)?; timings.submit_readback_duration = submit_start.elapsed(); Ok((previous_frame, timings)) @@ -1083,7 +1086,8 @@ pub async fn flush_pending_readback( session: &mut RenderSession, device: &wgpu::Device, ) -> Option> { - if let Some(pending) = session.pipelined_readback.take_pending() { + let readback = session.pipelined_readback.as_mut()?; + if let Some(pending) = readback.take_pending() { Some(pending.wait(device).await) } else { None diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index fe56996e6df..de465327433 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -4940,6 +4940,12 @@ impl<'a> FrameRenderer<'a> { } } + pub fn release_rgba_readback(&mut self) { + if let Some(session) = &mut self.session { + session.release_rgba_readback(); + } + } + pub async fn render_immediate_nv12( &mut self, segment_frames: DecodedSegmentFrames, From eb989945ce7cc21000e924cf8cdca6b67a436484 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:03:07 +0100 Subject: [PATCH 24/31] docs: update Electron performance benchmark --- .../assets/electron-vs-tauri-performance.png | Bin 201697 -> 236196 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/assets/electron-vs-tauri-performance.png b/.github/assets/electron-vs-tauri-performance.png index 5341c82f7c4ce5da8ccd62631eee8c4feea9498d..9240898ef7b95f89d79ff518454581886d69a3b0 100644 GIT binary patch literal 236196 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

$rjK`niuf40>J4)ty!w=dFIq5 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∋nY%ANYY}_ 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∾_PeKE~+tv}jo$a&hN-3Uvq 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 XCSY{St-uoK+_^Ur^5S`~4ZZ&t%O?cv literal 201697 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%0AIuGqenW2mS?NLiX`!Shhg2lB- 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%0AL>}+mm+&!02-hcZL5Hs}>sEsZT;b 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 SWCnh7Pg+7jyioMr$NvKvy^w1F From ca5c36005515c794154022f2a749e181c38b6818 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:42:41 +0100 Subject: [PATCH 25/31] docs: refresh final Electron benchmark --- .../assets/electron-vs-tauri-performance.png | Bin 236196 -> 253392 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/assets/electron-vs-tauri-performance.png b/.github/assets/electron-vs-tauri-performance.png index 9240898ef7b95f89d79ff518454581886d69a3b0..893b8f4f06cebbe847b867f3e4568e82cebc3126 100644 GIT binary patch literal 253392 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


o%|V~X`2IWZKL0@; zopIZO&4-ecDyAyRrMP<&=+V9|=A}<=F0NnQb$N#-%B&vu)$DUGvR^hoeYQ;0_g6}I z^Hx#2`B87)jJ%*(Z$%5P#e*s4!DUf5Irb%)%S-Aux2*>+stzkzUzH!43HsTHwQ|NVrdeqJVYriDZx&(!IV^zM*$N zBbcD%?j(S=Rej3OE8h8&3gnLPCo%intSE_gkxT+DdX3h>#?01ww}I>(?;$oc;Syl{ z)?yUzp4vwylNu9hgKs%Qf-_&4&5;V&jq{*knY+|W8pWs(e&SR03;QyXIZ^J=LNUw? zQuS5Cx!Ns3YDpYJU?B@+0~CslO3t!Q;<_YDnpJ5%yE8l%%8qv9ZMKhCx2x8(8W&eR z9|PqNHnWhTIpeEH>l%{kk}|C^9U7L~lpKT+%$c0N`r*x%N{z^@poIC%u?89msZ=Rz zrVh<6KU1REro@SKyqV4x;KRrPPs5PBmDcwQYk8@RY~93k>iILGW}nNg3w)0#!4U;Y z49i^Ev0;2oc4l_fPx(;82W!vKx#juDwzo8U%}%CA+b4NVx^hn?JH7VQc_qvd=7jlU z1vK$84X-EP6tr0AO3)33KI0rs;evh6t;@39yY8!2puQ5PjhBZrsW6$Xpz6 zu9K%n(m89SiX~lz^Q!V!o9uB~L^cE5-)lp+U6;=@4aHSZ`ep|I4rI#_(Qt6?IXaE+ zZ2p`W3j}|`!9p%fEUk@quVjwz#D;5B2DC6}*Y3gyXycnPlYkghh_jaFav29SOF}`R zTI3F!C+ju-V>6XFjz#Rd& zrtz~p-xtC)%vtJ0wETRq+tJO%wc zbg`=eKh(0+fb#76;yjsbfmBEd=XU||B)XKVrlvnyRx8MgZFRyyf^U%#5q{EfjFnP% zmKymBAMFR?LkhFA~&uAYJF>gpy3z+e%_2tq)!l?ZsZ|9Q(} z-hWZe#?EeC6BdNp-K=qax0nYv?5{kIuw`7ZM36c=**(40PN5T5BE$>g1INVKM+9>H z$FuSuU$nNSlgXS+H%hj0Ftu!}jAuYuS}r5W)Vkfdw*3)N_XJQjD_-hLIt5P6XC422 z*nhmb+UL)X%0qTCS6z14gfisB%e+GA9e7BUwQ2)uA_}zs_2UxL^^I7>s&=-v4=?_| z@K?UGJz6D=rih{(PnDSQg#PCP{9nfm+D3E54gZJ^ZM!FWSbyEzjH!zFiXV?Yl-#;@iaFzHA%cvdwR(|fo^(y)%>4SN|cilk)RCbP5$nw>16^I&GS{CtYfR#&5r6yzZ@A&j7HN)i z%2^MI%ByB&xgw!m=^gAzQS%b>=H})m5G?+>*!78CT*L-0D$bHjWf{`+)p+n{kBn}+ z=hw`bn7Eg?cFunnkT=>%jTn+Q(0Ayk;n!!BtS|p~rl$vRSBl4y0QPr`K*B0uH<8aC z=XQ-vz~}uD1_@{Oc>>M}2IcXE_fihe^Zr!L8P>_p&Thl=xsAiyA)xuZ{hgYI1|G)C z{r>5lZnk`*=KPl}6|+K1k}mYM)(|hHtj_Nu$V#Phb4=Z|BL4xarNisZDFZy0+Qmu~Q=pXB=K^RTjYo z02wlN+8^H|dL8VL!W?=Kx*7gMU zAQAYzO7#li(t_Jt;msukwT6=)0g#nkb2Ww%*VFbpcmBKsvOAqqUD;C41&XPcRAi20 z1w0(K4r94WMgEDPs}AtNW+)7bkiNd&rgZklM?jUTSgk!IxhG}L5F}jpbE!b-&G+I~ ztz21^^E`oDY)P?CGMO%wM@w}~4iz+6719lTEs*yeI|xupWgvjzp9$wXq@JRvP*b2` zZhw@1>EIo`+Kg55RkMa#YxQ>(f>Ua!6U%*Ky>zBH53=6`Z!}r3pM6>b<6eHR`RVIP z9)?+>+^)Q&NDvnZ6&#SQjsSMVzwgSeH6Bwe6ZhRF2sB?P`U{#+aj0%G98O{l7mNMa z7ltK_ejIDi=am&}IvC4iQM@f8pABy2&}bW~VO>EXHaJ!%$sA$Q{A62?@e7X5Ogg>OU5tk%^Mbf<{RVOeC0?pck7am7M?Het z9B(NkbylIwGz;>?!YQ2W_Xn-7o{EZ0w45KaTti*WO;dA(X|@!KGaDyzlg6rnr2D*4Nau*|S&cUP)ZtLWn|&|b5S&A1)#tH+F?rs?A_ z-b=Y&i}^dqYVIKVIU_nE!uvCxxj<2i{b>?-rRt4whBi!JUmu%p!*hR?{F2cSB~l(6 z2kUJl@6i=|4>8QnGb;VU$HP=Eq&F3J`-!iBUKEkmBp~#R)3$r1)Hqs}-ceTEZ@9*I z=By3IYmPEDH(PhJ*5EwWw{3Z$LPO%UJkU_k^s>=b;NH&WwwkL+sVSO?cVK+eW~%#f z!>1(^y7>8SEjTy|hwKOdy*9v36X}B29XeV0@UEAM$Ib*C_L2?+m-`+^A9+EnG{{G5 z1p0;heg{m|012qs^EAo+lPd8Qc%pg+C^Usgg{Qmvqy%R6%TSN9z;M%_Gjk~?yni@X zRU3xy0-Z~?eH&RaoA<+m-t42imIL0umDrg=a)b24-*}1mLdgJBFAfT7q4q+^0PYFA z0|woCPk|DLUGRHj$7^YkBL$}$7yr~EKxf$~wJ04wO--OuRgaqs=`f+If4Np}5|ZQ~ zo4`YV$ZVl|WvrPPSh#|2xx-I(M8&+de5;9!J8c~j1)&p}cO&pw-ARh%vkYw~Y6VIj zY{q1p2~BwOEb~JR?<7dY6(+ngo2^UjNbny@2CGJ9jeWP#E>x*?pT_OknY=jnPAe{Q zoGVQ@o-MFLIOobKi!qEmJ?Pl)mXvvG1&a}1QY#bbCFPMnYznb+!W<*P0N&tyvTDj) z2B=aS@4Ql`pMZ}2e7)GMl7T)j4u)EP`2=&pW>rCtkO1v*tnwW`5gF%`Vuyp)IgZLm zsGB7!s|lkfqATaKL5qWL9$mos+bg;}V`@hb~g4$ow9xeKuD0s=D{2K8FAiJ}C1B(w9BG8k0?P3oyX})HL{rpN8?S!cA`BhlK)v zT0;DfH$Rl+NHJUnci!E79(@2XAL4c${3q#fp%P|mL!)*-0+(BqBw%rHYv&E?ll8H( zk|H#?dY^9H_HA3`l5sV|7717Hlb8Ns>go1FTjLXr(uO8maSO1(v*IF!FI%nKnTl^j z?Gq$iapwiJ+!J;^SH5^$3>Lo+eMNymes(fj=yg5w+uT2W=E|S!D{X)mfw#V~5NITC zadn9J5%>cUIfKihsfrL564?7yR^qzXp1Z`nCM0V%9)l+~6q4+%W^%elA1z}0Hdg{C zG+73yynL&sY8|8wkqpk_ko3!?dbdn=f{U^HM9Ab)n&`I8X4_!8R_^f*=c`lp8z;rK zxYRP6T&ZE%Bf`8txm5}-p#z^Y$#=AdlN+?-EI&XZ2(8=n%X2JL30qj($#hLT1!oZ~ zFlN<)VzcZm&pZmpT?w1Md9%bHTYo%Xvf5$7ZImGa`=vup2Nt6by6AHmLyP+^xP#fj z@u8CekhXc;>cGH#9NqOv7HquWbS?myVD1o+f6D9n5xw%V61580_h{aqK<$%_9=N5nGj9{0O^XKs*?Fw(wySuD zY}ht*bDfR*+jjk-gQ}$cT&>r{Rs(%&f(>z0SaFyL!hw$o5b4XQSFZ5Y^|60;>C-6^ zl0R}eu9hym)BT&l_Ro_d-a*psFOgvYbwjV&okg^v)I3p-zNXp!RW^IN-dt6$8fh2X zrczTLgWRp_z#XJQKT2KyUt14sEJrQ_J(Wty?j*6lDxDt582M39(iD+=nE>%%n^>>4x0#&tipRmm*?*0)`*Z-Ln_KehZ`p;!1AG&_ zAFhA5dV(n!CtF=_YE8gz7qPjpp@AzTvb~iT3NWKIOiWDP-h~$-fB%{yByZ(gZ#4O< zU)=(&FOJ^D)t=PlO`#ly3FoHFWva=`gn74_yaNh>pm~_rVT8a#UpWaWsSGd45A9

~(GnB>fAhq8QABfIPi_XU?H^q)vx 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�u$z7e!{clR$-lVElur|^GU$xMJ5 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 TPM3;5{&Y0e^i)e!9=-T440SAt literal 236196 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

$rjK`niuf40>J4)ty!w=dFIq5 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∋nY%ANYY}_ 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∾_PeKE~+tv}jo$a&hN-3Uvq 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 XCSY{St-uoK+_^Ur^5S`~4ZZ&t%O?cv From 4aa2d3a3894cf7176a795de91bc4142cce633cc1 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:48:53 +0100 Subject: [PATCH 26/31] fix: revalidate release before promotion --- .github/workflows/desktop-release.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index bfbc723b0c6..22ff28e2c66 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -427,16 +427,20 @@ jobs: script: | const path = require("node:path"); const releaseId = Number(process.env.STAGING_RELEASE_ID); - const stagingRelease = (await github.rest.repos.getRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: releaseId, - })).data; - if (!stagingRelease.draft || stagingRelease.tag_name !== process.env.STAGING_TAG) { - throw new Error( - `Staging release '${process.env.STAGING_TAG}' is no longer an unpublished draft`, - ); - } + const requireUnpublishedStagingRelease = async () => { + const release = (await github.rest.repos.getRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + })).data; + if (!release.draft || release.tag_name !== process.env.STAGING_TAG) { + throw new Error( + `Staging release '${process.env.STAGING_TAG}' is no longer an unpublished draft`, + ); + } + return release; + }; + await requireUnpublishedStagingRelease(); const globber = await glob.create("release-assets/*"); const expectedAssets = []; @@ -499,6 +503,9 @@ jobs: if (error.status !== 404) throw error; } + // Keep this guard adjacent to the mutation. The validations above may + // take long enough for an operator to publish the staging release. + await requireUnpublishedStagingRelease(); const release = (await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, From 26be4fafae42f66ee9f5d1113dfe0405d6a0b130 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:58:31 +0100 Subject: [PATCH 27/31] fix: require immutable desktop releases --- .github/workflows/desktop-release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 22ff28e2c66..7744db76ada 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -61,6 +61,19 @@ jobs: if (!versionPattern || !versionPattern.test(version)) throw new Error(`Invalid ${channel} release version '${version}'`); + const immutableReleases = await github.request( + "GET /repos/{owner}/{repo}/immutable-releases", + { + owner: context.repo.owner, + repo: context.repo.repo, + }, + ); + if (!immutableReleases.data.enabled) { + throw new Error( + "Repository immutable releases must be enabled before preparing a desktop release", + ); + } + const tag = channel === "nightly" ? `cap-nightly-v${version}` : `cap-v${version}`; From 8b56f7946a77cf312f6e9b20910b16a888321a44 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:11:36 +0100 Subject: [PATCH 28/31] fix: validate release policy without admin token --- .github/workflows/desktop-release.yml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 7744db76ada..58b671d3d23 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -39,6 +39,12 @@ jobs: permissions: contents: write steps: + - name: Require immutable releases + if: ${{ vars.IMMUTABLE_RELEASES_ENABLED != 'true' }} + run: | + echo "Repository immutable releases must be enabled before preparing a desktop release" >&2 + exit 1 + - name: Prepare immutable release staging draft id: release uses: actions/github-script@v7 @@ -61,19 +67,6 @@ jobs: if (!versionPattern || !versionPattern.test(version)) throw new Error(`Invalid ${channel} release version '${version}'`); - const immutableReleases = await github.request( - "GET /repos/{owner}/{repo}/immutable-releases", - { - owner: context.repo.owner, - repo: context.repo.repo, - }, - ); - if (!immutableReleases.data.enabled) { - throw new Error( - "Repository immutable releases must be enabled before preparing a desktop release", - ); - } - const tag = channel === "nightly" ? `cap-nightly-v${version}` : `cap-v${version}`; From 6604aa6af9ea217b22b59e4dd29c3b821c62b615 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:58:55 +0100 Subject: [PATCH 29/31] perf: reduce editor layout measurements --- apps/desktop/src/routes/editor/Editor.tsx | 8 ++--- apps/desktop/src/routes/editor/Player.tsx | 8 ++--- .../src/routes/editor/Timeline/context.ts | 8 ++--- .../src/routes/editor/Timeline/index.tsx | 4 +-- .../src/routes/editor/resize-driven-bounds.ts | 31 +++++++++++++++++++ apps/desktop/src/routes/editor/ui.tsx | 12 +++---- 6 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 apps/desktop/src/routes/editor/resize-driven-bounds.ts diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index 4664ee299b8..f7f5355d22c 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { NumberField } from "@kobalte/core/number-field"; -import { createElementBounds } from "@solid-primitives/bounds"; import { trackDeep } from "@solid-primitives/deep"; +import { createElementSize } from "@solid-primitives/resize-observer"; import { debounce, throttle } from "@solid-primitives/scheduled"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation, createQuery, skipToken } from "@tanstack/solid-query"; @@ -407,7 +407,7 @@ function Inner() { }); const [layoutRef, setLayoutRef] = createSignal(); - const layoutBounds = createElementBounds(layoutRef); + const layoutSize = createElementSize(layoutRef); const [storedTimelineHeight, setStoredTimelineHeight] = makePersisted( createSignal(DEFAULT_TIMELINE_HEIGHT), { name: "editorTimelineHeight" }, @@ -419,7 +419,7 @@ function Inner() { } | null>(null); const clampTimelineHeight = (value: number) => { - const available = layoutBounds.height ?? 0; + const available = layoutSize.height ?? 0; const maxHeight = available > 0 ? Math.max(MIN_TIMELINE_HEIGHT, available - MIN_PLAYER_HEIGHT) @@ -457,7 +457,7 @@ function Inner() { }; createEffect(() => { - const available = layoutBounds.height; + const available = layoutSize.height; if (!available) return; setStoredTimelineHeight((height) => clampTimelineHeight(height)); }); diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index 535086045a0..64d54196a80 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -1,6 +1,6 @@ import { Select as KSelect } from "@kobalte/core/select"; import { ToggleButton as KToggleButton } from "@kobalte/core/toggle-button"; -import { createElementBounds } from "@solid-primitives/bounds"; +import { createElementSize } from "@solid-primitives/resize-observer"; import { debounce } from "@solid-primitives/scheduled"; import { cx } from "cva"; import { createEffect, createSignal, onMount, Show } from "solid-js"; @@ -521,7 +521,7 @@ function PreviewCanvas() { const [canvasContainerRef, setCanvasContainerRef] = createSignal(); - const containerBounds = createElementBounds(canvasContainerRef); + const containerSize = createElementSize(canvasContainerRef); const [debouncedBounds, setDebouncedBounds] = createSignal({ width: 0, @@ -534,8 +534,8 @@ function PreviewCanvas() { ); createEffect(() => { - const width = containerBounds.width ?? 0; - const height = containerBounds.height ?? 0; + const width = containerSize.width ?? 0; + const height = containerSize.height ?? 0; if (debouncedBounds().width === 0 && debouncedBounds().height === 0) { setDebouncedBounds({ width, height }); } else { diff --git a/apps/desktop/src/routes/editor/Timeline/context.ts b/apps/desktop/src/routes/editor/Timeline/context.ts index 2d614db6650..b5847242a08 100644 --- a/apps/desktop/src/routes/editor/Timeline/context.ts +++ b/apps/desktop/src/routes/editor/Timeline/context.ts @@ -1,12 +1,10 @@ -import { - createElementBounds, - type NullableBounds, -} from "@solid-primitives/bounds"; +import type { NullableBounds } from "@solid-primitives/bounds"; import { createContextProvider } from "@solid-primitives/context"; import { type Accessor, createMemo } from "solid-js"; import { createStore } from "solid-js/store"; import { useEditorContext } from "../context"; +import { createResizeDrivenBounds } from "../resize-driven-bounds"; export const MAX_TIMELINE_MARKINGS = 20; const TIMELINE_MARKING_RESOLUTIONS = [0.5, 1, 2.5, 5, 10, 30]; @@ -85,7 +83,7 @@ export const [TrackContextProvider, useTrackContext] = createContextProvider( const [trackState, setTrackState] = createStore({ draggingSegment: false, }); - const bounds = createElementBounds(() => props.ref()); + const bounds = createResizeDrivenBounds(() => props.ref()); const secsPerPixel = () => state.timeline.transform.zoom / (bounds.width ?? 1); diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index afb0b0fec4f..0f9a96ff1ac 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1,4 +1,3 @@ -import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListener } from "@solid-primitives/event-listener"; import { cx } from "cva"; import { @@ -19,6 +18,7 @@ import toast from "solid-toast"; import { LogicalPosition } from "~/electron/dpi"; import { Menu, MenuItem } from "~/electron/menu"; import { platform } from "~/electron/os"; +import { createResizeDrivenBounds } from "../resize-driven-bounds"; import "./styles.css"; @@ -180,7 +180,7 @@ export function Timeline(props: { const [timelineScrollRef, setTimelineScrollRef] = createSignal(); const [timelineRef, setTimelineRef] = createSignal(); - const timelineBounds = createElementBounds(timelineRef); + const timelineBounds = createResizeDrivenBounds(timelineRef); const secsPerPixel = () => transform().zoom / (timelineBounds.width ?? 1); diff --git a/apps/desktop/src/routes/editor/resize-driven-bounds.ts b/apps/desktop/src/routes/editor/resize-driven-bounds.ts new file mode 100644 index 00000000000..b19ce8578f4 --- /dev/null +++ b/apps/desktop/src/routes/editor/resize-driven-bounds.ts @@ -0,0 +1,31 @@ +import type { NullableBounds } from "@solid-primitives/bounds"; +import { createElementSize } from "@solid-primitives/resize-observer"; +import type { Accessor } from "solid-js"; + +export function createResizeDrivenBounds( + target: Accessor, +): Readonly { + const size = createElementSize(target); + const rect = () => target()?.getBoundingClientRect(); + + return { + get top() { + return rect()?.top ?? null; + }, + get left() { + return rect()?.left ?? null; + }, + get bottom() { + return rect()?.bottom ?? null; + }, + get right() { + return rect()?.right ?? null; + }, + get width() { + return size.width; + }, + get height() { + return size.height; + }, + }; +} diff --git a/apps/desktop/src/routes/editor/ui.tsx b/apps/desktop/src/routes/editor/ui.tsx index 789fc3b1c49..54c94022102 100644 --- a/apps/desktop/src/routes/editor/ui.tsx +++ b/apps/desktop/src/routes/editor/ui.tsx @@ -4,7 +4,6 @@ import { DropdownMenu } from "@kobalte/core/dropdown-menu"; import { Polymorphic, type PolymorphicProps } from "@kobalte/core/polymorphic"; import { Slider as KSlider } from "@kobalte/core/slider"; import { Tooltip as KTooltip } from "@kobalte/core/tooltip"; -import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListener } from "@solid-primitives/event-listener"; import { cva, cx, type VariantProps } from "cva"; @@ -82,8 +81,6 @@ export function Slider( const [thumbRef, setThumbRef] = createSignal(); - const thumbBounds = createElementBounds(thumbRef); - const [dragging, setDragging] = createSignal(false); return ( @@ -119,11 +116,12 @@ export function Slider( { + const rect = thumbRef()?.getBoundingClientRect(); return { - x: thumbBounds.left ?? undefined, - y: thumbBounds.top ?? undefined, - width: thumbBounds.width ?? undefined, - height: thumbBounds.height ?? undefined, + x: rect?.left, + y: rect?.top, + width: rect?.width, + height: rect?.height, }; }} content={ From 965567518fbd6a5c0be6b03035cb9026d136c5a3 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:59:10 +0100 Subject: [PATCH 30/31] perf: defer updates while main window is hidden --- .../routes/(window-chrome)/new-main/index.tsx | 6 ++ .../new-main/update-check-visibility.test.ts | 56 +++++++++++++++++++ .../new-main/update-check-visibility.ts | 34 +++++++++++ 3 files changed, 96 insertions(+) create mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.test.ts create mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.ts diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index 7b4f14431f8..c5b38030b71 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -115,6 +115,7 @@ import type { RecordingWithPath, ScreenshotWithPath } from "./TargetCard"; import TargetDropdownButton from "./TargetDropdownButton"; import TargetMenuGrid from "./TargetMenuGrid"; import TargetTypeButton from "./TargetTypeButton"; +import { waitUntilVisible } from "./update-check-visibility"; import useRequestPermission from "./useRequestPermission"; const MAIN_WINDOW_SIZE = { @@ -1742,12 +1743,17 @@ function createUpdateCheck() { if (import.meta.env.DEV) return; const navigate = useNavigate(); + const visibilityController = new AbortController(); + onCleanup(() => visibilityController.abort()); onMount(async () => { if (hasChecked) return; hasChecked = true; await new Promise((res) => setTimeout(res, 10_000)); + if (visibilityController.signal.aborted) return; + if (!(await waitUntilVisible(document, visibilityController.signal))) + return; // The Rust background loop owns nightly updates. const settings = await generalSettingsStore.get(); diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.test.ts b/apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.test.ts new file mode 100644 index 00000000000..e541c5ab342 --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import { + type VisibilityDocument, + waitUntilVisible, +} from "./update-check-visibility"; + +class FakeVisibilityDocument implements VisibilityDocument { + hidden = true; + readonly listeners = new Set<() => void>(); + + addEventListener(_type: "visibilitychange", listener: () => void) { + this.listeners.add(listener); + } + + removeEventListener(_type: "visibilitychange", listener: () => void) { + this.listeners.delete(listener); + } + + show() { + this.hidden = false; + for (const listener of [...this.listeners]) listener(); + } +} + +describe("waitUntilVisible", () => { + it("resolves immediately for a visible document", async () => { + const document = new FakeVisibilityDocument(); + document.hidden = false; + + await expect(waitUntilVisible(document)).resolves.toBe(true); + expect(document.listeners).toHaveLength(0); + }); + + it("waits for a hidden document to become visible", async () => { + const document = new FakeVisibilityDocument(); + const result = waitUntilVisible(document); + + expect(document.listeners).toHaveLength(1); + document.show(); + + await expect(result).resolves.toBe(true); + expect(document.listeners).toHaveLength(0); + }); + + it("cleans up and declines the check when aborted", async () => { + const document = new FakeVisibilityDocument(); + const controller = new AbortController(); + const result = waitUntilVisible(document, controller.signal); + + controller.abort(); + + await expect(result).resolves.toBe(false); + expect(document.listeners).toHaveLength(0); + }); +}); diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.ts b/apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.ts new file mode 100644 index 00000000000..aed26e68141 --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/new-main/update-check-visibility.ts @@ -0,0 +1,34 @@ +export interface VisibilityDocument { + readonly hidden: boolean; + addEventListener(type: "visibilitychange", listener: () => void): void; + removeEventListener(type: "visibilitychange", listener: () => void): void; +} + +export function waitUntilVisible( + document: VisibilityDocument, + signal?: AbortSignal, +): Promise { + if (signal?.aborted) return Promise.resolve(false); + if (!document.hidden) return Promise.resolve(true); + + return new Promise((resolve) => { + let settled = false; + const finish = (visible: boolean) => { + if (settled) return; + settled = true; + document.removeEventListener("visibilitychange", handleVisibilityChange); + signal?.removeEventListener("abort", handleAbort); + resolve(visible); + }; + const handleVisibilityChange = () => { + if (!document.hidden) finish(true); + }; + const handleAbort = () => finish(false); + + document.addEventListener("visibilitychange", handleVisibilityChange); + signal?.addEventListener("abort", handleAbort, { once: true }); + + if (signal?.aborted) finish(false); + else if (!document.hidden) finish(true); + }); +} From 0966358a05851b7cd7e181d48e0daf02bad4ab04 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:48:57 +0100 Subject: [PATCH 31/31] fix: format merged SEO metadata --- apps/web/components/pages/seo/AsyncVideoCodeReviewsPage.tsx | 3 ++- apps/web/components/pages/seo/BestScreenRecorderPage.tsx | 3 ++- .../components/pages/seo/DeveloperDocumentationVideosPage.tsx | 3 ++- .../components/pages/seo/HipaaCompliantScreenRecordingPage.tsx | 3 ++- .../components/pages/seo/MacScreenRecordingWithAudioPage.tsx | 3 ++- apps/web/components/pages/seo/OpenSourceScreenRecorderPage.tsx | 3 ++- apps/web/components/pages/seo/RecordScreenPage.tsx | 3 ++- apps/web/components/pages/seo/ScreenRecorderPage.tsx | 3 ++- apps/web/components/pages/seo/ScreenRecordingPage.tsx | 3 ++- .../web/components/pages/seo/SelfHostedScreenRecordingPage.tsx | 3 ++- apps/web/components/pages/seo/VideoRecordingSoftwarePage.tsx | 3 ++- 11 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/web/components/pages/seo/AsyncVideoCodeReviewsPage.tsx b/apps/web/components/pages/seo/AsyncVideoCodeReviewsPage.tsx index 4e24e4adf17..b7903003862 100644 --- a/apps/web/components/pages/seo/AsyncVideoCodeReviewsPage.tsx +++ b/apps/web/components/pages/seo/AsyncVideoCodeReviewsPage.tsx @@ -278,7 +278,8 @@ export const asyncVideoCodeReviewsContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap async video code review demo showing screen recording of a pull request walkthrough with timestamped comments", + title: + "Cap async video code review demo showing screen recording of a pull request walkthrough with timestamped comments", }, }, diff --git a/apps/web/components/pages/seo/BestScreenRecorderPage.tsx b/apps/web/components/pages/seo/BestScreenRecorderPage.tsx index 6c6b6c87a6e..53ee4601acb 100644 --- a/apps/web/components/pages/seo/BestScreenRecorderPage.tsx +++ b/apps/web/components/pages/seo/BestScreenRecorderPage.tsx @@ -254,7 +254,8 @@ export const bestScreenRecorderContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap screen recorder demo showing 4K capture, webcam overlay, and instant sharing", + title: + "Cap screen recorder demo showing 4K capture, webcam overlay, and instant sharing", }, }, diff --git a/apps/web/components/pages/seo/DeveloperDocumentationVideosPage.tsx b/apps/web/components/pages/seo/DeveloperDocumentationVideosPage.tsx index 20dd12ea63c..acaf4921d29 100644 --- a/apps/web/components/pages/seo/DeveloperDocumentationVideosPage.tsx +++ b/apps/web/components/pages/seo/DeveloperDocumentationVideosPage.tsx @@ -277,7 +277,8 @@ export const developerDocumentationVideosContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap developer documentation video demo showing screen recording of an API walkthrough with narration and instant share link", + title: + "Cap developer documentation video demo showing screen recording of an API walkthrough with narration and instant share link", }, }, diff --git a/apps/web/components/pages/seo/HipaaCompliantScreenRecordingPage.tsx b/apps/web/components/pages/seo/HipaaCompliantScreenRecordingPage.tsx index 8df0c2142d8..aa9babc2b25 100644 --- a/apps/web/components/pages/seo/HipaaCompliantScreenRecordingPage.tsx +++ b/apps/web/components/pages/seo/HipaaCompliantScreenRecordingPage.tsx @@ -285,7 +285,8 @@ export const hipaaCompliantScreenRecordingContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap HIPAA-compliant screen recording demo showing self-hosted storage configuration and secure sharing", + title: + "Cap HIPAA-compliant screen recording demo showing self-hosted storage configuration and secure sharing", }, }, diff --git a/apps/web/components/pages/seo/MacScreenRecordingWithAudioPage.tsx b/apps/web/components/pages/seo/MacScreenRecordingWithAudioPage.tsx index 98b80b1dd40..27431c0f589 100644 --- a/apps/web/components/pages/seo/MacScreenRecordingWithAudioPage.tsx +++ b/apps/web/components/pages/seo/MacScreenRecordingWithAudioPage.tsx @@ -271,7 +271,8 @@ export const macScreenRecordingWithAudioContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap recording Mac screen with system audio and microphone simultaneously", + title: + "Cap recording Mac screen with system audio and microphone simultaneously", }, }, diff --git a/apps/web/components/pages/seo/OpenSourceScreenRecorderPage.tsx b/apps/web/components/pages/seo/OpenSourceScreenRecorderPage.tsx index 3d16c92c835..31655d7840e 100644 --- a/apps/web/components/pages/seo/OpenSourceScreenRecorderPage.tsx +++ b/apps/web/components/pages/seo/OpenSourceScreenRecorderPage.tsx @@ -255,7 +255,8 @@ export const openSourceScreenRecorderContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap open source screen recorder demo showing recording, sharing, and self-hosting", + title: + "Cap open source screen recorder demo showing recording, sharing, and self-hosting", }, }, diff --git a/apps/web/components/pages/seo/RecordScreenPage.tsx b/apps/web/components/pages/seo/RecordScreenPage.tsx index 77a84c2f245..301bb3b29ce 100644 --- a/apps/web/components/pages/seo/RecordScreenPage.tsx +++ b/apps/web/components/pages/seo/RecordScreenPage.tsx @@ -247,7 +247,8 @@ export const recordScreenContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap screen recording demo showing one-click capture, webcam overlay, and instant sharing", + title: + "Cap screen recording demo showing one-click capture, webcam overlay, and instant sharing", }, }, diff --git a/apps/web/components/pages/seo/ScreenRecorderPage.tsx b/apps/web/components/pages/seo/ScreenRecorderPage.tsx index e57a6962768..55b86262358 100644 --- a/apps/web/components/pages/seo/ScreenRecorderPage.tsx +++ b/apps/web/components/pages/seo/ScreenRecorderPage.tsx @@ -107,7 +107,8 @@ export const screenRecorderContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap screen recorder demo showing high-quality and user-friendly features", + title: + "Cap screen recorder demo showing high-quality and user-friendly features", }, }, diff --git a/apps/web/components/pages/seo/ScreenRecordingPage.tsx b/apps/web/components/pages/seo/ScreenRecordingPage.tsx index c20899667dc..3a53896a4e7 100644 --- a/apps/web/components/pages/seo/ScreenRecordingPage.tsx +++ b/apps/web/components/pages/seo/ScreenRecordingPage.tsx @@ -191,7 +191,8 @@ export const screenRecordingContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap screen recording demo showing HD capture, webcam overlay, and instant sharing", + title: + "Cap screen recording demo showing HD capture, webcam overlay, and instant sharing", }, }, diff --git a/apps/web/components/pages/seo/SelfHostedScreenRecordingPage.tsx b/apps/web/components/pages/seo/SelfHostedScreenRecordingPage.tsx index e0e330c993b..37cf1602f6e 100644 --- a/apps/web/components/pages/seo/SelfHostedScreenRecordingPage.tsx +++ b/apps/web/components/pages/seo/SelfHostedScreenRecordingPage.tsx @@ -273,7 +273,8 @@ export const selfHostedScreenRecordingContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap self-hosted screen recording demo showing S3 storage configuration and instant sharing", + title: + "Cap self-hosted screen recording demo showing S3 storage configuration and instant sharing", }, }, diff --git a/apps/web/components/pages/seo/VideoRecordingSoftwarePage.tsx b/apps/web/components/pages/seo/VideoRecordingSoftwarePage.tsx index 56f3ee8d190..c083a3e7211 100644 --- a/apps/web/components/pages/seo/VideoRecordingSoftwarePage.tsx +++ b/apps/web/components/pages/seo/VideoRecordingSoftwarePage.tsx @@ -254,7 +254,8 @@ export const videoRecordingSoftwareContent: SeoPageContent = { video: { iframe: { src: "https://www.rend.so/embed/10512af0-b922-4efa-8974-f8f14fc1886a?accent=3e63dd", - title: "Cap video recording software demo showing HD screen capture, webcam overlay, and instant sharing", + title: + "Cap video recording software demo showing HD screen capture, webcam overlay, and instant sharing", }, },