From 239468a80f07da5e77ca50da418081ff83b7fa5d Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:51:27 +0000 Subject: [PATCH 1/6] refactor(desktop-gpui): own the resources borrowed from the Tauri app --- .../assets/backgrounds/blue/1-thumbnail.jpg | Bin .../assets/backgrounds/blue/1.jpg | Bin .../assets/backgrounds/blue/2-thumbnail.jpg | Bin .../assets/backgrounds/blue/2.jpg | Bin .../assets/backgrounds/blue/3-thumbnail.jpg | Bin .../assets/backgrounds/blue/3.jpg | Bin .../assets/backgrounds/blue/4-thumbnail.jpg | Bin .../assets/backgrounds/blue/4.jpg | Bin .../assets/backgrounds/blue/5-thumbnail.jpg | Bin .../assets/backgrounds/blue/5.jpg | Bin .../assets/backgrounds/blue/6-thumbnail.jpg | Bin .../assets/backgrounds/blue/6.jpg | Bin .../cities/liverpool-thumbnail.jpg | Bin .../assets/backgrounds/cities/liverpool.jpg | Bin .../backgrounds/cities/london-thumbnail.jpg | Bin .../assets/backgrounds/cities/london.jpg | Bin .../backgrounds/cities/miami-thumbnail.jpg | Bin .../assets/backgrounds/cities/miami.jpg | Bin .../backgrounds/cities/monaco-thumbnail.jpg | Bin .../assets/backgrounds/cities/monaco.jpg | Bin .../backgrounds/cities/nyc-thumbnail.jpg | Bin .../assets/backgrounds/cities/nyc.jpg | Bin .../backgrounds/cities/rome-thumbnail.jpg | Bin .../assets/backgrounds/cities/rome.jpg | Bin .../cities/santorini-thumbnail.jpg | Bin .../assets/backgrounds/cities/santorini.jpg | Bin .../backgrounds/cities/sf-thumbnail.jpg | Bin .../assets/backgrounds/cities/sf.jpg | Bin .../assets/backgrounds/dark/1-thumbnail.jpg | Bin .../assets/backgrounds/dark/1.jpg | Bin .../assets/backgrounds/dark/2-thumbnail.jpg | Bin .../assets/backgrounds/dark/2.jpg | Bin .../assets/backgrounds/dark/3-thumbnail.jpg | Bin .../assets/backgrounds/dark/3.jpg | Bin .../assets/backgrounds/dark/4-thumbnail.jpg | Bin .../assets/backgrounds/dark/4.jpg | Bin .../assets/backgrounds/dark/5-thumbnail.jpg | Bin .../assets/backgrounds/dark/5.jpg | Bin .../assets/backgrounds/dark/6-thumbnail.jpg | Bin .../assets/backgrounds/dark/6.jpg | Bin .../macOS/sequoia-dark-thumbnail.jpg | Bin .../assets/backgrounds/macOS/sequoia-dark.jpg | Bin .../macOS/sequoia-light-thumbnail.jpg | Bin .../backgrounds/macOS/sequoia-light.jpg | Bin .../macOS/sonoma-clouds-thumbnail.jpg | Bin .../backgrounds/macOS/sonoma-clouds.jpg | Bin .../macOS/sonoma-dark-thumbnail.jpg | Bin .../assets/backgrounds/macOS/sonoma-dark.jpg | Bin .../macOS/sonoma-evening-thumbnail.jpg | Bin .../backgrounds/macOS/sonoma-evening.jpg | Bin .../macOS/sonoma-fromabove-thumbnail.jpg | Bin .../backgrounds/macOS/sonoma-fromabove.jpg | Bin .../macOS/sonoma-horizon-thumbnail.jpg | Bin .../backgrounds/macOS/sonoma-horizon.jpg | Bin .../macOS/sonoma-light-thumbnail.jpg | Bin .../assets/backgrounds/macOS/sonoma-light.jpg | Bin .../macOS/sonoma-river-thumbnail.jpg | Bin .../assets/backgrounds/macOS/sonoma-river.jpg | Bin .../macOS/tahoe-dark-thumbnail.jpg | Bin .../assets/backgrounds/macOS/tahoe-dark.jpg | Bin .../macOS/tahoe-dawn-min-thumbnail.jpg | Bin .../backgrounds/macOS/tahoe-dawn-min.jpg | Bin .../macOS/tahoe-day-min-thumbnail.jpg | Bin .../backgrounds/macOS/tahoe-day-min.jpg | Bin .../macOS/tahoe-dusk-min-thumbnail.jpg | Bin .../backgrounds/macOS/tahoe-dusk-min.jpg | Bin .../macOS/tahoe-light-thumbnail.jpg | Bin .../assets/backgrounds/macOS/tahoe-light.jpg | Bin .../macOS/tahoe-night-min-thumbnail.jpg | Bin .../backgrounds/macOS/tahoe-night-min.jpg | Bin .../macOS/ventura-dark-thumbnail.jpg | Bin .../assets/backgrounds/macOS/ventura-dark.jpg | Bin .../macOS/ventura-semi-dark-thumbnail.jpg | Bin .../backgrounds/macOS/ventura-semi-dark.jpg | Bin .../backgrounds/macOS/ventura-thumbnail.jpg | Bin .../assets/backgrounds/macOS/ventura.jpg | Bin .../assets/backgrounds/orange/1-thumbnail.jpg | Bin .../assets/backgrounds/orange/1.jpg | Bin .../assets/backgrounds/orange/2-thumbnail.jpg | Bin .../assets/backgrounds/orange/2.jpg | Bin .../assets/backgrounds/orange/3-thumbnail.jpg | Bin .../assets/backgrounds/orange/3.jpg | Bin .../assets/backgrounds/orange/4-thumbnail.jpg | Bin .../assets/backgrounds/orange/4.jpg | Bin .../assets/backgrounds/orange/5-thumbnail.jpg | Bin .../assets/backgrounds/orange/5.jpg | Bin .../assets/backgrounds/orange/6-thumbnail.jpg | Bin .../assets/backgrounds/orange/6.jpg | Bin .../assets/backgrounds/orange/7-thumbnail.jpg | Bin .../assets/backgrounds/orange/7.jpg | Bin .../assets/backgrounds/orange/8-thumbnail.jpg | Bin .../assets/backgrounds/orange/8.jpg | Bin .../assets/backgrounds/orange/9-thumbnail.jpg | Bin .../assets/backgrounds/orange/9.jpg | Bin .../assets/backgrounds/purple/1-thumbnail.jpg | Bin .../assets/backgrounds/purple/1.jpg | Bin .../assets/backgrounds/purple/2-thumbnail.jpg | Bin .../assets/backgrounds/purple/2.jpg | Bin .../assets/backgrounds/purple/3-thumbnail.jpg | Bin .../assets/backgrounds/purple/3.jpg | Bin .../assets/backgrounds/purple/4-thumbnail.jpg | Bin .../assets/backgrounds/purple/4.jpg | Bin .../assets/backgrounds/purple/5-thumbnail.jpg | Bin .../assets/backgrounds/purple/5.jpg | Bin .../assets/backgrounds/purple/6-thumbnail.jpg | Bin .../assets/backgrounds/purple/6.jpg | Bin ...empty-streets-dreamstate-openmindaudio.mp3 | Bin .../assets/music/cassette-retrositive.mp3 | Bin .../good-night-lofi-cozy-chill-fassounds.mp3 | Bin .../assets/music/lofi-beats-mirostar.mp3 | Bin .../assets/music/lofi-cinematic-pulsebox.mp3 | Bin .../assets/music/lofi-hip-hop-leberch.mp3 | Bin .../assets/music/lofi-smooth-pulsebox.mp3 | Bin ...tudy-calm-peaceful-chill-hop-fassounds.mp3 | Bin .../music/raindrops-lofi-sleep-bluelike.mp3 | Bin .../sunday-mood-lofi-cafe-upbeat-bluelike.mp3 | Bin .../assets/onboarding}/cursor-macos.svg | 0 .../assets/onboarding}/cursor-windows.svg | 0 .../tears-and-fireflies-adi-goldstein.mp3 | Bin .../assets}/sounds/action.ogg | Bin .../assets}/sounds/start-recording.ogg | Bin .../assets}/sounds/stop-recording.ogg | Bin .../assets/tray}/tray-default-icon.png | Bin .../binaries/.gitignore | 0 apps/desktop-gpui/build.rs | 2 +- .../icons/128x128.png | Bin .../icons/128x128@2x.png | Bin .../icons/32x32.png | Bin .../icons/Square310x310Logo.png | Bin .../src-tauri => desktop-gpui}/icons/icon.ico | Bin .../src-tauri => desktop-gpui}/icons/icon.png | Bin .../icons/macos/icon.icns | Bin .../packaging/macos}/Entitlements.plist | 0 .../packaging/macos}/dmg-background.png | Bin .../packaging/windows}/nsis-header.bmp | Bin .../packaging/windows}/nsis-sidebar.bmp | Bin apps/desktop-gpui/src/app_sounds.rs | 7 ++-- .../src/app_sounds}/recording_start_sound.rs | 0 apps/desktop-gpui/src/assets.rs | 6 +-- apps/desktop-gpui/src/editor_audio.rs | 5 +-- apps/desktop-gpui/src/editor_clips.rs | 14 +++---- apps/desktop-gpui/src/editor_sidebar.rs | 29 +------------ apps/desktop-gpui/src/tray/windows.rs | 3 +- apps/desktop-gpui/tauri.conf.json | 39 ++++++++---------- .../tests/data}/clip-thumbnail-gop.mp4 | Bin .../tests/data}/clip-thumbnail-long-gop.mp4 | Bin apps/web/components/pages/HomeTwo/cursors.tsx | 2 +- .../pages/HomeTwo/demo/capIcons.tsx | 2 +- packages/utils/src/helpers.ts | 2 +- scripts/build-desktop-binaries.mjs | 8 +--- scripts/build-desktop-binaries.sh | 2 +- scripts/generate-background-thumbnails.mjs | 2 +- scripts/verify-recording-pipeline.mjs | 9 +++- scripts/windows-release-signing.ps1 | 4 +- 154 files changed, 49 insertions(+), 87 deletions(-) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/1-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/1.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/2-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/2.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/3-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/3.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/4-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/4.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/5-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/5.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/6-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/blue/6.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/liverpool-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/liverpool.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/london-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/london.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/miami-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/miami.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/monaco-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/monaco.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/nyc-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/nyc.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/rome-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/rome.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/santorini-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/santorini.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/sf-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/cities/sf.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/1-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/1.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/2-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/2.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/3-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/3.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/4-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/4.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/5-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/5.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/6-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/dark/6.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sequoia-dark-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sequoia-dark.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sequoia-light-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sequoia-light.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-clouds-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-clouds.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-dark-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-dark.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-evening-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-evening.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-fromabove-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-fromabove.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-horizon-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-horizon.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-light-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-light.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-river-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/sonoma-river.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-dark-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-dark.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-dawn-min-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-dawn-min.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-day-min-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-day-min.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-dusk-min-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-dusk-min.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-light-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-light.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-night-min-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/tahoe-night-min.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/ventura-dark-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/ventura-dark.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/ventura-semi-dark-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/ventura-semi-dark.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/ventura-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/macOS/ventura.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/1-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/1.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/2-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/2.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/3-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/3.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/4-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/4.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/5-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/5.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/6-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/6.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/7-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/7.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/8-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/8.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/9-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/orange/9.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/1-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/1.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/2-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/2.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/3-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/3.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/4-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/4.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/5-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/5.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/6-thumbnail.jpg (100%) rename apps/{desktop/src-tauri => desktop-gpui}/assets/backgrounds/purple/6.jpg (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/ambient-trap-empty-streets-dreamstate-openmindaudio.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/cassette-retrositive.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/good-night-lofi-cozy-chill-fassounds.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/lofi-beats-mirostar.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/lofi-cinematic-pulsebox.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/lofi-hip-hop-leberch.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/lofi-smooth-pulsebox.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/lofi-study-calm-peaceful-chill-hop-fassounds.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/raindrops-lofi-sleep-bluelike.mp3 (100%) rename apps/{desktop/src => desktop-gpui}/assets/music/sunday-mood-lofi-cafe-upbeat-bluelike.mp3 (100%) rename {packages/ui-solid/icons => apps/desktop-gpui/assets/onboarding}/cursor-macos.svg (100%) rename {packages/ui-solid/icons => apps/desktop-gpui/assets/onboarding}/cursor-windows.svg (100%) rename apps/{desktop/src/assets => desktop-gpui/assets/onboarding}/tears-and-fireflies-adi-goldstein.mp3 (100%) rename apps/{desktop/src-tauri => desktop-gpui/assets}/sounds/action.ogg (100%) rename apps/{desktop/src-tauri => desktop-gpui/assets}/sounds/start-recording.ogg (100%) rename apps/{desktop/src-tauri => desktop-gpui/assets}/sounds/stop-recording.ogg (100%) rename apps/{desktop/src-tauri/icons => desktop-gpui/assets/tray}/tray-default-icon.png (100%) rename apps/{desktop/src-tauri => desktop-gpui}/binaries/.gitignore (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/128x128.png (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/128x128@2x.png (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/32x32.png (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/Square310x310Logo.png (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/icon.ico (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/icon.png (100%) rename apps/{desktop/src-tauri => desktop-gpui}/icons/macos/icon.icns (100%) rename apps/{desktop/src-tauri => desktop-gpui/packaging/macos}/Entitlements.plist (100%) rename apps/{desktop/src-tauri/assets => desktop-gpui/packaging/macos}/dmg-background.png (100%) rename apps/{desktop/src-tauri/assets => desktop-gpui/packaging/windows}/nsis-header.bmp (100%) rename apps/{desktop/src-tauri/assets => desktop-gpui/packaging/windows}/nsis-sidebar.bmp (100%) rename apps/{desktop/src-tauri/src => desktop-gpui/src/app_sounds}/recording_start_sound.rs (100%) rename apps/{desktop/src-tauri/test-data => desktop-gpui/tests/data}/clip-thumbnail-gop.mp4 (100%) rename apps/{desktop/src-tauri/test-data => desktop-gpui/tests/data}/clip-thumbnail-long-gop.mp4 (100%) diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/1-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/blue/1-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/1-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/1-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/1.jpg b/apps/desktop-gpui/assets/backgrounds/blue/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/1.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/2-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/blue/2-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/2-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/2-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/2.jpg b/apps/desktop-gpui/assets/backgrounds/blue/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/2.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/3-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/blue/3-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/3-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/3-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/3.jpg b/apps/desktop-gpui/assets/backgrounds/blue/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/3.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/4-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/blue/4-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/4-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/4-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/4.jpg b/apps/desktop-gpui/assets/backgrounds/blue/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/4.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/5-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/blue/5-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/5-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/5-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/5.jpg b/apps/desktop-gpui/assets/backgrounds/blue/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/5.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/6-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/blue/6-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/6-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/6-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/blue/6.jpg b/apps/desktop-gpui/assets/backgrounds/blue/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/blue/6.jpg rename to apps/desktop-gpui/assets/backgrounds/blue/6.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/liverpool-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/liverpool-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/liverpool-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/liverpool-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/liverpool.jpg b/apps/desktop-gpui/assets/backgrounds/cities/liverpool.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/liverpool.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/liverpool.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/london-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/london-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/london-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/london-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/london.jpg b/apps/desktop-gpui/assets/backgrounds/cities/london.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/london.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/london.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/miami-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/miami-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/miami-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/miami-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/miami.jpg b/apps/desktop-gpui/assets/backgrounds/cities/miami.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/miami.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/miami.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/monaco-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/monaco-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/monaco-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/monaco-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/monaco.jpg b/apps/desktop-gpui/assets/backgrounds/cities/monaco.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/monaco.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/monaco.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/nyc-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/nyc-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/nyc-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/nyc-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/nyc.jpg b/apps/desktop-gpui/assets/backgrounds/cities/nyc.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/nyc.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/nyc.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/rome-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/rome-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/rome-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/rome-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/rome.jpg b/apps/desktop-gpui/assets/backgrounds/cities/rome.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/rome.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/rome.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/santorini-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/santorini-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/santorini-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/santorini-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/santorini.jpg b/apps/desktop-gpui/assets/backgrounds/cities/santorini.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/santorini.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/santorini.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/sf-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/cities/sf-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/sf-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/sf-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/cities/sf.jpg b/apps/desktop-gpui/assets/backgrounds/cities/sf.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/cities/sf.jpg rename to apps/desktop-gpui/assets/backgrounds/cities/sf.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/1-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/dark/1-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/1-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/1-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/1.jpg b/apps/desktop-gpui/assets/backgrounds/dark/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/1.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/2-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/dark/2-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/2-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/2-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/2.jpg b/apps/desktop-gpui/assets/backgrounds/dark/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/2.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/3-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/dark/3-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/3-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/3-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/3.jpg b/apps/desktop-gpui/assets/backgrounds/dark/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/3.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/4-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/dark/4-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/4-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/4-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/4.jpg b/apps/desktop-gpui/assets/backgrounds/dark/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/4.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/5-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/dark/5-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/5-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/5-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/5.jpg b/apps/desktop-gpui/assets/backgrounds/dark/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/5.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/6-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/dark/6-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/6-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/6-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/dark/6.jpg b/apps/desktop-gpui/assets/backgrounds/dark/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/dark/6.jpg rename to apps/desktop-gpui/assets/backgrounds/dark/6.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-dark-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sequoia-dark-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-dark-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sequoia-dark-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-dark.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sequoia-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-light-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sequoia-light-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-light-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sequoia-light-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sequoia-light.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sequoia-light.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-clouds-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-clouds-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-clouds-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-clouds-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-clouds.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-clouds.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-dark-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-dark-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-dark-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-dark-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-dark.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-evening-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-evening-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-evening-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-evening-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-evening.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-evening.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-fromabove-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-fromabove-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-fromabove-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-fromabove-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-fromabove.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-fromabove.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-horizon-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-horizon-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-horizon-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-horizon-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-horizon.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-horizon.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-light-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-light-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-light-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-light-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-light.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-light.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-river-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/sonoma-river-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-river-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/sonoma-river-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/sonoma-river.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/sonoma-river.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dark-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/tahoe-dark-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dark-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/tahoe-dark-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dark.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/tahoe-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dawn-min-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/tahoe-dawn-min-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dawn-min-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/tahoe-dawn-min-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dawn-min.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/tahoe-dawn-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-day-min-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/tahoe-day-min-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-day-min-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/tahoe-day-min-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-day-min.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/tahoe-day-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dusk-min-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/tahoe-dusk-min-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dusk-min-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/tahoe-dusk-min-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-dusk-min.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/tahoe-dusk-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-light-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/tahoe-light-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-light-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/tahoe-light-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-light.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/tahoe-light.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-night-min-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/tahoe-night-min-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-night-min-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/tahoe-night-min-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/tahoe-night-min.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/tahoe-night-min.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-dark-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/ventura-dark-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-dark-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/ventura-dark-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-dark.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/ventura-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-semi-dark-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/ventura-semi-dark-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-semi-dark-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/ventura-semi-dark-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-semi-dark.jpg b/apps/desktop-gpui/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-gpui/assets/backgrounds/macOS/ventura-semi-dark.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/ventura-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/ventura-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/macOS/ventura.jpg b/apps/desktop-gpui/assets/backgrounds/macOS/ventura.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/macOS/ventura.jpg rename to apps/desktop-gpui/assets/backgrounds/macOS/ventura.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/1-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/1-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/1-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/1-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/1.jpg b/apps/desktop-gpui/assets/backgrounds/orange/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/1.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/2-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/2-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/2-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/2-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/2.jpg b/apps/desktop-gpui/assets/backgrounds/orange/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/2.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/3-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/3-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/3-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/3-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/3.jpg b/apps/desktop-gpui/assets/backgrounds/orange/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/3.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/4-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/4-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/4-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/4-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/4.jpg b/apps/desktop-gpui/assets/backgrounds/orange/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/4.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/5-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/5-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/5-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/5-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/5.jpg b/apps/desktop-gpui/assets/backgrounds/orange/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/5.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/6-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/6-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/6-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/6-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/6.jpg b/apps/desktop-gpui/assets/backgrounds/orange/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/6.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/6.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/7-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/7-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/7-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/7-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/7.jpg b/apps/desktop-gpui/assets/backgrounds/orange/7.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/7.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/7.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/8-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/8-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/8-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/8-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/8.jpg b/apps/desktop-gpui/assets/backgrounds/orange/8.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/8.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/8.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/9-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/orange/9-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/9-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/9-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/orange/9.jpg b/apps/desktop-gpui/assets/backgrounds/orange/9.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/orange/9.jpg rename to apps/desktop-gpui/assets/backgrounds/orange/9.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/1-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/purple/1-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/1-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/1-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/1.jpg b/apps/desktop-gpui/assets/backgrounds/purple/1.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/1.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/1.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/2-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/purple/2-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/2-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/2-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/2.jpg b/apps/desktop-gpui/assets/backgrounds/purple/2.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/2.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/2.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/3-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/purple/3-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/3-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/3-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/3.jpg b/apps/desktop-gpui/assets/backgrounds/purple/3.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/3.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/3.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/4-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/purple/4-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/4-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/4-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/4.jpg b/apps/desktop-gpui/assets/backgrounds/purple/4.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/4.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/4.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/5-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/purple/5-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/5-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/5-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/5.jpg b/apps/desktop-gpui/assets/backgrounds/purple/5.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/5.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/5.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/6-thumbnail.jpg b/apps/desktop-gpui/assets/backgrounds/purple/6-thumbnail.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/6-thumbnail.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/6-thumbnail.jpg diff --git a/apps/desktop/src-tauri/assets/backgrounds/purple/6.jpg b/apps/desktop-gpui/assets/backgrounds/purple/6.jpg similarity index 100% rename from apps/desktop/src-tauri/assets/backgrounds/purple/6.jpg rename to apps/desktop-gpui/assets/backgrounds/purple/6.jpg diff --git a/apps/desktop/src/assets/music/ambient-trap-empty-streets-dreamstate-openmindaudio.mp3 b/apps/desktop-gpui/assets/music/ambient-trap-empty-streets-dreamstate-openmindaudio.mp3 similarity index 100% rename from apps/desktop/src/assets/music/ambient-trap-empty-streets-dreamstate-openmindaudio.mp3 rename to apps/desktop-gpui/assets/music/ambient-trap-empty-streets-dreamstate-openmindaudio.mp3 diff --git a/apps/desktop/src/assets/music/cassette-retrositive.mp3 b/apps/desktop-gpui/assets/music/cassette-retrositive.mp3 similarity index 100% rename from apps/desktop/src/assets/music/cassette-retrositive.mp3 rename to apps/desktop-gpui/assets/music/cassette-retrositive.mp3 diff --git a/apps/desktop/src/assets/music/good-night-lofi-cozy-chill-fassounds.mp3 b/apps/desktop-gpui/assets/music/good-night-lofi-cozy-chill-fassounds.mp3 similarity index 100% rename from apps/desktop/src/assets/music/good-night-lofi-cozy-chill-fassounds.mp3 rename to apps/desktop-gpui/assets/music/good-night-lofi-cozy-chill-fassounds.mp3 diff --git a/apps/desktop/src/assets/music/lofi-beats-mirostar.mp3 b/apps/desktop-gpui/assets/music/lofi-beats-mirostar.mp3 similarity index 100% rename from apps/desktop/src/assets/music/lofi-beats-mirostar.mp3 rename to apps/desktop-gpui/assets/music/lofi-beats-mirostar.mp3 diff --git a/apps/desktop/src/assets/music/lofi-cinematic-pulsebox.mp3 b/apps/desktop-gpui/assets/music/lofi-cinematic-pulsebox.mp3 similarity index 100% rename from apps/desktop/src/assets/music/lofi-cinematic-pulsebox.mp3 rename to apps/desktop-gpui/assets/music/lofi-cinematic-pulsebox.mp3 diff --git a/apps/desktop/src/assets/music/lofi-hip-hop-leberch.mp3 b/apps/desktop-gpui/assets/music/lofi-hip-hop-leberch.mp3 similarity index 100% rename from apps/desktop/src/assets/music/lofi-hip-hop-leberch.mp3 rename to apps/desktop-gpui/assets/music/lofi-hip-hop-leberch.mp3 diff --git a/apps/desktop/src/assets/music/lofi-smooth-pulsebox.mp3 b/apps/desktop-gpui/assets/music/lofi-smooth-pulsebox.mp3 similarity index 100% rename from apps/desktop/src/assets/music/lofi-smooth-pulsebox.mp3 rename to apps/desktop-gpui/assets/music/lofi-smooth-pulsebox.mp3 diff --git a/apps/desktop/src/assets/music/lofi-study-calm-peaceful-chill-hop-fassounds.mp3 b/apps/desktop-gpui/assets/music/lofi-study-calm-peaceful-chill-hop-fassounds.mp3 similarity index 100% rename from apps/desktop/src/assets/music/lofi-study-calm-peaceful-chill-hop-fassounds.mp3 rename to apps/desktop-gpui/assets/music/lofi-study-calm-peaceful-chill-hop-fassounds.mp3 diff --git a/apps/desktop/src/assets/music/raindrops-lofi-sleep-bluelike.mp3 b/apps/desktop-gpui/assets/music/raindrops-lofi-sleep-bluelike.mp3 similarity index 100% rename from apps/desktop/src/assets/music/raindrops-lofi-sleep-bluelike.mp3 rename to apps/desktop-gpui/assets/music/raindrops-lofi-sleep-bluelike.mp3 diff --git a/apps/desktop/src/assets/music/sunday-mood-lofi-cafe-upbeat-bluelike.mp3 b/apps/desktop-gpui/assets/music/sunday-mood-lofi-cafe-upbeat-bluelike.mp3 similarity index 100% rename from apps/desktop/src/assets/music/sunday-mood-lofi-cafe-upbeat-bluelike.mp3 rename to apps/desktop-gpui/assets/music/sunday-mood-lofi-cafe-upbeat-bluelike.mp3 diff --git a/packages/ui-solid/icons/cursor-macos.svg b/apps/desktop-gpui/assets/onboarding/cursor-macos.svg similarity index 100% rename from packages/ui-solid/icons/cursor-macos.svg rename to apps/desktop-gpui/assets/onboarding/cursor-macos.svg diff --git a/packages/ui-solid/icons/cursor-windows.svg b/apps/desktop-gpui/assets/onboarding/cursor-windows.svg similarity index 100% rename from packages/ui-solid/icons/cursor-windows.svg rename to apps/desktop-gpui/assets/onboarding/cursor-windows.svg diff --git a/apps/desktop/src/assets/tears-and-fireflies-adi-goldstein.mp3 b/apps/desktop-gpui/assets/onboarding/tears-and-fireflies-adi-goldstein.mp3 similarity index 100% rename from apps/desktop/src/assets/tears-and-fireflies-adi-goldstein.mp3 rename to apps/desktop-gpui/assets/onboarding/tears-and-fireflies-adi-goldstein.mp3 diff --git a/apps/desktop/src-tauri/sounds/action.ogg b/apps/desktop-gpui/assets/sounds/action.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/action.ogg rename to apps/desktop-gpui/assets/sounds/action.ogg diff --git a/apps/desktop/src-tauri/sounds/start-recording.ogg b/apps/desktop-gpui/assets/sounds/start-recording.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/start-recording.ogg rename to apps/desktop-gpui/assets/sounds/start-recording.ogg diff --git a/apps/desktop/src-tauri/sounds/stop-recording.ogg b/apps/desktop-gpui/assets/sounds/stop-recording.ogg similarity index 100% rename from apps/desktop/src-tauri/sounds/stop-recording.ogg rename to apps/desktop-gpui/assets/sounds/stop-recording.ogg diff --git a/apps/desktop/src-tauri/icons/tray-default-icon.png b/apps/desktop-gpui/assets/tray/tray-default-icon.png similarity index 100% rename from apps/desktop/src-tauri/icons/tray-default-icon.png rename to apps/desktop-gpui/assets/tray/tray-default-icon.png diff --git a/apps/desktop/src-tauri/binaries/.gitignore b/apps/desktop-gpui/binaries/.gitignore similarity index 100% rename from apps/desktop/src-tauri/binaries/.gitignore rename to apps/desktop-gpui/binaries/.gitignore diff --git a/apps/desktop-gpui/build.rs b/apps/desktop-gpui/build.rs index 151c6bf353a..1a417c62433 100644 --- a/apps/desktop-gpui/build.rs +++ b/apps/desktop-gpui/build.rs @@ -22,7 +22,7 @@ fn main() { println!("cargo:rustc-link-arg-bin=cap-gpui=/STACK:16777216"); } - let icon = "../desktop/src-tauri/icons/icon.ico"; + let icon = "icons/icon.ico"; println!("cargo:rerun-if-changed={icon}"); tauri_winres::WindowsResource::new() .set_icon_with_id(icon, "1") diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop-gpui/icons/128x128.png similarity index 100% rename from apps/desktop/src-tauri/icons/128x128.png rename to apps/desktop-gpui/icons/128x128.png diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop-gpui/icons/128x128@2x.png similarity index 100% rename from apps/desktop/src-tauri/icons/128x128@2x.png rename to apps/desktop-gpui/icons/128x128@2x.png diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop-gpui/icons/32x32.png similarity index 100% rename from apps/desktop/src-tauri/icons/32x32.png rename to apps/desktop-gpui/icons/32x32.png diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop-gpui/icons/Square310x310Logo.png similarity index 100% rename from apps/desktop/src-tauri/icons/Square310x310Logo.png rename to apps/desktop-gpui/icons/Square310x310Logo.png diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop-gpui/icons/icon.ico similarity index 100% rename from apps/desktop/src-tauri/icons/icon.ico rename to apps/desktop-gpui/icons/icon.ico diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop-gpui/icons/icon.png similarity index 100% rename from apps/desktop/src-tauri/icons/icon.png rename to apps/desktop-gpui/icons/icon.png diff --git a/apps/desktop/src-tauri/icons/macos/icon.icns b/apps/desktop-gpui/icons/macos/icon.icns similarity index 100% rename from apps/desktop/src-tauri/icons/macos/icon.icns rename to apps/desktop-gpui/icons/macos/icon.icns diff --git a/apps/desktop/src-tauri/Entitlements.plist b/apps/desktop-gpui/packaging/macos/Entitlements.plist similarity index 100% rename from apps/desktop/src-tauri/Entitlements.plist rename to apps/desktop-gpui/packaging/macos/Entitlements.plist diff --git a/apps/desktop/src-tauri/assets/dmg-background.png b/apps/desktop-gpui/packaging/macos/dmg-background.png similarity index 100% rename from apps/desktop/src-tauri/assets/dmg-background.png rename to apps/desktop-gpui/packaging/macos/dmg-background.png diff --git a/apps/desktop/src-tauri/assets/nsis-header.bmp b/apps/desktop-gpui/packaging/windows/nsis-header.bmp similarity index 100% rename from apps/desktop/src-tauri/assets/nsis-header.bmp rename to apps/desktop-gpui/packaging/windows/nsis-header.bmp diff --git a/apps/desktop/src-tauri/assets/nsis-sidebar.bmp b/apps/desktop-gpui/packaging/windows/nsis-sidebar.bmp similarity index 100% rename from apps/desktop/src-tauri/assets/nsis-sidebar.bmp rename to apps/desktop-gpui/packaging/windows/nsis-sidebar.bmp diff --git a/apps/desktop-gpui/src/app_sounds.rs b/apps/desktop-gpui/src/app_sounds.rs index c2ab9dd6400..2e59938c37d 100644 --- a/apps/desktop-gpui/src/app_sounds.rs +++ b/apps/desktop-gpui/src/app_sounds.rs @@ -3,7 +3,6 @@ use std::io::Cursor; use anyhow::Context as _; use rodio::{Decoder, OutputStream, Sink}; -#[path = "../../desktop/src-tauri/src/recording_start_sound.rs"] mod recording_start_sound; pub use recording_start_sound::StartCue; @@ -27,12 +26,12 @@ impl AppSound { fn bytes(self) -> &'static [u8] { match self { Self::StartRecording => { - include_bytes!("../../desktop/src-tauri/sounds/start-recording.ogg") + include_bytes!("../assets/sounds/start-recording.ogg") } Self::StopRecording => { - include_bytes!("../../desktop/src-tauri/sounds/stop-recording.ogg") + include_bytes!("../assets/sounds/stop-recording.ogg") } - Self::Notification => include_bytes!("../../desktop/src-tauri/sounds/action.ogg"), + Self::Notification => include_bytes!("../assets/sounds/action.ogg"), } } diff --git a/apps/desktop/src-tauri/src/recording_start_sound.rs b/apps/desktop-gpui/src/app_sounds/recording_start_sound.rs similarity index 100% rename from apps/desktop/src-tauri/src/recording_start_sound.rs rename to apps/desktop-gpui/src/app_sounds/recording_start_sound.rs diff --git a/apps/desktop-gpui/src/assets.rs b/apps/desktop-gpui/src/assets.rs index f94bfb25386..0a89510493b 100644 --- a/apps/desktop-gpui/src/assets.rs +++ b/apps/desktop-gpui/src/assets.rs @@ -248,9 +248,9 @@ const ONBOARDING: &[(&str, &[u8])] = &[ ("onboarding/mist.svg", br##""##), ("onboarding/vignette.svg", br##""##), ("onboarding/wordmark.svg", br##""##), - ("onboarding/music.mp3", include_bytes!("../../desktop/src/assets/tears-and-fireflies-adi-goldstein.mp3")), - ("onboarding/cursor-macos.svg", include_bytes!("../../../packages/ui-solid/icons/cursor-macos.svg")), - ("onboarding/cursor-windows.svg", include_bytes!("../../../packages/ui-solid/icons/cursor-windows.svg")), + ("onboarding/music.mp3", include_bytes!("../assets/onboarding/tears-and-fireflies-adi-goldstein.mp3")), + ("onboarding/cursor-macos.svg", include_bytes!("../assets/onboarding/cursor-macos.svg")), + ("onboarding/cursor-windows.svg", include_bytes!("../assets/onboarding/cursor-windows.svg")), ("onboarding/volume-2.svg", include_bytes!("../assets/icons/volume-2.svg")), ("onboarding/volume-x.svg", include_bytes!("../assets/icons/volume-x.svg")), ]; diff --git a/apps/desktop-gpui/src/editor_audio.rs b/apps/desktop-gpui/src/editor_audio.rs index fad14dccf3f..742f4ca4968 100644 --- a/apps/desktop-gpui/src/editor_audio.rs +++ b/apps/desktop-gpui/src/editor_audio.rs @@ -131,10 +131,7 @@ fn bundled_track_path_from( .iter() .map(|directory| directory.join("assets/music").join(&file)) .collect::>(); - candidates.extend([ - manifest.join("../desktop/src/assets/music").join(&file), - manifest.join("assets/music").join(&file), - ]); + candidates.push(manifest.join("assets/music").join(&file)); candidates.into_iter().find(|path| path.is_file()) } diff --git a/apps/desktop-gpui/src/editor_clips.rs b/apps/desktop-gpui/src/editor_clips.rs index 23a0dccd4b0..872d3592965 100644 --- a/apps/desktop-gpui/src/editor_clips.rs +++ b/apps/desktop-gpui/src/editor_clips.rs @@ -3539,9 +3539,7 @@ mod tests { #[test] fn split_thumbnails_use_requested_time_inside_keyframe_interval() { - let fixture = ImportFixture::new(include_bytes!( - "../../desktop/src-tauri/test-data/clip-thumbnail-gop.mp4" - )); + let fixture = ImportFixture::new(include_bytes!("../tests/data/clip-thumbnail-gop.mp4")); for (time, expected_channel) in [(0.0, 2), (1.5, 1), (2.5, 0), (3.0, 0)] { let thumbnail = decode_thumbnail_frame(&fixture.source, time).unwrap(); let data = thumbnail.as_bytes(0).unwrap(); @@ -3560,9 +3558,8 @@ mod tests { #[test] fn long_gop_thumbnails_reach_requested_time_and_preserve_eof_fallback() { - let fixture = ImportFixture::new(include_bytes!( - "../../desktop/src-tauri/test-data/clip-thumbnail-long-gop.mp4" - )); + let fixture = + ImportFixture::new(include_bytes!("../tests/data/clip-thumbnail-long-gop.mp4")); for time in [8.5, 10.0, 11.0] { let thumbnail = decode_thumbnail_frame(&fixture.source, time).unwrap(); let data = thumbnail.as_bytes(0).unwrap(); @@ -3576,9 +3573,8 @@ mod tests { #[test] fn exhausted_decode_budget_does_not_return_partial_thumbnail() { - let fixture = ImportFixture::new(include_bytes!( - "../../desktop/src-tauri/test-data/clip-thumbnail-long-gop.mp4" - )); + let fixture = + ImportFixture::new(include_bytes!("../tests/data/clip-thumbnail-long-gop.mp4")); for (packet_limit, timeout, expected_error) in [ (240, std::time::Duration::from_secs(2), "packet budget"), ( diff --git a/apps/desktop-gpui/src/editor_sidebar.rs b/apps/desktop-gpui/src/editor_sidebar.rs index 443ce9226d2..52febe73953 100644 --- a/apps/desktop-gpui/src/editor_sidebar.rs +++ b/apps/desktop-gpui/src/editor_sidebar.rs @@ -534,37 +534,10 @@ fn wallpaper_dir_from( candidates.push(PathBuf::from( "/Applications/Cap.app/Contents/Resources/assets/backgrounds", )); - candidates.push( - manifest - .join("../desktop/src-tauri/assets/backgrounds") - .clean(), - ); + candidates.push(manifest.join("assets/backgrounds")); candidates.into_iter().find(|path| path.is_dir()) } -/// `PathBuf::canonicalize` fails on a path that does not exist, and a `..` -/// segment left in place reads badly in a log line, so the `../` is folded -/// here instead. -trait Clean { - fn clean(self) -> PathBuf; -} - -impl Clean for PathBuf { - fn clean(self) -> PathBuf { - let mut out = PathBuf::new(); - for component in self.components() { - match component { - std::path::Component::ParentDir => { - out.pop(); - } - std::path::Component::CurDir => {} - other => out.push(other), - } - } - out - } -} - pub fn wallpaper_path(id: &str) -> Option { if !WALLPAPER_NAMES.contains(&id) { return None; diff --git a/apps/desktop-gpui/src/tray/windows.rs b/apps/desktop-gpui/src/tray/windows.rs index aec176d7c66..1e311ff669c 100644 --- a/apps/desktop-gpui/src/tray/windows.rs +++ b/apps/desktop-gpui/src/tray/windows.rs @@ -15,8 +15,7 @@ use super::{ use crate::{i18n::tr, main_window::Mode}; const TRAY_ID: &str = "cap-gpui-tray"; -const DEFAULT_ICON: &[u8] = - include_bytes!("../../../desktop/src-tauri/icons/tray-default-icon.png"); +const DEFAULT_ICON: &[u8] = include_bytes!("../../assets/tray/tray-default-icon.png"); const STOP_ICON: &[u8] = include_bytes!("../../assets/tray/tray-stop-icon.png"); enum Event { diff --git a/apps/desktop-gpui/tauri.conf.json b/apps/desktop-gpui/tauri.conf.json index bc6696d7056..ed29600ca92 100644 --- a/apps/desktop-gpui/tauri.conf.json +++ b/apps/desktop-gpui/tauri.conf.json @@ -22,25 +22,22 @@ "targets": "all", "category": "Video", "longDescription": "Cap is the open source alternative to Loom. Lightweight, powerful, and cross-platform. Record and share in seconds.", - "externalBin": [ - "../desktop/src-tauri/binaries/cap-muxer", - "../desktop/src-tauri/binaries/cap-cli" - ], + "externalBin": ["binaries/cap-muxer", "binaries/cap-cli"], "icon": [ - "../desktop/src-tauri/icons/32x32.png", - "../desktop/src-tauri/icons/128x128.png", - "../desktop/src-tauri/icons/128x128@2x.png", - "../desktop/src-tauri/icons/macos/icon.icns", - "../desktop/src-tauri/icons/icon.ico" + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/macos/icon.icns", + "icons/icon.ico" ], "resources": { - "../desktop/src-tauri/assets/backgrounds/macOS/*": "assets/backgrounds/macOS/", - "../desktop/src-tauri/assets/backgrounds/blue/*": "assets/backgrounds/blue/", - "../desktop/src-tauri/assets/backgrounds/cities/*": "assets/backgrounds/cities/", - "../desktop/src-tauri/assets/backgrounds/dark/*": "assets/backgrounds/dark/", - "../desktop/src-tauri/assets/backgrounds/orange/*": "assets/backgrounds/orange/", - "../desktop/src-tauri/assets/backgrounds/purple/*": "assets/backgrounds/purple/", - "../desktop/src/assets/music/*.mp3": "assets/music/" + "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/", + "assets/music/*.mp3": "assets/music/" }, "fileAssociations": [ { @@ -52,7 +49,7 @@ ], "macOS": { "minimumSystemVersion": "12.3", - "entitlements": "../desktop/src-tauri/Entitlements.plist", + "entitlements": "packaging/macos/Entitlements.plist", "frameworks": ["../../target/native-deps/Spacedrive.framework"], "files": { "Resources/en.lproj/InfoPlist.strings": "macos/en.lproj/InfoPlist.strings", @@ -64,7 +61,7 @@ "Resources/hi.lproj/InfoPlist.strings": "macos/hi.lproj/InfoPlist.strings" }, "dmg": { - "background": "../desktop/src-tauri/assets/dmg-background.png", + "background": "packaging/macos/dmg-background.png", "appPosition": { "x": 180, "y": 140 @@ -77,9 +74,9 @@ }, "windows": { "nsis": { - "headerImage": "../desktop/src-tauri/assets/nsis-header.bmp", - "sidebarImage": "../desktop/src-tauri/assets/nsis-sidebar.bmp", - "installerIcon": "../desktop/src-tauri/icons/icon.ico", + "headerImage": "packaging/windows/nsis-header.bmp", + "sidebarImage": "packaging/windows/nsis-sidebar.bmp", + "installerIcon": "icons/icon.ico", "installerHooks": "packaging/windows/installer-hooks.nsh", "compression": "lzma" }, diff --git a/apps/desktop/src-tauri/test-data/clip-thumbnail-gop.mp4 b/apps/desktop-gpui/tests/data/clip-thumbnail-gop.mp4 similarity index 100% rename from apps/desktop/src-tauri/test-data/clip-thumbnail-gop.mp4 rename to apps/desktop-gpui/tests/data/clip-thumbnail-gop.mp4 diff --git a/apps/desktop/src-tauri/test-data/clip-thumbnail-long-gop.mp4 b/apps/desktop-gpui/tests/data/clip-thumbnail-long-gop.mp4 similarity index 100% rename from apps/desktop/src-tauri/test-data/clip-thumbnail-long-gop.mp4 rename to apps/desktop-gpui/tests/data/clip-thumbnail-long-gop.mp4 diff --git a/apps/web/components/pages/HomeTwo/cursors.tsx b/apps/web/components/pages/HomeTwo/cursors.tsx index 6772f35948b..f2dd886c362 100644 --- a/apps/web/components/pages/HomeTwo/cursors.tsx +++ b/apps/web/components/pages/HomeTwo/cursors.tsx @@ -1,6 +1,6 @@ /** * The two pointers Cap Desktop actually ships, traced 1:1 from - * `packages/ui-solid/icons/cursor-macos.svg` and `cursor-windows.svg`. + * `apps/desktop-gpui/assets/onboarding/cursor-macos.svg` and `cursor-windows.svg`. * * The only change is the shadow: the source files carry it as an SVG filter * with a hard-coded id, which would collide wherever two cursors render on diff --git a/apps/web/components/pages/HomeTwo/demo/capIcons.tsx b/apps/web/components/pages/HomeTwo/demo/capIcons.tsx index 14949a96a79..913b7f4b76f 100644 --- a/apps/web/components/pages/HomeTwo/demo/capIcons.tsx +++ b/apps/web/components/pages/HomeTwo/demo/capIcons.tsx @@ -1,6 +1,6 @@ /** * Exact SVG copies of the icons the Cap desktop app renders, lifted from - * packages/ui-solid/icons/*.svg and the icon sets the app imports + * apps/desktop-gpui/assets/icons/*.svg and the icon sets the app draws from * (lucide, mdi, ph, material-symbols). Fill colours that ship as white/black * in the source files are swapped to currentColor so CSS drives them, which * is also what the app itself does via its `invert` utility. diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts index d847bc276ad..7d3afa7ce4d 100644 --- a/packages/utils/src/helpers.ts +++ b/packages/utils/src/helpers.ts @@ -14,7 +14,7 @@ export const uuidFormat = (uuid: string) => { }; export const CAP_LOGO_URL = - "https://raw.githubusercontent.com/CapSoftware/cap/main/apps/desktop/src-tauri/icons/Square310x310Logo.png"; + "https://raw.githubusercontent.com/CapSoftware/cap/main/apps/desktop-gpui/icons/Square310x310Logo.png"; export const saveLatestVideoId = (videoId: string) => { try { diff --git a/scripts/build-desktop-binaries.mjs b/scripts/build-desktop-binaries.mjs index fdda8350917..ed26ab5d4cb 100644 --- a/scripts/build-desktop-binaries.mjs +++ b/scripts/build-desktop-binaries.mjs @@ -10,13 +10,7 @@ import { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); -const binariesDir = path.join( - repoRoot, - "apps", - "desktop", - "src-tauri", - "binaries", -); +const binariesDir = path.join(repoRoot, "apps", "desktop-gpui", "binaries"); function detectHostTriple() { const result = spawnSync("rustc", ["-vV"], { encoding: "utf8" }); diff --git a/scripts/build-desktop-binaries.sh b/scripts/build-desktop-binaries.sh index ee08300f9db..f319a4ba86a 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-gpui/binaries" EXT="" case "$TARGET" in diff --git a/scripts/generate-background-thumbnails.mjs b/scripts/generate-background-thumbnails.mjs index 6c870a9dbc3..0a55a3384cb 100644 --- a/scripts/generate-background-thumbnails.mjs +++ b/scripts/generate-background-thumbnails.mjs @@ -12,7 +12,7 @@ if (args.some((argument) => argument !== "--check")) { } const check = args.includes("--check"); const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const backgrounds = join(root, "apps/desktop/src-tauri/assets/backgrounds"); +const backgrounds = join(root, "apps/desktop-gpui/assets/backgrounds"); const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg"; const temporary = await mkdtemp(join(tmpdir(), "cap-background-thumbnails-")); let count = 0; diff --git a/scripts/verify-recording-pipeline.mjs b/scripts/verify-recording-pipeline.mjs index e261f42abf9..0a15a716a63 100644 --- a/scripts/verify-recording-pipeline.mjs +++ b/scripts/verify-recording-pipeline.mjs @@ -41,7 +41,14 @@ 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-gpui", + "packaging", + "macos", + "Entitlements.plist", + ); const desktopLogDirs = ["so.cap.desktop.dev", "so.cap.desktop"].map((name) => path.join(os.homedir(), "Library", "Logs", name), ); diff --git a/scripts/windows-release-signing.ps1 b/scripts/windows-release-signing.ps1 index 297ea043fd8..33e66afb007 100644 --- a/scripts/windows-release-signing.ps1 +++ b/scripts/windows-release-signing.ps1 @@ -33,11 +33,11 @@ function Get-PayloadDefinitions { }, [pscustomobject]@{ Name = "cap-cli.exe" - Path = Join-Path $WorkspaceRoot "apps/desktop/src-tauri/binaries/cap-cli-$Target.exe" + Path = Join-Path $WorkspaceRoot "apps/desktop-gpui/binaries/cap-cli-$Target.exe" }, [pscustomobject]@{ Name = "cap-muxer.exe" - Path = Join-Path $WorkspaceRoot "apps/desktop/src-tauri/binaries/cap-muxer-$Target.exe" + Path = Join-Path $WorkspaceRoot "apps/desktop-gpui/binaries/cap-muxer-$Target.exe" } ) } From 50f2fafb5449460ad9f3b86d16ab2e3eef17a92b Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:51:27 +0000 Subject: [PATCH 2/6] refactor(desktop-gpui): remove the dev switch back to the Tauri app --- apps/desktop-gpui/dev.sh | 26 +- apps/desktop-gpui/locales/de.toml | 18 - apps/desktop-gpui/locales/en.toml | 18 - apps/desktop-gpui/locales/es.toml | 18 - apps/desktop-gpui/locales/fr.toml | 18 - apps/desktop-gpui/locales/hi.toml | 18 - apps/desktop-gpui/locales/ja.toml | 18 - apps/desktop-gpui/locales/zh-CN.toml | 18 - .../src/i18n/settings_surface_tests.rs | 54 --- apps/desktop-gpui/src/main.rs | 12 +- apps/desktop-gpui/src/settings_pages.rs | 397 +----------------- apps/desktop-gpui/src/settings_window.rs | 8 +- apps/desktop-gpui/src/store.rs | 112 +---- scripts/dev-desktop.mjs | 175 +++----- 14 files changed, 81 insertions(+), 829 deletions(-) diff --git a/apps/desktop-gpui/dev.sh b/apps/desktop-gpui/dev.sh index c073cd6239d..39157e45883 100755 --- a/apps/desktop-gpui/dev.sh +++ b/apps/desktop-gpui/dev.sh @@ -10,7 +10,7 @@ STATE_FILE="$PWD/target/dev-restore.json" BIN="$PWD/target/debug/cap-gpui" BUILD=cargo -WATCH_PATHS=(src kernels/src kernels/Cargo.toml i18n-build/src i18n-build/Cargo.toml locales assets Cargo.toml ../desktop/src-tauri/sounds) +WATCH_PATHS=(src kernels/src kernels/Cargo.toml i18n-build/src i18n-build/Cargo.toml locales assets Cargo.toml) [ -d resources ] && WATCH_PATHS+=(resources) for crate in camera scap-targets recording timestamp utils project rendering editor export; do [ -d "../../crates/$crate/src" ] && WATCH_PATHS+=("../../crates/$crate/src") @@ -27,22 +27,6 @@ fingerprint() { APP_PID="" -SHARED_DIR="$HOME/Library/Application Support/so.cap.desktop" - -# Launching unconditionally would put this app on screen next to the classic -# one every session: under the experimental switch the two share ownership of -# the session (`enableGpuiApp` in the shared store), so a build only launches -# when this app owns it or an instance is already running (a rebuild swap). -gpui_owns_session() { - grep -Eq '"enableGpuiApp":[[:space:]]*true' "$SHARED_DIR/store" 2>/dev/null -} - -instance_live() { - local pid - pid=$(cat "$SHARED_DIR/cap-gpui.pid" 2>/dev/null) || return 1 - [ -n "$pid" ] && ps -p "$pid" -o comm= 2>/dev/null | grep -q cap-gpui -} - stop_app() { [ -n "$APP_PID" ] || return 0 if ! kill -0 "$APP_PID" 2>/dev/null; then @@ -80,12 +64,8 @@ while true; do LAST="$CURRENT" echo "[dev] building..." if "$BUILD" build; then - if [ -n "$APP_PID" ] || gpui_owns_session || instance_live; then - stop_app - start_app - else - echo "[dev] built; not launching (the classic app owns the session)" - fi + stop_app + start_app else echo "[dev] build failed; keeping the previous app running" fi diff --git a/apps/desktop-gpui/locales/de.toml b/apps/desktop-gpui/locales/de.toml index 186c6c912e8..20701865b6e 100644 --- a/apps/desktop-gpui/locales/de.toml +++ b/apps/desktop-gpui/locales/de.toml @@ -544,27 +544,10 @@ terms_title = "Gemerkte Begriffe" title = "Transkription" [settings.experimental] -developer = "Entwickler" -developer_description = "Wird nur in Entwicklungs-Builds angezeigt." muxer = "Muxer in separatem Prozess" muxer_description = "Führt den Muxer für fragmentiertes MP4 in einem isolierten Unterprozess aus, damit Muxer-Abstürze Ihre Aufnahme nicht beenden. Erfordert das mitgelieferte Programm cap-muxer." -native_app = "Native App verwenden" -native_app_description = "Ausschalten, um diese App zu schließen und die klassische Tauri-App in der Entwicklungsumgebung wieder zu öffnen. Aufnahmen und Einstellungen werden geteilt." reliability = "Zuverlässigkeit" -[settings.experimental.switch] -busy = "Schließen Sie Ihre Aufnahme, den Export, Upload, Import oder die Transkription ab, bevor Sie zur klassischen App wechseln." -check_failed = "Es konnte nicht geprüft werden, ob die klassische Cap-App geöffnet wurde: {error}" -data_stays = "Ihre Aufnahmen und Einstellungen bleiben genau dort, wo sie sind." -failed = "Die klassische Cap-App konnte nicht geöffnet werden." -not_found = "Die klassische Cap-App, zu der gewechselt werden soll, wurde nicht gefunden." -open_failed = "Die klassische Cap-App konnte nicht geöffnet werden: {error}" -opening = "Die klassische Cap-App wird geöffnet" -opening_detail = "Dieses Fenster wird geschlossen, sobald die klassische Cap-App bereit ist." -save_failed = "Ihre App-Einstellung konnte nicht gespeichert werden. Cap bleibt geöffnet." -switching = "Es wird zur klassischen Cap-App zurückgewechselt." -timed_out = "Die klassische App wurde nicht geöffnet. Prüfen Sie das Entwicklungsterminal auf Build-Fehler und versuchen Sie es dann erneut." - [settings.feedback] app_version = "App-Version" capture_support = "Aufnahmeunterstützung" @@ -960,7 +943,6 @@ name = "Benachrichtigen, wenn ein Upload fertig ist" description = "Den Link zum Teilen an Slack, Discord oder Ihren eigenen Webhook senden." name = "Slack beim Teilen benachrichtigen" - [main] delete_recording_confirm = "Möchten Sie diese Aufnahme wirklich löschen?" delete_screenshot_confirm = "Möchten Sie dieses Bildschirmfoto wirklich löschen?" diff --git a/apps/desktop-gpui/locales/en.toml b/apps/desktop-gpui/locales/en.toml index adc646a6803..ff6f72e44b1 100644 --- a/apps/desktop-gpui/locales/en.toml +++ b/apps/desktop-gpui/locales/en.toml @@ -546,27 +546,10 @@ terms_title = "Remembered terms" title = "Transcription" [settings.experimental] -developer = "Developer" -developer_description = "Only shown in development builds." muxer = "Out-of-process muxer" muxer_description = "Run the fragmented-MP4 muxer in an isolated subprocess so muxer crashes can't take down your recording. Requires the bundled cap-muxer binary." -native_app = "Use native app" -native_app_description = "Turn off to close this app and reopen the classic Tauri app in the dev harness. Recordings and settings are shared." reliability = "Reliability" -[settings.experimental.switch] -busy = "Finish your recording, export, upload, import, or transcription task before switching to the classic app." -check_failed = { text = "Couldn't check whether the classic Cap app opened: {error}", context = "{error} is a technical error message, usually in English." } -data_stays = "Your recordings and settings stay exactly where they are." -failed = "Couldn't open the classic Cap app." -not_found = "Couldn't find the classic Cap app to switch back to." -open_failed = { text = "Couldn't open the classic Cap app: {error}", context = "{error} is a technical error message, usually in English." } -opening = "Opening the classic Cap app" -opening_detail = "This window will close when the classic Cap app is ready." -save_failed = "Couldn't save your app preference. Cap is still open." -switching = "Switching back to the classic Cap app." -timed_out = "The classic app hasn't opened. Check the dev terminal for build errors, then try again." - [settings.feedback] app_version = "App Version" capture_support = "Capture Support" @@ -962,7 +945,6 @@ name = "Ping me when an upload is ready" description = "Send the share link to Slack, Discord, or your own webhook." name = "Tell Slack when you share something" - [main] delete_recording_confirm = "Are you sure you want to delete this recording?" delete_screenshot_confirm = "Are you sure you want to delete this screenshot?" diff --git a/apps/desktop-gpui/locales/es.toml b/apps/desktop-gpui/locales/es.toml index c0b5f57b038..3156d6cb740 100644 --- a/apps/desktop-gpui/locales/es.toml +++ b/apps/desktop-gpui/locales/es.toml @@ -544,27 +544,10 @@ terms_title = "Términos guardados" title = "Transcripción" [settings.experimental] -developer = "Desarrollador" -developer_description = "Solo se muestra en las compilaciones de desarrollo." muxer = "Multiplexor fuera de proceso" muxer_description = "Ejecuta el multiplexor de MP4 fragmentado en un subproceso aislado para que sus fallos no arruinen tu grabación. Requiere el binario cap-muxer incluido." -native_app = "Usar la app nativa" -native_app_description = "Desactívalo para cerrar esta app y volver a abrir la app Tauri clásica en el entorno de desarrollo. Las grabaciones y los ajustes se comparten." reliability = "Fiabilidad" -[settings.experimental.switch] -busy = "Termina la grabación, exportación, subida, importación o transcripción antes de cambiar a la app clásica." -check_failed = "No se pudo comprobar si la app clásica de Cap se abrió: {error}" -data_stays = "Tus grabaciones y ajustes se quedan exactamente donde están." -failed = "No se pudo abrir la app clásica de Cap." -not_found = "No se encontró la app clásica de Cap a la que volver." -open_failed = "No se pudo abrir la app clásica de Cap: {error}" -opening = "Abriendo la app clásica de Cap" -opening_detail = "Esta ventana se cerrará cuando la app clásica de Cap esté lista." -save_failed = "No se pudo guardar tu preferencia de app. Cap sigue abierto." -switching = "Volviendo a la app clásica de Cap." -timed_out = "La app clásica no se ha abierto. Revisa los errores de compilación en la terminal de desarrollo y vuelve a intentarlo." - [settings.feedback] app_version = "Versión de la app" capture_support = "Compatibilidad de captura" @@ -960,7 +943,6 @@ name = "Avisarme cuando una subida esté lista" description = "Envía el enlace para compartir a Slack, Discord o tu propio webhook." name = "Avisar a Slack cuando compartas algo" - [main] delete_recording_confirm = "¿Seguro que quieres eliminar esta grabación?" delete_screenshot_confirm = "¿Seguro que quieres eliminar esta captura de pantalla?" diff --git a/apps/desktop-gpui/locales/fr.toml b/apps/desktop-gpui/locales/fr.toml index dee8942f499..16385c4743d 100644 --- a/apps/desktop-gpui/locales/fr.toml +++ b/apps/desktop-gpui/locales/fr.toml @@ -544,27 +544,10 @@ terms_title = "Termes mémorisés" title = "Transcription" [settings.experimental] -developer = "Développeur" -developer_description = "Affiché uniquement dans les versions de développement." muxer = "Multiplexeur hors processus" muxer_description = "Exécute le multiplexeur MP4 fragmenté dans un sous-processus isolé pour que ses plantages ne fassent pas échouer votre enregistrement. Nécessite le binaire cap-muxer fourni." -native_app = "Utiliser l'app native" -native_app_description = "Désactivez pour fermer cette app et rouvrir l'app Tauri classique dans l'environnement de développement. Les enregistrements et les paramètres sont partagés." reliability = "Fiabilité" -[settings.experimental.switch] -busy = "Terminez votre enregistrement, exportation, mise en ligne, importation ou transcription avant de passer à l'app classique." -check_failed = "Impossible de vérifier si l'app Cap classique s'est ouverte : {error}" -data_stays = "Vos enregistrements et vos paramètres restent exactement où ils sont." -failed = "Impossible d'ouvrir l'app Cap classique." -not_found = "Impossible de trouver l'app Cap classique vers laquelle revenir." -open_failed = "Impossible d'ouvrir l'app Cap classique : {error}" -opening = "Ouverture de l'app Cap classique" -opening_detail = "Cette fenêtre se fermera quand l'app Cap classique sera prête." -save_failed = "Impossible d'enregistrer votre préférence d'app. Cap reste ouvert." -switching = "Retour à l'app Cap classique." -timed_out = "L'app classique ne s'est pas ouverte. Vérifiez les erreurs de compilation dans le terminal de développement, puis réessayez." - [settings.feedback] app_version = "Version de l'app" capture_support = "Prise en charge de la capture" @@ -960,7 +943,6 @@ name = "Me prévenir quand une mise en ligne est prête" description = "Envoyez le lien de partage à Slack, Discord ou votre propre webhook." name = "Prévenir Slack quand vous partagez" - [main] delete_recording_confirm = "Voulez-vous vraiment supprimer cet enregistrement ?" delete_screenshot_confirm = "Voulez-vous vraiment supprimer cette capture d'écran ?" diff --git a/apps/desktop-gpui/locales/hi.toml b/apps/desktop-gpui/locales/hi.toml index 7b7393439b8..b8d7d9f0015 100644 --- a/apps/desktop-gpui/locales/hi.toml +++ b/apps/desktop-gpui/locales/hi.toml @@ -544,27 +544,10 @@ terms_title = "याद रखे गए शब्द" title = "ट्रांसक्रिप्शन" [settings.experimental] -developer = "डेवलपर" -developer_description = "सिर्फ़ डेवलपमेंट बिल्ड में दिखता है।" muxer = "अलग प्रोसेस में मक्सर" muxer_description = "फ़्रैगमेंटेड-MP4 मक्सर को अलग सबप्रोसेस में चलाता है ताकि मक्सर क्रैश होने से आपकी रिकॉर्डिंग न रुके। इसके लिए बंडल की गई cap-muxer बाइनरी ज़रूरी है।" -native_app = "नेटिव ऐप इस्तेमाल करें" -native_app_description = "बंद करने पर यह ऐप बंद होगा और डेवलपमेंट हार्नेस में क्लासिक Tauri ऐप फिर से खुलेगा। रिकॉर्डिंग और सेटिंग्स साझा रहती हैं।" reliability = "विश्वसनीयता" -[settings.experimental.switch] -busy = "क्लासिक ऐप पर जाने से पहले अपनी रिकॉर्डिंग, एक्सपोर्ट, अपलोड, इम्पोर्ट या ट्रांसक्रिप्शन का काम पूरा करें।" -check_failed = "यह जाँच नहीं हो सकी कि क्लासिक Cap ऐप खुला या नहीं: {error}" -data_stays = "आपकी रिकॉर्डिंग और सेटिंग्स ठीक वहीं रहती हैं जहाँ वे हैं।" -failed = "क्लासिक Cap ऐप नहीं खुल सका।" -not_found = "वापस जाने के लिए क्लासिक Cap ऐप नहीं मिला।" -open_failed = "क्लासिक Cap ऐप नहीं खुल सका: {error}" -opening = "क्लासिक Cap ऐप खुल रहा है" -opening_detail = "क्लासिक Cap ऐप तैयार होने पर यह विंडो बंद हो जाएगी।" -save_failed = "आपकी ऐप पसंद सेव नहीं हो सकी। Cap अभी भी खुला है।" -switching = "क्लासिक Cap ऐप पर वापस जा रहे हैं।" -timed_out = "क्लासिक ऐप नहीं खुला। डेव टर्मिनल में बिल्ड की गड़बड़ियाँ देखें, फिर दोबारा कोशिश करें।" - [settings.feedback] app_version = "ऐप वर्ज़न" capture_support = "कैप्चर सपोर्ट" @@ -960,7 +943,6 @@ name = "अपलोड तैयार होने पर मुझे बत description = "शेयर लिंक Slack, Discord या अपने वेबहुक पर भेजें।" name = "कुछ शेयर करने पर Slack को बताएँ" - [main] delete_recording_confirm = "क्या आप वाकई यह रिकॉर्डिंग हटाना चाहते हैं?" delete_screenshot_confirm = "क्या आप वाकई यह स्क्रीनशॉट हटाना चाहते हैं?" diff --git a/apps/desktop-gpui/locales/ja.toml b/apps/desktop-gpui/locales/ja.toml index 60637b0c9c1..c04e300ea12 100644 --- a/apps/desktop-gpui/locales/ja.toml +++ b/apps/desktop-gpui/locales/ja.toml @@ -544,27 +544,10 @@ terms_title = "登録した用語" title = "文字起こし" [settings.experimental] -developer = "開発者" -developer_description = "開発ビルドでのみ表示されます。" muxer = "別プロセスのマルチプレクサ" muxer_description = "フラグメント化MP4のマルチプレクサを独立したサブプロセスで実行し、マルチプレクサがクラッシュしても録画が失われないようにします。同梱のcap-muxerバイナリが必要です。" -native_app = "ネイティブアプリを使う" -native_app_description = "オフにするとこのアプリを閉じ、開発用ハーネスで従来のTauriアプリを開き直します。録画と設定は共有されます。" reliability = "信頼性" -[settings.experimental.switch] -busy = "従来のアプリに切り替える前に、録画、書き出し、アップロード、読み込み、文字起こしの作業を完了してください。" -check_failed = "従来のCapアプリが開いたかどうか確認できませんでした:{error}" -data_stays = "録画と設定はそのまま残ります。" -failed = "従来のCapアプリを開けませんでした。" -not_found = "切り替え先の従来のCapアプリが見つかりませんでした。" -open_failed = "従来のCapアプリを開けませんでした:{error}" -opening = "従来のCapアプリを開いています" -opening_detail = "従来のCapアプリの準備ができると、このウィンドウは閉じます。" -save_failed = "アプリの設定を保存できませんでした。Capは開いたままです。" -switching = "従来のCapアプリに戻しています。" -timed_out = "従来のアプリが開きません。開発用ターミナルでビルドエラーを確認してから、もう一度お試しください。" - [settings.feedback] app_version = "アプリのバージョン" capture_support = "キャプチャ対応" @@ -960,7 +943,6 @@ name = "アップロードの準備ができたら通知" description = "共有リンクをSlack、Discord、または独自のWebhookに送ります。" name = "共有したらSlackに知らせる" - [main] delete_recording_confirm = "この録画を削除してもよろしいですか?" delete_screenshot_confirm = "このスクリーンショットを削除してもよろしいですか?" diff --git a/apps/desktop-gpui/locales/zh-CN.toml b/apps/desktop-gpui/locales/zh-CN.toml index 1c18cc7ab65..0d3ddef7591 100644 --- a/apps/desktop-gpui/locales/zh-CN.toml +++ b/apps/desktop-gpui/locales/zh-CN.toml @@ -544,27 +544,10 @@ terms_title = "记住的词语" title = "转写" [settings.experimental] -developer = "开发者" -developer_description = "仅在开发版本中显示。" muxer = "进程外混流器" muxer_description = "在独立的子进程中运行分段 MP4 混流器,混流器崩溃时不会影响你的录制。需要内置的 cap-muxer 程序。" -native_app = "使用原生应用" -native_app_description = "关闭后会退出此应用,并在开发环境中重新打开经典的 Tauri 应用。录制和设置是共享的。" reliability = "可靠性" -[settings.experimental.switch] -busy = "请先完成录制、导出、上传、导入或转写任务,再切换到经典版应用。" -check_failed = "无法确认经典版 Cap 应用是否已打开:{error}" -data_stays = "你的录制和设置会原样保留。" -failed = "无法打开经典版 Cap 应用。" -not_found = "找不到可以切换回去的经典版 Cap 应用。" -open_failed = "无法打开经典版 Cap 应用:{error}" -opening = "正在打开经典版 Cap 应用" -opening_detail = "经典版 Cap 应用准备就绪后,此窗口将关闭。" -save_failed = "无法保存你的应用偏好设置。Cap 仍保持打开。" -switching = "正在切换回经典版 Cap 应用。" -timed_out = "经典版应用尚未打开。请在开发终端中检查构建错误,然后重试。" - [settings.feedback] app_version = "应用版本" capture_support = "捕获支持" @@ -960,7 +943,6 @@ name = "上传就绪时提醒我" description = "将分享链接发送到 Slack、Discord 或你自己的 Webhook。" name = "分享内容时通知 Slack" - [main] delete_recording_confirm = "确定要删除这段录制吗?" delete_screenshot_confirm = "确定要删除这张截图吗?" diff --git a/apps/desktop-gpui/src/i18n/settings_surface_tests.rs b/apps/desktop-gpui/src/i18n/settings_surface_tests.rs index 4cae02da1d6..b03e99928aa 100644 --- a/apps/desktop-gpui/src/i18n/settings_surface_tests.rs +++ b/apps/desktop-gpui/src/i18n/settings_surface_tests.rs @@ -512,52 +512,6 @@ fn settings_surfaces_match_the_literals_they_replaced() { tr::settings::experimental::muxer_description(), "Run the fragmented-MP4 muxer in an isolated subprocess so muxer crashes can't take down your recording. Requires the bundled cap-muxer binary.", ), - (tr::settings::experimental::developer(), "Developer"), - ( - tr::settings::experimental::developer_description(), - "Only shown in development builds.", - ), - (tr::settings::experimental::native_app(), "Use native app"), - ( - tr::settings::experimental::native_app_description(), - "Turn off to close this app and reopen the classic Tauri app in the dev harness. Recordings and settings are shared.", - ), - ( - tr::settings::experimental::switch::switching(), - "Switching back to the classic Cap app.", - ), - ( - tr::settings::experimental::switch::data_stays(), - "Your recordings and settings stay exactly where they are.", - ), - ( - tr::settings::experimental::switch::opening(), - "Opening the classic Cap app", - ), - ( - tr::settings::experimental::switch::opening_detail(), - "This window will close when the classic Cap app is ready.", - ), - ( - tr::settings::experimental::switch::busy(), - "Finish your recording, export, upload, import, or transcription task before switching to the classic app.", - ), - ( - tr::settings::experimental::switch::not_found(), - "Couldn't find the classic Cap app to switch back to.", - ), - ( - tr::settings::experimental::switch::save_failed(), - "Couldn't save your app preference. Cap is still open.", - ), - ( - tr::settings::experimental::switch::timed_out(), - "The classic app hasn't opened. Check the dev terminal for build errors, then try again.", - ), - ( - tr::settings::experimental::switch::failed(), - "Couldn't open the classic Cap app.", - ), (tr::settings::feedback::title(), "Feedback"), ( tr::settings::feedback::description(), @@ -1496,14 +1450,6 @@ fn settings_surface_messages_format_like_the_code_they_replaced() { tr::settings::cli::load_failed(&error), format!("Couldn't load CLI status: {error}") ); - assert_eq!( - tr::settings::experimental::switch::open_failed(&error), - format!("Couldn't open the classic Cap app: {error}") - ); - assert_eq!( - tr::settings::experimental::switch::check_failed(&error), - format!("Couldn't check whether the classic Cap app opened: {error}") - ); let path_entry = "~/.local/bin".to_string(); assert_eq!( tr::settings::cli::path_missing(&path_entry), diff --git a/apps/desktop-gpui/src/main.rs b/apps/desktop-gpui/src/main.rs index b0a3ef392f3..0819040393f 100644 --- a/apps/desktop-gpui/src/main.rs +++ b/apps/desktop-gpui/src/main.rs @@ -165,10 +165,9 @@ fn resolve_auto_screenshot_editor(target: &str) -> Option { } /// Windows ends the process at logoff or shutdown without running -/// `on_app_quit`, so both markers that mean "this session crashed" clear here. +/// `on_app_quit`, so the marker that means "this session crashed" clears here. #[cfg(target_os = "windows")] fn end_windows_session() { - crate::store::clear_handoff_marker(); crash_capture::mark_clean_exit(); } @@ -343,9 +342,6 @@ fn main() { single_instance::acquire(); let previous_session = crash_capture::begin_session(&diagnostics::logs_dir()); blur_recovery::configure(previous_session); - if cfg!(debug_assertions) { - store::mark_handoff_session(); - } #[cfg(target_os = "macos")] platform::install_appkit_exception_guards(); @@ -479,12 +475,6 @@ fn main() { return; }; - if cfg!(debug_assertions) { - cx.on_app_quit(|_| async { - crate::store::clear_handoff_marker(); - }) - .detach(); - } cx.on_app_quit(|_| async { crash_capture::mark_clean_exit(); }) diff --git a/apps/desktop-gpui/src/settings_pages.rs b/apps/desktop-gpui/src/settings_pages.rs index 3ab0cd94daf..1042fa4a81d 100644 --- a/apps/desktop-gpui/src/settings_pages.rs +++ b/apps/desktop-gpui/src/settings_pages.rs @@ -360,12 +360,6 @@ pub(crate) struct PagesState { s3: S3Page, gdrive: GDrivePage, - // Experimental (experimental.tsx). The Developer section's hand-back takes the - // whole settings window over, so its clock and the ticker that repaints the - // sequence live here and the overlay renders from the window root. - switch_back: Option, - switch_back_ticker: Option>, - // Automations (automations.tsx) automations: AutomationsStore, automations_loaded: bool, @@ -475,8 +469,6 @@ impl PagesState { error: None, poll: None, }, - switch_back: None, - switch_back_ticker: None, automations: AutomationsStore::default(), automations_loaded: false, automations_save_failed: false, @@ -1441,10 +1433,6 @@ impl SettingsWindow { self.pages.listening.is_some() } - pub(crate) fn switch_back_running(&self) -> bool { - self.pages.switch_back.is_some() - } - /// Leave recording mode without touching the binding. Safe to call when /// nothing is recording; every exit path (Escape, a click elsewhere, /// another page, the window losing focus or closing) lands here. @@ -2666,75 +2654,13 @@ impl SettingsWindow { // Experimental (experimental.tsx) // --------------------------------------------------------------------------- -pub(crate) enum SwitchBack { - Running(std::time::Instant), - WaitingForClassic, - Failed(String), -} - -/// A cold dev build can genuinely take this long; past it, assume the build -/// failed and hand the user back their app with a pointer at the terminal. -const CLASSIC_WAIT_TIMEOUT: Duration = Duration::from_secs(10 * 60); - -const SWITCH_SENTENCES: &[fn() -> &'static str] = &[ - tr::settings::experimental::switch::switching, - tr::settings::experimental::switch::data_stays, -]; -const SWITCH_SENTENCE_MS: u64 = 2000; -const SWITCH_FADE_MS: f32 = 400.; -const SWITCH_COUNTDOWN_FROM: u32 = 5; -const SWITCH_TAKEOVER_MS: u64 = SWITCH_COUNTDOWN_FROM as u64 * 1000; - -/// The overlay's whole appearance at one instant: which sentence, how opaque, -/// and the number. -fn takeover_frame(elapsed_ms: f32) -> (usize, f32, u32) { - let last = SWITCH_SENTENCES.len() - 1; - let index = ((elapsed_ms / SWITCH_SENTENCE_MS as f32) as usize).min(last); - let within = elapsed_ms - index as f32 * SWITCH_SENTENCE_MS as f32; - let fade_in = (within / SWITCH_FADE_MS).clamp(0., 1.); - // The last line holds rather than fading out; blanking the column while the - // countdown finishes would read as a stall. - let alpha = if index == last { - fade_in - } else { - fade_in.min(((SWITCH_SENTENCE_MS as f32 - within) / SWITCH_FADE_MS).clamp(0., 1.)) - }; - // Five on the first frame down to one in the last second; the switch fires - // rather than ever showing zero. - let left = SWITCH_TAKEOVER_MS as f32 - elapsed_ms; - let remaining = (left / 1000.) - .ceil() - .clamp(1., SWITCH_COUNTDOWN_FROM as f32) as u32; - (index, alpha, remaining) -} - -/// Cap ships as this app alone, so the classic Tauri app only exists inside -/// the `tauri dev` harness: a debug cargo build can hand the session back to -/// it through the dev-session supervisor (`store::request_classic_reopen`), -/// and nothing else has a classic app to switch to. -fn dev_supervisor_available_for(exe: Option<&std::path::Path>, debug_build: bool) -> bool { - debug_build - && exe.is_some_and(|exe| { - exe.components() - .any(|component| component.as_os_str() == "target") - }) -} - -fn dev_supervisor_available() -> bool { - dev_supervisor_available_for( - std::env::current_exe().ok().as_deref(), - cfg!(debug_assertions), - ) -} - impl SettingsWindow { pub(crate) fn render_experimental(&self, cx: &mut Context) -> Vec { - // No "Native camera preview" toggle here: in this app the native - // camera path is the only implementation, so the Tauri page's - // experimental switch has nothing to switch. The store key - // (`enableNativeCameraPreview`) stays readable in `store.rs` and is - // left untouched in the shared store -- the Tauri app still uses it. - let mut sections = vec![ + // No "Native camera preview" toggle here: the native camera path is + // the only implementation, so the 0.6.0 page's experimental switch has + // nothing to switch. The store key (`enableNativeCameraPreview`) stays + // readable in `store.rs` and is left untouched in the store. + vec![ self.section( tr::settings::experimental::reliability(), None, @@ -2763,283 +2689,7 @@ impl SettingsWindow { ], ) .into_any_element(), - ]; - if dev_supervisor_available() { - sections.push( - self.section( - tr::settings::experimental::developer(), - Some(tr::settings::experimental::developer_description()), - None, - vec![self.rows(vec![self.classic_app_row(cx)]).into_any_element()], - ) - .into_any_element(), - ); - } - sections - } - - /// The dev harness's way back to the classic app: being in this app is what - /// the toggle means, and turning it off reopens the classic app under - /// `tauri dev`. - fn classic_app_row(&self, cx: &mut Context) -> gpui::AnyElement { - let checked = !matches!(self.pages.switch_back, Some(SwitchBack::Running(_))); - self.setting_row( - tr::settings::experimental::native_app(), - Some(tr::settings::experimental::native_app_description()), - self.toggle( - "enable-gpui-app", - tr::settings::experimental::native_app(), - checked, - cx, - |this, cx| { - match this.pages.switch_back { - Some(SwitchBack::Running(_)) => this.cancel_switch_back(cx), - // Committed: the classic app is already being brought up. - Some(SwitchBack::WaitingForClassic) => {} - _ => this.start_switch_back(cx), - } - }, - ) - .into_any_element(), - ) - } - - /// The full-window takeover, painted from `SettingsWindow::render` so it - /// covers the sidebar and the menus too. - pub(crate) fn render_switch_overlay(&self, cx: &mut Context) -> Option { - let switch = self.pages.switch_back.as_ref()?; - let white = gpui::white(); - let mut column = div() - .absolute() - .inset_0() - .when(cfg!(target_os = "windows"), |overlay| overlay.top(px(36.))) - // Nothing behind the takeover is clickable while it runs. - .occlude() - .flex() - .flex_col() - .items_center() - .justify_center() - .gap(px(28.)) - .bg(gpui::hsla(0., 0., 0., 0.92)); - - match switch { - SwitchBack::Running(started) => { - let (index, alpha, remaining) = - takeover_frame(started.elapsed().as_secs_f32() * 1000.); - column = column - .child( - div() - .text_size(px(72.)) - .line_height(px(76.)) - .font_weight(FontWeight::SEMIBOLD) - .text_color(white) - .child(SharedString::from(remaining.to_string())), - ) - .child( - div() - .h(px(40.)) - .max_w(px(380.)) - .flex() - .items_center() - .text_size(px(15.)) - .text_center() - .text_color(gpui::hsla(0., 0., 1., alpha)) - .child(SWITCH_SENTENCES[index]()), - ); - } - SwitchBack::WaitingForClassic => { - column = column - .child( - div() - .max_w(px(380.)) - .text_size(px(17.)) - .font_weight(FontWeight::SEMIBOLD) - .text_center() - .text_color(white) - .child(tr::settings::experimental::switch::opening()), - ) - .child( - div() - .max_w(px(380.)) - .text_size(px(14.)) - .text_center() - .text_color(gpui::hsla(0., 0., 1., 0.7)) - .child(tr::settings::experimental::switch::opening_detail()), - ); - } - SwitchBack::Failed(error) => { - column = column.child( - div() - .max_w(px(380.)) - .text_size(px(14.)) - .text_center() - .text_color(rgb(0xf87171)) - .child(SharedString::from(error.clone())), - ); - } - } - - // Committed states have nothing to cancel. - if matches!(switch, SwitchBack::WaitingForClassic) { - return Some(column.into_any_element()); - } - - Some( - column - .child( - div() - .id("switch-back-cancel") - .flex() - .items_center() - .px(px(16.)) - .h(px(32.)) - .rounded(px(8.)) - .border_1() - .border_color(gpui::hsla(0., 0., 1., 0.25)) - .text_size(px(13.)) - .text_color(white) - .cursor_pointer() - .hover(|style| style.bg(gpui::hsla(0., 0., 1., 0.12))) - .on_click(cx.listener(|this, _, _window, cx| this.cancel_switch_back(cx))) - .child(if matches!(switch, SwitchBack::Failed(_)) { - tr::common::close() - } else { - tr::common::cancel() - }), - ) - .into_any_element(), - ) - } - - fn start_switch_back(&mut self, cx: &mut Context) { - if self.switch_back_blocked(cx) { - return; - } - - self.pages.switch_back = Some(SwitchBack::Running(std::time::Instant::now())); - // gpui only renders on invalidation, so the fades and the countdown - // need a pulse to run at all -- the `toggle_placeholders` shape. - // Assigning over the previous task drops it, which is how Cancel and a - // restart stop the one already in flight. - self.pages.switch_back_ticker = Some(cx.spawn(async move |this, cx| { - loop { - cx.background_executor() - .timer(Duration::from_millis(16)) - .await; - let running = this - .update(cx, |this, cx| { - let started = match &this.pages.switch_back { - Some(SwitchBack::Running(started)) => *started, - _ => return false, - }; - if started.elapsed() >= Duration::from_millis(SWITCH_TAKEOVER_MS) { - this.finish_switch_back(cx); - return false; - } - cx.notify(); - true - }) - .unwrap_or(false); - if !running { - return; - } - } - })); - cx.notify(); - } - - fn cancel_switch_back(&mut self, cx: &mut Context) { - self.pages.switch_back = None; - self.pages.switch_back_ticker = None; - cx.notify(); - } - - fn switch_back_blocked(&mut self, cx: &mut Context) -> bool { - if !crate::updates::work_in_flight(cx) { - return false; - } - - self.pages.switch_back = Some(SwitchBack::Failed( - tr::settings::experimental::switch::busy().to_string(), - )); - cx.notify(); - true - } - - /// Hand the session (`enableGpuiApp`) to the classic dev app, ask the dev - /// supervisor to start it, then quit once it is visible. - fn finish_switch_back(&mut self, cx: &mut Context) { - if self.switch_back_blocked(cx) { - return; - } - if !dev_supervisor_available() { - self.pages.switch_back = Some(SwitchBack::Failed( - tr::settings::experimental::switch::not_found().to_string(), - )); - cx.notify(); - return; - } - if let Err(error) = crate::app_windows::flush_pending_editor_saves(cx) { - self.pages.switch_back = Some(SwitchBack::Failed(error)); - cx.notify(); - return; - } - - if !store::set_store_setting(GENERAL_SETTINGS, "enableGpuiApp", Value::Bool(false)) { - self.pages.switch_back = Some(SwitchBack::Failed( - tr::settings::experimental::switch::save_failed().to_string(), - )); - cx.notify(); - return; - } - let pending = store::mark_classic_pending(); - self.pages.switch_back = Some(SwitchBack::WaitingForClassic); - self.pages.switch_back_ticker = None; - cx.notify(); - - // The handoff must finish even if its settings window closes. - cx.spawn(async move |this, cx| { - let result = cx - .background_executor() - .spawn(async move { pending.and_then(|()| store::request_classic_reopen()) }) - .await; - let mut failure = result - .err() - .map(tr::settings::experimental::switch::open_failed); - let started = std::time::Instant::now(); - while failure.is_none() { - match store::classic_pending_path().try_exists() { - Ok(false) => { - tracing::info!("classic app is visible; quitting GPUI"); - cx.update(crate::menus::quit); - return; - } - Ok(true) => {} - Err(error) => { - failure = Some(tr::settings::experimental::switch::check_failed(error)); - break; - } - } - if started.elapsed() >= CLASSIC_WAIT_TIMEOUT { - failure = Some(tr::settings::experimental::switch::timed_out().to_string()); - break; - } - cx.background_executor() - .timer(Duration::from_millis(250)) - .await; - } - let message = - failure.unwrap_or_else(|| tr::settings::experimental::switch::failed().to_string()); - tracing::error!("{message}"); - store::clear_classic_pending(); - store::set_store_setting(GENERAL_SETTINGS, "enableGpuiApp", Value::Bool(true)); - this.update(cx, |this, cx| { - this.pages.switch_back = Some(SwitchBack::Failed(message)); - cx.notify(); - }) - .ok(); - }) - .detach(); + ] } } @@ -8825,41 +8475,6 @@ mod tests { ); } - #[test] - fn only_debug_cargo_builds_can_switch_back_to_the_dev_harness() { - let cargo_build = - std::path::Path::new("/Users/x/Cap/apps/desktop-gpui/target/debug/cap-gpui"); - let installed_app = std::path::Path::new("/Applications/Cap.app/Contents/MacOS/Cap"); - - assert!(dev_supervisor_available_for(Some(cargo_build), true)); - assert!(!dev_supervisor_available_for(Some(cargo_build), false)); - assert!(!dev_supervisor_available_for(Some(installed_app), true)); - assert!(!dev_supervisor_available_for(Some(installed_app), false)); - assert!(!dev_supervisor_available_for(None, true)); - } - - /// The takeover's whole timeline, read off its one clock. - #[test] - fn the_takeover_counts_down_while_the_lines_fade() { - // The number is up from the first frame and never reaches zero. - assert_eq!(takeover_frame(0.).2, SWITCH_COUNTDOWN_FROM); - assert_eq!(takeover_frame(1000.).2, SWITCH_COUNTDOWN_FROM - 1); - assert_eq!(takeover_frame(SWITCH_TAKEOVER_MS as f32 - 1.).2, 1); - assert_eq!(takeover_frame(SWITCH_TAKEOVER_MS as f32).2, 1); - - // First line, faded in over its lead, and out again as its slot ends. - let (index, alpha, _) = takeover_frame(0.); - assert_eq!(index, 0); - assert_eq!(alpha, 0.); - assert_eq!(takeover_frame(SWITCH_FADE_MS).1, 1.); - assert_eq!(takeover_frame(SWITCH_SENTENCE_MS as f32 - 1.).1.round(), 0.); - - // The last line holds rather than blanking while the number finishes. - let (index, alpha, _) = takeover_frame(SWITCH_TAKEOVER_MS as f32 - 1.); - assert_eq!(index, SWITCH_SENTENCES.len() - 1); - assert_eq!(alpha, 1.); - } - /// `conditionAppliesToTrigger` / `actionAppliesToTrigger` against the /// tables in `utils/automations.ts`. #[test] diff --git a/apps/desktop-gpui/src/settings_window.rs b/apps/desktop-gpui/src/settings_window.rs index c3e649a37f6..cd7339ca053 100644 --- a/apps/desktop-gpui/src/settings_window.rs +++ b/apps/desktop-gpui/src/settings_window.rs @@ -854,7 +854,7 @@ impl SettingsWindow { let pages = crate::settings_pages::PagesState::new(window, cx); let page_scroll = ScrollHandle::new(); let keyboard_nav = ui::keyboard_nav::install(window, cx, |this: &Self, _| { - this.menu.is_some() || this.shortcut_listening() || this.switch_back_running() + this.menu.is_some() || this.shortcut_listening() }); Self { @@ -1736,9 +1736,6 @@ fn covers_default_exclusion(entry: &WindowExclusion, default: &WindowExclusion) impl Render for SettingsWindow { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.sync_appearance(window, cx); - if self.switch_back_running() { - ui::keyboard_nav::leave(window, cx); - } self.keyboard_nav = ui::keyboard_nav::active(window, cx); let theme = self.theme; if self.page == Page::Quality @@ -1852,9 +1849,6 @@ impl Render for SettingsWindow { // the drag layer the zoom slider needs while the button is held. .children(self.render_menu(cx)) .children(self.render_slider_drag_layer(cx)) - // Over everything, including the menus: the Experimental page's - // hand-back takes the whole window while it counts down. - .children(self.render_switch_overlay(cx)) } } diff --git a/apps/desktop-gpui/src/store.rs b/apps/desktop-gpui/src/store.rs index 543f3a6d246..97ce398ba88 100644 --- a/apps/desktop-gpui/src/store.rs +++ b/apps/desktop-gpui/src/store.rs @@ -1,16 +1,18 @@ -//! Persisted state: this app's own, and the Tauri app's settings store. +//! Persisted state: this app's own, and the settings store it shares with +//! 0.6.0 and earlier (the Tauri app, still downloadable and able to open the +//! same data after a downgrade). //! //! Two files, both inside `so.cap.desktop`'s app-data dir: //! -//! - `gpui-state.json` -- ours. The Tauri app keeps the camera window's chrome +//! - `gpui-state.json` -- ours. The Tauri app kept the camera window's chrome //! state in the webview's `localStorage` (`cameraWindowState`); there is no //! webview here, so the same shape lives in a file. Reads happen once at //! open; writes are whole-file rewrites on a background thread -- the state //! is a handful of scalars, atomicity beyond rename is not worth plumbing. -//! - `store` -- the tauri-plugin-store file the shipping app loads -//! (`Store.load("store")` in `apps/desktop/src/store.ts`), holding -//! `general_settings`, `recording_start_safety`, `auth`, `presets` and the -//! rest. Both apps read and write it, so every write here is a +//! - `store` -- the tauri-plugin-store file 0.6.0 loads +//! (`Store.load("store")` in `apps/desktop/src/store.ts` at `cap-v0.6.2`), +//! holding `general_settings`, `recording_start_safety`, `auth`, `presets` +//! and the rest. Both apps read and write it, so every write here is a //! read-modify-write on the raw JSON that touches exactly one key: see //! [`set_store_setting`]. @@ -154,9 +156,9 @@ pub struct ExportPrefs { /// `so.cap.desktop`'s app-data dir -- where both stores live. /// -/// Same identifier the Tauri app uses (`so.cap.desktop`), resolved the way -/// each OS's app-data convention spells it so a recording made on Windows or -/// Linux lands in the folder both apps already agree on. +/// The identifier every Cap release has used (`so.cap.desktop`), resolved +/// the way each OS's app-data convention spells it, so recordings and +/// settings from 0.6.0 and earlier are found where those builds left them. pub fn app_data_dir() -> PathBuf { if let Ok(dir) = std::env::var("CAP_GPUI_APP_DATA_DIR") && !dir.trim().is_empty() @@ -221,81 +223,6 @@ fn bundled_resource_dirs_for( directories } -/// The Tauri dev harness's hand-off marker (`gpui_app.rs` over there): it -/// survives the whole session and is removed only on a clean quit, so a marker -/// left behind reads as a crash and routes the next `bun run dev:desktop` back -/// to the classic app. -pub fn handoff_marker_path() -> PathBuf { - app_data_dir().join("cap-gpui.handoff") -} - -pub fn mark_handoff_session() { - if let Err(error) = mark_handoff_session_at(&handoff_marker_path()) { - tracing::warn!("writing the hand-off marker: {error}"); - } -} - -fn mark_handoff_session_at(path: &Path) -> std::io::Result<()> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - let staged = path.with_extension(format!("handoff-{}", std::process::id())); - std::fs::write(&staged, std::process::id().to_string())?; - std::fs::rename(&staged, path).inspect_err(|_| { - let _ = std::fs::remove_file(&staged); - }) -} - -pub fn clear_handoff_marker() { - clear_handoff_marker_at(&handoff_marker_path()); -} - -fn clear_handoff_marker_at(path: &Path) { - match std::fs::remove_file(path) { - Ok(()) => tracing::info!("cleared the hand-off marker"), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => tracing::warn!("clearing the hand-off marker: {error}"), - } -} - -/// A dev-checkout switch-back has no `Cap.app` to `open`: the classic app only -/// runs inside its `tauri dev` harness, which exited with the hand-off. This -/// sentinel asks whatever supervises the dev session (`scripts/dev-desktop.mjs` -/// watches for it) to start that harness again; with no supervisor listening it -/// is inert, and the flag written alongside it still routes the next -/// `bun run dev:desktop` to the classic app. -pub fn classic_reopen_path() -> PathBuf { - app_data_dir().join("cap-classic.reopen") -} - -pub fn request_classic_reopen() -> std::io::Result<()> { - let path = classic_reopen_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(&path, std::process::id().to_string()) -} - -pub fn classic_pending_path() -> PathBuf { - app_data_dir().join("cap-classic.pending") -} - -pub fn mark_classic_pending() -> std::io::Result<()> { - let path = classic_pending_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(&path, std::process::id().to_string()) -} - -pub fn clear_classic_pending() { - if let Err(error) = std::fs::remove_file(classic_pending_path()) - && error.kind() != std::io::ErrorKind::NotFound - { - tracing::warn!(%error, "couldn't clear classic handoff readiness"); - } -} - fn state_path() -> PathBuf { app_data_dir().join("gpui-state.json") } @@ -2106,23 +2033,6 @@ pub fn preset_names() -> Vec { #[cfg(test)] mod tests { - #[test] - fn the_handoff_marker_holds_this_pid_until_a_clean_quit_clears_it() { - let directory = - std::env::temp_dir().join(format!("cap-gpui-handoff-{}", super::new_uuid_v4())); - let marker = directory.join("cap-gpui.handoff"); - super::mark_handoff_session_at(&marker).unwrap(); - assert_eq!( - std::fs::read_to_string(&marker).unwrap(), - std::process::id().to_string() - ); - assert_eq!(std::fs::read_dir(&directory).unwrap().count(), 1); - super::clear_handoff_marker_at(&marker); - assert!(!marker.exists()); - super::clear_handoff_marker_at(&marker); - std::fs::remove_dir_all(directory).unwrap(); - } - use super::*; struct TempStore { diff --git a/scripts/dev-desktop.mjs b/scripts/dev-desktop.mjs index 9411c47e4ff..7c3cc303ded 100644 --- a/scripts/dev-desktop.mjs +++ b/scripts/dev-desktop.mjs @@ -1,21 +1,8 @@ -// The desktop dev entry: `bun run tauri dev` plus the gpui app's save-to-relaunch -// loop (`apps/desktop-gpui/dev.sh`), so working on either app under -// `bun run dev:desktop` gets hot reload without a second terminal. The gpui loop -// is macOS-only (dev.sh uses BSD stat/md5), skipped when the workspace is -// absent (main without the gpui branch), and opt-out via CAP_GPUI_DEV=0. Its -// output is prefixed [gpui] so the two cargo streams stay tellable apart. -// -// This script is also the dev half of the experimental app-switch: handing off -// to the gpui app exits the Tauri process by design, so the session stays up -// while the loop runs, and switching back inside the gpui app writes a reopen -// sentinel (`store::request_classic_reopen`) that this supervisor answers by -// starting `tauri dev` again -- the dev classic app only exists inside that -// harness, so nothing else could reopen it. -import { spawn } from "node:child_process"; -import { existsSync, rmSync } from "node:fs"; -import os from "node:os"; +// `bun run dev:desktop`: Cap's desktop app (apps/desktop-gpui) from source. +// macOS runs the save-to-relaunch loop in apps/desktop-gpui/dev.sh, which +// relies on BSD stat/md5; Windows and Linux build and run the app once. +import { spawn, spawnSync } from "node:child_process"; import path from "node:path"; -import readline from "node:readline"; import { fileURLToPath } from "node:url"; const repoRoot = path.resolve( @@ -23,111 +10,67 @@ const repoRoot = path.resolve( "..", ); const gpuiDir = path.join(repoRoot, "apps", "desktop-gpui"); -const gpuiDevScript = path.join(gpuiDir, "dev.sh"); -const reopenSentinel = path.join( - os.homedir(), - "Library/Application Support/so.cap.desktop/cap-classic.reopen", -); - -let tauri = null; -let gpui = null; -let lastTauriCode = 0; -let exiting = false; -function startTauri() { - tauri = spawn("bun", ["run", "tauri", "dev"], { stdio: "inherit" }); - tauri.on("exit", (code, signal) => { - tauri = null; - lastTauriCode = signal ? 1 : (code ?? 1); - if (exiting || !gpui) { - finish(signal); - return; - } - // The experimental toggle hands the session off to the gpui app: this - // exit is by design, and the loop keeps the app the user switched to - // alive. The sentinel watcher below brings `tauri dev` back when the - // gpui app asks to switch back again. - console.error( - "[gpui] Tauri app exited; the gpui dev loop is still running (Ctrl-C to stop)", - ); - }); +// setup.js copies the FFmpeg and ONNX Runtime libraries into the root +// workspace's target/, which is not where cargo puts this app's binary. +function runtimeEnvironment(env) { + const prepend = (name, directories) => { + // Windows spells it `Path`; a second `PATH` key would shadow it. + const key = + Object.keys(env).find((existing) => existing.toUpperCase() === name) ?? + name; + return { + ...env, + [key]: [...directories, env[key]].filter(Boolean).join(path.delimiter), + }; + }; + if (process.platform === "win32") { + return prepend("PATH", [path.join(repoRoot, "target", "debug")]); + } + if (process.platform === "linux") { + return prepend("LD_LIBRARY_PATH", [ + path.join(repoRoot, "target", "native-deps", "lib"), + path.join(repoRoot, "target", "debug"), + ]); + } + return env; } -function finish(signal) { - exiting = true; - stopGpui(); - if (signal) { - // Re-raise with the handler gone, or it would swallow its own signal. - process.removeAllListeners(signal); - process.kill(process.pid, signal); - } else process.exit(lastTauriCode); -} +function main() { + const prepared = spawnSync( + "bash", + [path.join(repoRoot, "scripts", "prepare-gpui-dependency.sh")], + { stdio: "inherit" }, + ); + if (prepared.error) throw prepared.error; + if (prepared.status !== 0) process.exit(prepared.status ?? 1); -// dev.sh traps TERM and shuts the running app down with it; give that a -// moment before giving up on a stuck loop. -function stopGpui() { - const child = gpui; - if (!child) return; - child.kill("SIGTERM"); - setTimeout(() => { - if (gpui === child) child.kill("SIGKILL"); - }, 5000).unref(); -} + const child = + process.platform === "darwin" + ? spawn("bash", [path.join(gpuiDir, "dev.sh")], { + cwd: gpuiDir, + stdio: "inherit", + }) + : spawn("cargo", ["run"], { + cwd: gpuiDir, + stdio: "inherit", + env: runtimeEnvironment(process.env), + }); -if ( - process.platform === "darwin" && - process.env.CAP_GPUI_DEV !== "0" && - existsSync(gpuiDevScript) -) { - gpui = spawn("bash", [gpuiDevScript], { - cwd: gpuiDir, - stdio: ["ignore", "pipe", "pipe"], - env: process.env, + for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, () => child.kill(signal)); + } + child.on("exit", (code, signal) => { + if (signal) { + process.removeAllListeners(signal); + process.kill(process.pid, signal); + } else process.exit(code ?? 1); }); - const prefix = (stream, out) => { - readline - .createInterface({ input: stream }) - .on("line", (line) => out.write(`[gpui] ${line}\n`)); - }; - prefix(gpui.stdout, process.stdout); - prefix(gpui.stderr, process.stderr); - gpui.on("exit", (code, signal) => { - gpui = null; - // With no Tauri child either, the loop was the session's last process: - // exit whether this is a shutdown or a crash in detached mode. - if (!tauri) { - finish(null); - return; - } - if (exiting) return; - console.error( - `[gpui] dev loop exited (${signal ?? code}); restart with: cd apps/desktop-gpui && ./dev.sh`, - ); - }); - - // A sentinel from before this session is stale, not a request. - rmSync(reopenSentinel, { force: true }); - setInterval(() => { - if (exiting || !existsSync(reopenSentinel)) return; - // Always consumed once seen: with the Tauri side already up (or still - // starting, e.g. mid-compile) the request is satisfied, and leaving - // the file would fire it spuriously after the next hand-off. - rmSync(reopenSentinel, { force: true }); - if (tauri) return; - console.error("[gpui] switch-back requested; starting the Tauri app again"); - startTauri(); - }, 500).unref(); } -for (const signal of ["SIGINT", "SIGTERM"]) { - process.on(signal, () => { - exiting = true; - stopGpui(); - // Ctrl-C already reaches the children through the process group; - // forward for the direct-signal case, then let the exit handlers run. - if (tauri) tauri.kill(signal); - else if (!gpui) process.exit(lastTauriCode); - }); +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + main(); } - -startTauri(); From e4f4ca9bc2cac5126a2640238522410f5834002e Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:51:28 +0000 Subject: [PATCH 3/6] chore: remove the Tauri app and its build tooling --- .config/hakari.toml | 16 +- .github/workflows/ci.yml | 50 +- .github/workflows/desktop-release.yml | 33 +- .github/workflows/promote-nightly.yml | 2 +- .github/workflows/publish-nightly.yml | 2 +- .github/workflows/publish.yml | 2 +- Cargo.lock | 3236 +---- Cargo.toml | 11 - apps/desktop-gpui/package.json | 12 + apps/desktop/.gitignore | 2 - apps/desktop/.vscode/extensions.json | 3 - apps/desktop/app.config.ts | 84 - apps/desktop/package.json | 101 - apps/desktop/postcss.config.cjs | 6 - .../desktop-display-transport-benchmark.js | 591 - .../desktop-display-transport-browser.js | 50 - .../desktop-display-transport-browser.test.js | 70 - .../scripts/desktop-memory-soak-lib.js | 299 - apps/desktop/scripts/desktop-memory-soak.js | 583 - .../scripts/desktop-memory-soak.test.js | 95 - .../scripts/desktop-picker-performance.md | 64 - apps/desktop/scripts/prepare.js | 194 - apps/desktop/scripts/prepare.test.js | 36 - apps/desktop/scripts/prodBeforeBundle.js | 74 - .../scripts/stop-editor-performance.md | 712 - apps/desktop/src-tauri/.gitignore | 7 - apps/desktop/src-tauri/Cargo.toml | 196 - apps/desktop/src-tauri/Info.plist | 33 - apps/desktop/src-tauri/assets/wix-banner.bmp | Bin 405270 -> 0 bytes apps/desktop/src-tauri/assets/wix-dialog.bmp | Bin 2180502 -> 0 bytes apps/desktop/src-tauri/build.rs | 13 - .../src-tauri/capabilities/default.json | 73 - .../desktop-display-transport-benchmark.rs | 421 - .../src-tauri/icons/Square107x107Logo.png | Bin 5945 -> 0 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 8353 -> 0 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 9048 -> 0 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 18233 -> 0 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 1073 -> 0 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 1788 -> 0 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 3690 -> 0 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 4719 -> 0 bytes apps/desktop/src-tauri/icons/StoreLogo.png | Bin 2344 -> 0 bytes .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 2093 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 10036 -> 0 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 2093 -> 0 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 2117 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 5945 -> 0 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 2117 -> 0 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 5292 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 13491 -> 0 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 5292 -> 0 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 8619 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 21074 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 8619 -> 0 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 11836 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 29013 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 11836 -> 0 bytes apps/desktop/src-tauri/icons/icon.icns | Bin 148492 -> 0 bytes .../src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 585 -> 0 bytes .../icons/ios/AppIcon-20x20@2x-1.png | Bin 1525 -> 0 bytes .../src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 1525 -> 0 bytes .../src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 2732 -> 0 bytes .../src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 987 -> 0 bytes .../icons/ios/AppIcon-29x29@2x-1.png | Bin 2422 -> 0 bytes .../src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 2422 -> 0 bytes .../src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 4292 -> 0 bytes .../src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 1525 -> 0 bytes .../icons/ios/AppIcon-40x40@2x-1.png | Bin 3822 -> 0 bytes .../src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 3822 -> 0 bytes .../src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 6558 -> 0 bytes .../src-tauri/icons/ios/AppIcon-512@2x.png | Bin 25261 -> 0 bytes .../src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 6558 -> 0 bytes .../src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 9947 -> 0 bytes .../src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 3610 -> 0 bytes .../src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 8462 -> 0 bytes .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin 9274 -> 0 bytes .../so.cap.desktop-tray-default-symbolic.svg | 3 - .../so.cap.desktop-tray-instant-symbolic.svg | 3 - ...o.cap.desktop-tray-screenshot-symbolic.svg | 3 - .../so.cap.desktop-tray-stop-symbolic.svg | 4 - .../so.cap.desktop-tray-studio-symbolic.svg | 3 - apps/desktop/src-tauri/icons/macos/icon.png | Bin 54427 -> 0 bytes .../icons/tray-default-icon-instant.png | Bin 4316 -> 0 bytes .../icons/tray-default-icon-screenshot.png | Bin 4597 -> 0 bytes .../icons/tray-default-icon-studio.png | Bin 4550 -> 0 bytes .../src-tauri/icons/tray-stop-icon.png | Bin 6274 -> 0 bytes apps/desktop/src-tauri/sounds/screenshot.ogg | Bin 16122 -> 0 bytes apps/desktop/src-tauri/src/api.rs | 492 - apps/desktop/src-tauri/src/audio.rs | 95 - apps/desktop/src-tauri/src/audio_library.rs | 165 - apps/desktop/src-tauri/src/audio_meter.rs | 242 - apps/desktop/src-tauri/src/auth.rs | 131 - apps/desktop/src-tauri/src/automation.rs | 1022 -- apps/desktop/src-tauri/src/camera.rs | 2126 --- apps/desktop/src-tauri/src/camera.wgsl | 179 - apps/desktop/src-tauri/src/camera_legacy.rs | 1084 -- apps/desktop/src-tauri/src/camera_native.rs | 356 - apps/desktop/src-tauri/src/captions.rs | 2910 ---- apps/desktop/src-tauri/src/clean_capture.rs | 4536 ------- apps/desktop/src-tauri/src/cli.rs | 19 - apps/desktop/src-tauri/src/clip_thumbnails.rs | 455 - apps/desktop/src-tauri/src/crash_sentinel.rs | 377 - .../desktop/src-tauri/src/deeplink_actions.rs | 558 - apps/desktop/src-tauri/src/diagnostics.rs | 787 -- .../desktop/src-tauri/src/editor_preparing.rs | 1398 -- .../src-tauri/src/editor_preparing/audio.rs | 227 - .../desktop/src-tauri/src/editor_recording.rs | 206 - apps/desktop/src-tauri/src/editor_window.rs | 776 -- apps/desktop/src-tauri/src/exit_shutdown.rs | 278 - apps/desktop/src-tauri/src/export.rs | 2747 ---- .../src-tauri/src/export/estimate_request.rs | 290 - apps/desktop/src-tauri/src/fake_window.rs | 962 -- apps/desktop/src-tauri/src/flags.rs | 16 - apps/desktop/src-tauri/src/frame_ws.rs | 1260 -- .../desktop/src-tauri/src/general_settings.rs | 999 -- apps/desktop/src-tauri/src/gpu_context.rs | 206 - .../src-tauri/src/gpu_device_health.rs | 119 - apps/desktop/src-tauri/src/gpui_app.rs | 1545 --- apps/desktop/src-tauri/src/hotkeys.rs | 854 -- apps/desktop/src-tauri/src/http_client.rs | 61 - apps/desktop/src-tauri/src/import.rs | 2268 ---- apps/desktop/src-tauri/src/lib.rs | 11117 ---------------- .../src-tauri/src/linux_instant_camera.rs | 1144 -- apps/desktop/src-tauri/src/logging.rs | 267 - .../desktop/src-tauri/src/macos_save_panel.rs | 108 - apps/desktop/src-tauri/src/main.rs | 597 - .../src-tauri/src/main_window_geometry.rs | 54 - apps/desktop/src-tauri/src/notifications.rs | 160 - apps/desktop/src-tauri/src/panel_manager.rs | 243 - apps/desktop/src-tauri/src/permissions.rs | 979 -- .../desktop/src-tauri/src/picker-benchmark.js | 28 - .../desktop/src-tauri/src/picker_benchmark.rs | 189 - .../src-tauri/src/platform/macos/delegates.rs | 509 - .../src-tauri/src/platform/macos/mod.rs | 788 -- .../platform/macos/sc_shareable_content.rs | 198 - apps/desktop/src-tauri/src/platform/mod.rs | 76 - apps/desktop/src-tauri/src/platform/win.rs | 258 - apps/desktop/src-tauri/src/power_observer.rs | 278 - .../src-tauri/src/preparing_finalization.rs | 588 - apps/desktop/src-tauri/src/presets.rs | 88 - apps/desktop/src-tauri/src/recording.rs | 10476 --------------- .../src-tauri/src/recording_settings.rs | 168 - .../src-tauri/src/recording_telemetry.rs | 271 - .../src-tauri/src/recordings_locations.rs | 655 - apps/desktop/src-tauri/src/recovery.rs | 242 - .../src-tauri/src/render_frame_event.rs | 140 - apps/desktop/src-tauri/src/resource.rs | 52 - .../src-tauri/src/screenshot_editor.rs | 1897 --- apps/desktop/src-tauri/src/startup.rs | 104 - .../src-tauri/src/stop-editor-benchmark.js | 30 - .../src-tauri/src/stop-editor-parity-dom.js | 214 - .../src-tauri/src/stop_editor_benchmark.rs | 822 -- .../src-tauri/src/target_select_overlay.rs | 1054 -- .../src/target_select_overlay/lifecycle.rs | 262 - apps/desktop/src-tauri/src/telemetry.rs | 468 - .../desktop/src-tauri/src/thumbnails/linux.rs | 51 - apps/desktop/src-tauri/src/thumbnails/mac.rs | 377 - apps/desktop/src-tauri/src/thumbnails/mod.rs | 137 - .../src-tauri/src/thumbnails/windows.rs | 287 - apps/desktop/src-tauri/src/tray.rs | 1236 -- .../src-tauri/src/update_project_names.rs | 325 - apps/desktop/src-tauri/src/updates.rs | 423 - apps/desktop/src-tauri/src/upload.rs | 5511 -------- .../desktop/src-tauri/src/upload/lifecycle.rs | 1725 --- .../src-tauri/src/upload/preparation.rs | 96 - .../src-tauri/src/upload/recovery_age.rs | 36 - apps/desktop/src-tauri/src/upload/resume.rs | 1 - apps/desktop/src-tauri/src/web_api.rs | 325 - .../desktop/src-tauri/src/window_exclusion.rs | 407 - .../src/window_position_persistence.rs | 126 - apps/desktop/src-tauri/src/windows.rs | 4483 ------- apps/desktop/src-tauri/tauri.conf.json | 129 - apps/desktop/src-tauri/tauri.prod.conf.json | 29 - .../desktop/src-tauri/tests/crash_sentinel.rs | 2 - apps/desktop/src-tauri/tests/exit_shutdown.rs | 527 - .../tests/export_estimate_request.rs | 2 - .../src-tauri/tests/web_api_startup.rs | 43 - apps/desktop/src/app.tsx | 408 - .../src/assets/illustrations/cloud-1.png | Bin 467102 -> 0 bytes .../src/assets/illustrations/cloud-2.png | Bin 362822 -> 0 bytes .../src/assets/illustrations/cloud-3.png | Bin 612983 -> 0 bytes .../src/assets/illustrations/color.webp | Bin 758 -> 0 bytes .../src/assets/illustrations/display.png | Bin 11280 -> 0 bytes .../src/assets/illustrations/gradient.webp | Bin 1980 -> 0 bytes .../src/assets/illustrations/image.webp | Bin 3356 -> 0 bytes .../illustrations/instant-mode-dark.png | Bin 46960 -> 0 bytes .../illustrations/instant-mode-light.png | Bin 42379 -> 0 bytes .../assets/illustrations/studio-mode-dark.png | Bin 46117 -> 0 bytes .../illustrations/studio-mode-light.png | Bin 41868 -> 0 bytes .../src/assets/illustrations/transparent.webp | Bin 840 -> 0 bytes apps/desktop/src/assets/instant-toggle.jpg | Bin 821333 -> 0 bytes apps/desktop/src/assets/rive/pricing.riv | Bin 61357 -> 0 bytes .../src/assets/theme-previews/auto.jpg | Bin 44560 -> 0 bytes .../src/assets/theme-previews/dark.jpg | Bin 35259 -> 0 bytes .../src/assets/theme-previews/legacy-auto.jpg | Bin 49913 -> 0 bytes .../src/assets/theme-previews/legacy-dark.jpg | Bin 34580 -> 0 bytes .../assets/theme-previews/legacy-light.jpg | Bin 39852 -> 0 bytes .../src/assets/theme-previews/light.jpg | Bin 35868 -> 0 bytes .../src/components/CameraPreviewChrome.tsx | 398 - .../src/components/CapErrorBoundary.tsx | 77 - apps/desktop/src/components/Cropper.tsx | 1671 --- apps/desktop/src/components/Loader.tsx | 9 - apps/desktop/src/components/Mode.tsx | 152 - apps/desktop/src/components/ModeSelect.tsx | 121 - apps/desktop/src/components/RecoveryToast.tsx | 138 - apps/desktop/src/components/SignInButton.tsx | 29 - apps/desktop/src/components/SwitchTab.tsx | 0 apps/desktop/src/components/Toggle.tsx | 50 - apps/desktop/src/components/Tooltip.tsx | 57 - .../src/components/TooltipIconButton.tsx | 0 .../src/components/callback.template.ts | 59 - .../src/components/crop-alignment.test.ts | 74 - apps/desktop/src/components/crop-alignment.ts | 76 - apps/desktop/src/components/cropper.css | 62 - .../desktop/src/components/selection-hint.tsx | 47 - .../src/components/titlebar/Titlebar.tsx | 62 - .../controls/CaptionControlsMacOS.tsx | 174 - .../controls/CaptionControlsWindows11.tsx | 195 - .../titlebar/controls/WindowControlButton.tsx | 14 - apps/desktop/src/declaration.d.ts | 4 - apps/desktop/src/entry-client.tsx | 32 - apps/desktop/src/entry-server.tsx | 34 - apps/desktop/src/global.d.ts | 1895 --- apps/desktop/src/icons.tsx | 90 - apps/desktop/src/routes/(window-chrome).tsx | 143 - .../src/routes/(window-chrome)/Context.tsx | 53 - .../routes/(window-chrome)/OptionsContext.tsx | 30 - .../src/routes/(window-chrome)/icons.tsx | 22 - .../(window-chrome)/new-main/CameraSelect.tsx | 345 - .../new-main/ChangeLogButton.tsx | 85 - .../new-main/DeviceSelectOverlay.tsx | 280 - .../(window-chrome)/new-main/InfoPill.tsx | 22 - .../new-main/MicrophoneSelect.tsx | 261 - .../new-main/ModeInfoPanel.tsx | 127 - .../(window-chrome)/new-main/SystemAudio.tsx | 82 - .../(window-chrome)/new-main/TargetCard.tsx | 619 - .../new-main/TargetDropdownButton.tsx | 44 - .../new-main/TargetMenuGrid.tsx | 486 - .../new-main/TargetSelectInfoPill.tsx | 49 - .../new-main/TargetTypeButton.tsx | 65 - .../new-main/deviceRowStyles.ts | 12 - .../routes/(window-chrome)/new-main/index.tsx | 3438 ----- .../new-main/use-request-permission.test.ts | 543 - .../new-main/useRequestPermission.ts | 185 - .../new-main/window-geometry.test.ts | 48 - .../new-main/window-geometry.ts | 49 - .../src/routes/(window-chrome)/onboarding.tsx | 2391 ---- .../src/routes/(window-chrome)/settings.tsx | 586 - .../(window-chrome)/settings/Setting.tsx | 112 - .../(window-chrome)/settings/automations.tsx | 1476 -- .../(window-chrome)/settings/changelog.tsx | 87 - .../routes/(window-chrome)/settings/cli.tsx | 223 - .../(window-chrome)/settings/experimental.tsx | 254 - .../(window-chrome)/settings/feedback.tsx | 476 - .../(window-chrome)/settings/general.tsx | 1390 -- .../(window-chrome)/settings/hotkeys.tsx | 251 - .../routes/(window-chrome)/settings/index.tsx | 8 - .../settings/integrations/config-header.tsx | 24 - .../integrations/google-drive-config.tsx | 454 - .../settings/integrations/index.tsx | 155 - .../settings/integrations/s3-config.tsx | 297 - .../(window-chrome)/settings/license.tsx | 271 - .../(window-chrome)/settings/quality.tsx | 325 - .../(window-chrome)/settings/recordings.tsx | 563 - .../(window-chrome)/settings/screenshots.tsx | 381 - .../settings/transcription.tsx | 199 - .../src/routes/(window-chrome)/update.tsx | 217 - .../src/routes/(window-chrome)/upgrade.tsx | 30 - apps/desktop/src/routes/camera.tsx | 1183 -- apps/desktop/src/routes/capture-area.tsx | 305 - apps/desktop/src/routes/debug.tsx | 138 - .../routes/editor/AnimatedGradientEditor.tsx | 759 -- .../src/routes/editor/AspectRatioSelect.tsx | 101 - .../src/routes/editor/AudioLibrary.tsx | 414 - .../src/routes/editor/BrandColorsDropdown.tsx | 77 - .../routes/editor/CanvasElementsOverlay.tsx | 950 -- .../src/routes/editor/CaptionOverlay.tsx | 352 - .../routes/editor/CaptionsRegenerateBadge.tsx | 129 - .../desktop/src/routes/editor/CaptionsTab.tsx | 1531 --- .../src/routes/editor/ClipsSidebar.tsx | 741 - .../routes/editor/ColorCorrectionSection.tsx | 205 - .../src/routes/editor/ConfigSidebar.tsx | 5203 -------- .../src/routes/editor/CursorStylePicker.tsx | 235 - apps/desktop/src/routes/editor/Editor.tsx | 1809 --- .../src/routes/editor/EditorErrorScreen.tsx | 311 - apps/desktop/src/routes/editor/ExportPage.tsx | 2055 --- apps/desktop/src/routes/editor/FontPicker.tsx | 96 - .../desktop/src/routes/editor/FrameButton.tsx | 255 - .../src/routes/editor/GradientEditor.tsx | 294 - apps/desktop/src/routes/editor/Header.tsx | 372 - .../src/routes/editor/ImportProgress.tsx | 144 - .../desktop/src/routes/editor/KeyboardTab.tsx | 592 - .../desktop/src/routes/editor/MaskOverlay.tsx | 390 - .../routes/editor/OrganizationDropdown.tsx | 523 - .../src/routes/editor/PerformanceOverlay.tsx | 271 - apps/desktop/src/routes/editor/Player.tsx | 804 -- .../src/routes/editor/PresetsDropdown.tsx | 206 - .../src/routes/editor/ShadowSettings.tsx | 93 - .../desktop/src/routes/editor/ShareButton.tsx | 169 - .../src/routes/editor/SplitScreenOverlay.tsx | 318 - apps/desktop/src/routes/editor/TextInput.tsx | 21 - .../desktop/src/routes/editor/TextOverlay.tsx | 898 -- .../src/routes/editor/Timeline/AudioTrack.tsx | 604 - .../routes/editor/Timeline/CaptionsTrack.tsx | 298 - .../src/routes/editor/Timeline/ClipTrack.tsx | 1899 --- .../routes/editor/Timeline/KeyboardTrack.tsx | 296 - .../src/routes/editor/Timeline/MaskTrack.tsx | 538 - .../src/routes/editor/Timeline/Minimap.tsx | 138 - .../src/routes/editor/Timeline/SceneTrack.tsx | 643 - .../src/routes/editor/Timeline/TextTrack.tsx | 522 - .../routes/editor/Timeline/ThreeDTrack.tsx | 736 - .../src/routes/editor/Timeline/Track.tsx | 302 - .../routes/editor/Timeline/TrackManager.tsx | 256 - .../src/routes/editor/Timeline/ZoomTrack.tsx | 831 -- .../src/routes/editor/Timeline/context.ts | 113 - .../routes/editor/Timeline/image-track.tsx | 5 - .../src/routes/editor/Timeline/index.tsx | 2147 --- .../editor/Timeline/playback-follow.test.ts | 148 - .../routes/editor/Timeline/playback-follow.ts | 60 - .../routes/editor/Timeline/sectionMarker.ts | 167 - .../routes/editor/Timeline/split-snapping.ts | 68 - .../routes/editor/Timeline/style-track.tsx | 262 - .../src/routes/editor/Timeline/styles.css | 341 - .../src/routes/editor/TranscriptPage.tsx | 881 -- .../src/routes/editor/ZoomModeHelper.tsx | 62 - .../editor/animated-gradient-drag.test.ts | 197 - .../routes/editor/animated-gradient-drag.ts | 52 - .../src/routes/editor/audio-import.test.ts | 133 - .../desktop/src/routes/editor/audio-import.ts | 69 - apps/desktop/src/routes/editor/audio.ts | 49 - .../src/routes/editor/captions-export.ts | 263 - apps/desktop/src/routes/editor/captions.ts | 996 -- .../src/routes/editor/clip-audio.test.ts | 23 - apps/desktop/src/routes/editor/clip-audio.ts | 14 - apps/desktop/src/routes/editor/clip-merge.ts | 168 - .../routes/editor/clip-transitions.test.ts | 211 - .../src/routes/editor/clip-transitions.ts | 306 - .../desktop/src/routes/editor/color-utils.tsx | 101 - .../src/routes/editor/colorCorrection.ts | 211 - apps/desktop/src/routes/editor/context.ts | 2869 ---- .../src/routes/editor/cursor-style.test.ts | 22 - .../desktop/src/routes/editor/cursor-style.ts | 29 - .../src/routes/editor/editor-layout.test.ts | 25 - .../src/routes/editor/editor-layout.ts | 12 - .../src/routes/editor/editor-skeleton.tsx | 232 - .../editor/existing-recording-picker.test.ts | 28 - .../editor/existing-recording-picker.ts | 21 - .../routes/editor/export-estimates.test.ts | 21 - .../src/routes/editor/export-estimates.ts | 26 - .../src/routes/editor/image-overlay.tsx | 290 - .../routes/editor/image-segment-config.tsx | 236 - apps/desktop/src/routes/editor/images.test.ts | 222 - apps/desktop/src/routes/editor/images.ts | 346 - apps/desktop/src/routes/editor/index.tsx | 62 - .../src/routes/editor/keyboard-timing.test.ts | 334 - .../src/routes/editor/keyboard-timing.ts | 206 - apps/desktop/src/routes/editor/masks.test.ts | 44 - apps/desktop/src/routes/editor/masks.ts | 159 - .../routes/editor/overlay-segments.test.ts | 150 - .../src/routes/editor/overlay-segments.ts | 37 - .../editor/playback-intent-routing.test.ts | 163 - .../routes/editor/playback-intent-routing.ts | 14 - .../editor/preparing-editor-context.test.ts | 101 - .../editor/preparing-editor-context.tsx | 277 - .../editor/preparing-editor-model.test.ts | 294 - .../routes/editor/preparing-editor-model.ts | 250 - .../preparing-editor-subscription.test.ts | 198 - .../editor/preparing-editor-subscription.ts | 90 - .../editor/preparing-frame-lease.test.ts | 131 - .../routes/editor/preparing-frame-lease.ts | 60 - .../editor/preparing-frame-transport.test.ts | 285 - .../editor/preparing-frame-transport.ts | 47 - .../src/routes/editor/preparing-frame.tsx | 34 - .../routes/editor/preparing-handoff.test.ts | 494 - .../src/routes/editor/preparing-handoff.ts | 180 - .../editor/preparing-playback-handoff.test.ts | 184 - .../editor/preparing-playback-handoff.ts | 119 - .../src/routes/editor/preparing-timeline.tsx | 123 - .../src/routes/editor/preview-bounds.test.ts | 264 - .../src/routes/editor/preview-bounds.ts | 94 - .../routes/editor/project-config-save.test.ts | 325 - .../src/routes/editor/project-config-save.ts | 86 - .../src/routes/editor/projectConfig.ts | 35 - .../editor/recording-title-save.test.ts | 115 - .../src/routes/editor/recording-title-save.ts | 34 - apps/desktop/src/routes/editor/snapping.ts | 218 - .../routes/editor/style-segment-config.tsx | 167 - apps/desktop/src/routes/editor/style.test.ts | 194 - apps/desktop/src/routes/editor/style.ts | 137 - .../src/routes/editor/text-animations.css | 129 - .../src/routes/editor/text-presets.test.ts | 80 - .../desktop/src/routes/editor/text-presets.ts | 614 - .../src/routes/editor/text-segment-config.tsx | 977 -- apps/desktop/src/routes/editor/text-style.tsx | 187 - apps/desktop/src/routes/editor/text.ts | 159 - .../src/routes/editor/three-d-panel.tsx | 1056 -- .../desktop/src/routes/editor/three-d.test.ts | 1357 -- apps/desktop/src/routes/editor/three-d.ts | 1562 --- .../src/routes/editor/timeline-holds.test.ts | 36 - .../src/routes/editor/timeline-holds.ts | 96 - .../src/routes/editor/timeline-tracks.test.ts | 129 - .../src/routes/editor/timeline-utils.ts | 613 - .../src/routes/editor/timelineTracks.ts | 221 - .../routes/editor/transcript-edits.test.ts | 217 - .../src/routes/editor/transcript-edits.ts | 174 - apps/desktop/src/routes/editor/ui.tsx | 582 - .../src/routes/editor/useEditorShortcuts.ts | 53 - apps/desktop/src/routes/editor/utils.ts | 27 - .../src/routes/in-progress-recording.tsx | 1403 -- apps/desktop/src/routes/mode-select.tsx | 53 - apps/desktop/src/routes/notifications.tsx | 81 - .../desktop/src/routes/recordings-overlay.tsx | 966 -- .../screenshot-editor/AnnotationConfig.tsx | 278 - .../screenshot-editor/AnnotationLayer.tsx | 1064 -- .../screenshot-editor/AnnotationTools.tsx | 110 - .../routes/screenshot-editor/ColorPicker.tsx | 110 - .../src/routes/screenshot-editor/Editor.tsx | 492 - .../src/routes/screenshot-editor/Header.tsx | 218 - .../routes/screenshot-editor/LayersPanel.tsx | 326 - .../screenshot-editor/OcrSelectionOverlay.tsx | 227 - .../src/routes/screenshot-editor/Preview.tsx | 796 -- .../routes/screenshot-editor/TextInput.tsx | 16 - .../src/routes/screenshot-editor/arrow.ts | 30 - .../src/routes/screenshot-editor/context.tsx | 703 - .../src/routes/screenshot-editor/index.tsx | 35 - .../src/routes/screenshot-editor/layout.ts | 180 - .../popovers/AnnotationPopover.tsx | 212 - .../popovers/AspectRatioSelect.tsx | 103 - .../popovers/BackgroundSettingsPopover.tsx | 584 - .../popovers/BorderPopover.tsx | 122 - .../popovers/PaddingPopover.tsx | 82 - .../popovers/RoundingPopover.tsx | 137 - .../popovers/ShadowPopover.tsx | 103 - .../popovers/ShadowSettings.tsx | 90 - .../screenshot-editor-skeleton.tsx | 103 - .../screenshot-editor/screenshotExport.ts | 519 - .../src/routes/screenshot-editor/ui.tsx | 451 - .../screenshot-editor/useScreenshotExport.ts | 262 - .../src/routes/target-select-overlay.tsx | 2451 ---- .../src/routes/teleprompter-utils.test.ts | 134 - apps/desktop/src/routes/teleprompter-utils.ts | 77 - apps/desktop/src/routes/teleprompter.tsx | 604 - .../src/routes/window-capture-occluder.tsx | 84 - apps/desktop/src/store.ts | 151 - apps/desktop/src/store/captions.ts | 391 - apps/desktop/src/store/keyboard.ts | 33 - apps/desktop/src/styles/theme.css | 1327 -- apps/desktop/src/utils/analytics.ts | 134 - apps/desktop/src/utils/area-selection.test.ts | 124 - apps/desktop/src/utils/area-selection.ts | 112 - apps/desktop/src/utils/auth.ts | 232 - apps/desktop/src/utils/automations.ts | 268 - .../src/utils/camera-background.test.ts | 41 - .../src/utils/camera-presentation.test.ts | 127 - apps/desktop/src/utils/camera-presentation.ts | 91 - apps/desktop/src/utils/camera-window.ts | 24 - .../desktop/src/utils/composeEventHandlers.ts | 26 - apps/desktop/src/utils/createEventListener.ts | 42 - apps/desktop/src/utils/createPresets.ts | 76 - apps/desktop/src/utils/currency.ts | 28 - apps/desktop/src/utils/devices.ts | 238 - apps/desktop/src/utils/env.ts | 3 - apps/desktop/src/utils/events.ts | 35 - apps/desktop/src/utils/export.ts | 130 - apps/desktop/src/utils/fonts.ts | 39 - apps/desktop/src/utils/frame-identity.ts | 11 - apps/desktop/src/utils/frame-worker.ts | 1221 -- .../src/utils/general-settings.test.ts | 110 - apps/desktop/src/utils/general-settings.ts | 93 - apps/desktop/src/utils/hex-color.test.ts | 42 - apps/desktop/src/utils/hex-color.ts | 50 - apps/desktop/src/utils/hide-window.ts | 8 - apps/desktop/src/utils/importMedia.ts | 97 - .../src/utils/macos-window-material.ts | 69 - .../src/utils/organization-branding.test.ts | 146 - .../src/utils/organization-branding.ts | 465 - apps/desktop/src/utils/os-permissions.test.ts | 96 - apps/desktop/src/utils/os-permissions.ts | 74 - apps/desktop/src/utils/plans.ts | 34 - apps/desktop/src/utils/pricing.ts | 12 - .../src/utils/queries.clean-capture.test.ts | 54 - .../src/utils/queries.hydration.test.ts | 180 - .../utils/queries.requested-inputs.test.ts | 571 - apps/desktop/src/utils/queries.ts | 471 - apps/desktop/src/utils/recording-menu.test.ts | 66 - apps/desktop/src/utils/recording-menu.ts | 20 - .../src/utils/recording-thumbnail.test.ts | 45 - apps/desktop/src/utils/recording-thumbnail.ts | 44 - apps/desktop/src/utils/recording.test.ts | 93 - apps/desktop/src/utils/recording.ts | 128 - apps/desktop/src/utils/rive.ts | 20 - .../src/utils/serialized-store.test.ts | 147 - apps/desktop/src/utils/serialized-store.ts | 35 - .../src/utils/server-url-routing.test.ts | 87 - apps/desktop/src/utils/server-url-routing.ts | 30 - apps/desktop/src/utils/shared-frame-buffer.ts | 579 - apps/desktop/src/utils/socket.ts | 1449 -- .../src/utils/stride-correction-worker.ts | 65 - apps/desktop/src/utils/tauri.ts | 1446 -- apps/desktop/src/utils/tauriSpectaHack.ts | 28 - .../utils/teleprompter-window-options.test.ts | 32 - .../src/utils/teleprompter-window-options.ts | 43 - apps/desktop/src/utils/teleprompter.test.ts | 132 - apps/desktop/src/utils/teleprompter.ts | 87 - apps/desktop/src/utils/titlebar-state.ts | 60 - apps/desktop/src/utils/updater.test.ts | 112 - apps/desktop/src/utils/updater.ts | 31 - apps/desktop/src/utils/web-api.ts | 84 - .../desktop/src/utils/webgpu-renderer.test.ts | 98 - apps/desktop/src/utils/webgpu-renderer.ts | 578 - apps/desktop/src/vite-env.d.ts | 13 - apps/desktop/tsconfig.json | 32 - apps/desktop/vitest.config.ts | 13 - apps/render-farm/Dockerfile | 1 - apps/render-farm/Dockerfile.dockerignore | 1 - apps/src/utils/tauri.ts | 871 -- biome.json | 11 - bun.lock | 1712 +-- package.json | 7 +- packages/ui-solid/icons/adria-studio.svg | 3 - packages/ui-solid/icons/apps.svg | 6 - packages/ui-solid/icons/arrow-left-circle.svg | 3 - .../ui-solid/icons/arrow-right-circle.svg | 3 - packages/ui-solid/icons/arrows.svg | 3 - packages/ui-solid/icons/audio-off.svg | 3 - packages/ui-solid/icons/audio-on.svg | 3 - packages/ui-solid/icons/bg-blur.svg | 13 - packages/ui-solid/icons/blur.svg | 26 - packages/ui-solid/icons/camera.svg | 4 - packages/ui-solid/icons/captions.svg | 1 - packages/ui-solid/icons/caret-down.svg | 3 - packages/ui-solid/icons/check.svg | 1 - packages/ui-solid/icons/chevron-down.svg | 3 - packages/ui-solid/icons/circle-check.svg | 3 - packages/ui-solid/icons/circle-info.svg | 4 - packages/ui-solid/icons/circle-plus.svg | 3 - packages/ui-solid/icons/circle-x.svg | 3 - packages/ui-solid/icons/circle.svg | 3 - packages/ui-solid/icons/clapperboard.svg | 3 - packages/ui-solid/icons/copy.svg | 3 - packages/ui-solid/icons/corners.svg | 3 - packages/ui-solid/icons/crop.svg | 3 - packages/ui-solid/icons/cursor.svg | 3 - packages/ui-solid/icons/download.svg | 3 - packages/ui-solid/icons/editor.svg | 3 - packages/ui-solid/icons/ellipsis-menu.svg | 5 - packages/ui-solid/icons/enlarge.svg | 3 - packages/ui-solid/icons/file.svg | 1 - packages/ui-solid/icons/film-cut.svg | 3 - packages/ui-solid/icons/filter.svg | 3 - packages/ui-solid/icons/frame-first.svg | 4 - packages/ui-solid/icons/frame-last.svg | 4 - packages/ui-solid/icons/fullscreen.svg | 3 - packages/ui-solid/icons/gauge.svg | 1 - packages/ui-solid/icons/gear.svg | 12 - packages/ui-solid/icons/grid.svg | 11 - packages/ui-solid/icons/hotkeys.svg | 3 - packages/ui-solid/icons/image-filled.svg | 3 - packages/ui-solid/icons/image.svg | 3 - packages/ui-solid/icons/info.svg | 3 - packages/ui-solid/icons/inset.svg | 3 - packages/ui-solid/icons/instant.svg | 3 - packages/ui-solid/icons/layout.svg | 3 - packages/ui-solid/icons/link.svg | 1 - packages/ui-solid/icons/logo-full-dark.svg | 8 - packages/ui-solid/icons/logo-full.svg | 8 - packages/ui-solid/icons/logo.svg | 7 - packages/ui-solid/icons/message-bubble.svg | 4 - packages/ui-solid/icons/microphone.svg | 3 - packages/ui-solid/icons/minimize.svg | 3 - packages/ui-solid/icons/monitor.svg | 45 - packages/ui-solid/icons/more-horizontal.svg | 5 - packages/ui-solid/icons/more-vertical.svg | 5 - packages/ui-solid/icons/move-left.svg | 1 - packages/ui-solid/icons/move-right.svg | 1 - packages/ui-solid/icons/next.svg | 3 - packages/ui-solid/icons/padding.svg | 3 - packages/ui-solid/icons/pause-circle.svg | 5 - packages/ui-solid/icons/pause.svg | 3 - packages/ui-solid/icons/pencil.svg | 3 - packages/ui-solid/icons/play-circle.svg | 4 - packages/ui-solid/icons/play.svg | 3 - packages/ui-solid/icons/presets.svg | 6 - packages/ui-solid/icons/prev.svg | 3 - packages/ui-solid/icons/reaction-add.svg | 3 - packages/ui-solid/icons/reactions.svg | 6 - packages/ui-solid/icons/redo.svg | 3 - packages/ui-solid/icons/restart.svg | 3 - packages/ui-solid/icons/scissors.svg | 4 - packages/ui-solid/icons/screen.svg | 3 - packages/ui-solid/icons/screenshot.svg | 1 - packages/ui-solid/icons/search.svg | 3 - packages/ui-solid/icons/settings.svg | 4 - packages/ui-solid/icons/shadow.svg | 3 - packages/ui-solid/icons/sidebar-1.svg | 4 - packages/ui-solid/icons/sidebar.svg | 4 - packages/ui-solid/icons/sign-out.svg | 3 - packages/ui-solid/icons/square.svg | 3 - packages/ui-solid/icons/star.svg | 3 - packages/ui-solid/icons/statistics.svg | 3 - packages/ui-solid/icons/stop-circle.svg | 4 - packages/ui-solid/icons/support.svg | 3 - packages/ui-solid/icons/trash.svg | 3 - packages/ui-solid/icons/undo.svg | 3 - packages/ui-solid/icons/upload.svg | 3 - packages/ui-solid/icons/views.svg | 4 - packages/ui-solid/icons/window.svg | 4 - packages/ui-solid/icons/x.svg | 3 - packages/ui-solid/icons/zoom-in.svg | 1 - packages/ui-solid/icons/zoom-out.svg | 1 - packages/ui-solid/package.json | 35 - packages/ui-solid/src/Button.tsx | 58 - packages/ui-solid/src/ProgressCircle.tsx | 112 - packages/ui-solid/src/SwitchTab.tsx | 48 - packages/ui-solid/src/auto-imports.d.ts | 141 - packages/ui-solid/src/index.tsx | 3 - packages/ui-solid/src/main.css | 214 - packages/ui-solid/src/types.d.ts | 3 - packages/ui-solid/tsconfig.json | 11 - packages/ui-solid/vite.js | 57 - packages/ui/package.json | 3 - scripts/build-gpui-binary.sh | 9 +- scripts/build-linux-packages.mjs | 26 +- scripts/build-macos-packages.mjs | 37 +- scripts/build-macos-packages.test.mjs | 25 +- scripts/bundle-gpui.mjs | 4 +- scripts/bundle-gpui.test.mjs | 30 +- scripts/check-tauri-plugin-versions.js | 367 - scripts/check-tauri-plugin-versions.test.mjs | 127 - scripts/finalize-linux-appimage.mjs | 6 +- scripts/linux-bundle-config.mjs | 36 +- scripts/linux-bundle-config.test.mjs | 96 +- scripts/run-gpui-build.mjs | 91 - scripts/run-gpui-build.test.mjs | 50 - scripts/setup.js | 41 +- scripts/setup.test.mjs | 26 +- scripts/sync-desktop-versions.mjs | 26 +- scripts/verify-gpui-release-inputs.mjs | 78 - tsconfig.json | 2 - turbo.json | 10 +- vendor/tao/.cargo-ok | 1 - vendor/tao/.cargo_vcs_info.json | 7 - vendor/tao/Cargo.lock | 3267 ----- vendor/tao/Cargo.toml | 391 - vendor/tao/Cargo.toml.orig | 158 - vendor/tao/LICENSE | 201 - vendor/tao/LICENSE.spdx | 18 - vendor/tao/README.md | 80 - vendor/tao/examples/control_flow.rs | 120 - vendor/tao/examples/cursor.rs | 89 - vendor/tao/examples/cursor_grab.rs | 66 - vendor/tao/examples/custom_events.rs | 51 - vendor/tao/examples/decorations.rs | 51 - vendor/tao/examples/drag_window.rs | 78 - vendor/tao/examples/fullscreen.rs | 129 - vendor/tao/examples/handling_close.rs | 87 - vendor/tao/examples/min_max_size.rs | 87 - vendor/tao/examples/minimize.rs | 47 - vendor/tao/examples/monitor_list.rs | 14 - vendor/tao/examples/mouse_wheel.rs | 54 - vendor/tao/examples/multithreaded.rs | 187 - vendor/tao/examples/multiwindow.rs | 55 - vendor/tao/examples/overlay.rs | 128 - vendor/tao/examples/parentwindow.rs | 70 - vendor/tao/examples/progress_bar.rs | 89 - vendor/tao/examples/reopen_event.rs | 45 - vendor/tao/examples/request_redraw.rs | 43 - .../tao/examples/request_redraw_threaded.rs | 45 - vendor/tao/examples/resizable.rs | 51 - vendor/tao/examples/set_ime_position.rs | 56 - vendor/tao/examples/theme.rs | 50 - vendor/tao/examples/timer.rs | 43 - vendor/tao/examples/transparent.rs | 66 - vendor/tao/examples/video_modes.rs | 24 - vendor/tao/examples/window.rs | 52 - vendor/tao/examples/window_debug.rs | 176 - vendor/tao/examples/window_icon.rs | 64 - vendor/tao/examples/window_run_return.rs | 59 - vendor/tao/src/error.rs | 88 - vendor/tao/src/event.rs | 928 -- vendor/tao/src/event_loop.rs | 411 - vendor/tao/src/icon.rs | 169 - vendor/tao/src/keyboard.rs | 1596 --- vendor/tao/src/lib.rs | 199 - vendor/tao/src/monitor.rs | 163 - vendor/tao/src/platform/android.rs | 48 - vendor/tao/src/platform/ios.rs | 345 - vendor/tao/src/platform/linux.rs | 5 - vendor/tao/src/platform/macos.rs | 458 - vendor/tao/src/platform/mod.rs | 28 - vendor/tao/src/platform/run_return.rs | 50 - vendor/tao/src/platform/unix.rs | 308 - vendor/tao/src/platform/windows.rs | 449 - vendor/tao/src/platform_impl/android/mod.rs | 1262 -- .../tao/src/platform_impl/android/ndk_glue.rs | 378 - vendor/tao/src/platform_impl/ios/app_state.rs | 1033 -- vendor/tao/src/platform_impl/ios/badge.rs | 15 - .../tao/src/platform_impl/ios/event_loop.rs | 385 - vendor/tao/src/platform_impl/ios/ffi.rs | 433 - vendor/tao/src/platform_impl/ios/keycode.rs | 13 - vendor/tao/src/platform_impl/ios/mod.rs | 123 - vendor/tao/src/platform_impl/ios/monitor.rs | 300 - vendor/tao/src/platform_impl/ios/view.rs | 696 - vendor/tao/src/platform_impl/ios/window.rs | 790 -- vendor/tao/src/platform_impl/linux/device.rs | 80 - .../tao/src/platform_impl/linux/event_loop.rs | 1290 -- vendor/tao/src/platform_impl/linux/icon.rs | 47 - .../tao/src/platform_impl/linux/keyboard.rs | 288 - vendor/tao/src/platform_impl/linux/keycode.rs | 325 - vendor/tao/src/platform_impl/linux/mod.rs | 97 - vendor/tao/src/platform_impl/linux/monitor.rs | 96 - vendor/tao/src/platform_impl/linux/taskbar.rs | 161 - vendor/tao/src/platform_impl/linux/util.rs | 111 - .../src/platform_impl/linux/wayland/header.rs | 36 - .../src/platform_impl/linux/wayland/mod.rs | 5 - vendor/tao/src/platform_impl/linux/window.rs | 1103 -- vendor/tao/src/platform_impl/linux/x11/ffi.rs | 8 - vendor/tao/src/platform_impl/linux/x11/mod.rs | 8 - .../src/platform_impl/linux/x11/xdisplay.rs | 169 - vendor/tao/src/platform_impl/macos/app.rs | 126 - .../src/platform_impl/macos/app_delegate.rs | 229 - .../tao/src/platform_impl/macos/app_state.rs | 470 - vendor/tao/src/platform_impl/macos/badge.rs | 13 - vendor/tao/src/platform_impl/macos/dock.rs | 91 - vendor/tao/src/platform_impl/macos/event.rs | 335 - .../tao/src/platform_impl/macos/event_loop.rs | 368 - vendor/tao/src/platform_impl/macos/ffi.rs | 273 - vendor/tao/src/platform_impl/macos/icon.rs | 15 - vendor/tao/src/platform_impl/macos/keycode.rs | 264 - vendor/tao/src/platform_impl/macos/mod.rs | 97 - vendor/tao/src/platform_impl/macos/monitor.rs | 338 - .../tao/src/platform_impl/macos/observer.rs | 305 - .../src/platform_impl/macos/progress_bar.rs | 161 - .../tao/src/platform_impl/macos/util/async.rs | 257 - .../src/platform_impl/macos/util/cursor.rs | 177 - .../tao/src/platform_impl/macos/util/mod.rs | 146 - vendor/tao/src/platform_impl/macos/view.rs | 1184 -- vendor/tao/src/platform_impl/macos/window.rs | 1790 --- .../platform_impl/macos/window_delegate.rs | 700 - vendor/tao/src/platform_impl/mod.rs | 40 - .../src/platform_impl/windows/dark_mode.rs | 240 - vendor/tao/src/platform_impl/windows/dpi.rs | 109 - .../src/platform_impl/windows/drop_handler.rs | 181 - vendor/tao/src/platform_impl/windows/event.rs | 421 - .../src/platform_impl/windows/event_loop.rs | 2669 ---- .../windows/event_loop/runner.rs | 470 - vendor/tao/src/platform_impl/windows/icon.rs | 170 - .../tao/src/platform_impl/windows/keyboard.rs | 817 -- .../platform_impl/windows/keyboard_layout.rs | 983 -- .../tao/src/platform_impl/windows/keycode.rs | 338 - .../src/platform_impl/windows/minimal_ime.rs | 88 - vendor/tao/src/platform_impl/windows/mod.rs | 157 - .../tao/src/platform_impl/windows/monitor.rs | 270 - .../src/platform_impl/windows/raw_input.rs | 217 - vendor/tao/src/platform_impl/windows/util.rs | 511 - .../tao/src/platform_impl/windows/window.rs | 1554 --- .../src/platform_impl/windows/window_state.rs | 513 - vendor/tao/src/window.rs | 1699 --- vendor/tauri-nspanel/CAP-CHANGES.md | 67 - vendor/tauri-nspanel/Cargo.toml | 25 - vendor/tauri-nspanel/LICENSE_APACHE-2.0 | 177 - vendor/tauri-nspanel/LICENSE_MIT | 21 - vendor/tauri-nspanel/README.md | 123 - vendor/tauri-nspanel/src/lib.rs | 191 - vendor/tauri-nspanel/src/macros.rs | 139 - vendor/tauri-nspanel/src/raw_nspanel.rs | 351 - 766 files changed, 485 insertions(+), 236362 deletions(-) create mode 100644 apps/desktop-gpui/package.json delete mode 100644 apps/desktop/.gitignore delete mode 100644 apps/desktop/.vscode/extensions.json delete mode 100644 apps/desktop/app.config.ts delete mode 100644 apps/desktop/package.json delete mode 100644 apps/desktop/postcss.config.cjs delete mode 100644 apps/desktop/scripts/desktop-display-transport-benchmark.js delete mode 100644 apps/desktop/scripts/desktop-display-transport-browser.js delete mode 100644 apps/desktop/scripts/desktop-display-transport-browser.test.js delete mode 100644 apps/desktop/scripts/desktop-memory-soak-lib.js delete mode 100644 apps/desktop/scripts/desktop-memory-soak.js delete mode 100644 apps/desktop/scripts/desktop-memory-soak.test.js delete mode 100644 apps/desktop/scripts/desktop-picker-performance.md delete mode 100644 apps/desktop/scripts/prepare.js delete mode 100644 apps/desktop/scripts/prepare.test.js delete mode 100644 apps/desktop/scripts/prodBeforeBundle.js delete mode 100644 apps/desktop/scripts/stop-editor-performance.md delete mode 100644 apps/desktop/src-tauri/.gitignore delete mode 100644 apps/desktop/src-tauri/Cargo.toml delete mode 100644 apps/desktop/src-tauri/Info.plist delete mode 100644 apps/desktop/src-tauri/assets/wix-banner.bmp delete mode 100644 apps/desktop/src-tauri/assets/wix-dialog.bmp 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/examples/desktop-display-transport-benchmark.rs delete mode 100644 apps/desktop/src-tauri/icons/Square107x107Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square142x142Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square150x150Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square284x284Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square30x30Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square44x44Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square71x71Logo.png delete mode 100644 apps/desktop/src-tauri/icons/Square89x89Logo.png delete mode 100644 apps/desktop/src-tauri/icons/StoreLogo.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png delete mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 apps/desktop/src-tauri/icons/icon.icns delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png delete mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png delete mode 100644 apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-default-symbolic.svg delete mode 100644 apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-instant-symbolic.svg delete mode 100644 apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg delete mode 100644 apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-stop-symbolic.svg delete mode 100644 apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-studio-symbolic.svg delete mode 100644 apps/desktop/src-tauri/icons/macos/icon.png delete mode 100644 apps/desktop/src-tauri/icons/tray-default-icon-instant.png delete mode 100644 apps/desktop/src-tauri/icons/tray-default-icon-screenshot.png delete mode 100644 apps/desktop/src-tauri/icons/tray-default-icon-studio.png delete mode 100644 apps/desktop/src-tauri/icons/tray-stop-icon.png delete mode 100644 apps/desktop/src-tauri/sounds/screenshot.ogg delete mode 100644 apps/desktop/src-tauri/src/api.rs delete mode 100644 apps/desktop/src-tauri/src/audio.rs delete mode 100644 apps/desktop/src-tauri/src/audio_library.rs delete mode 100644 apps/desktop/src-tauri/src/audio_meter.rs delete mode 100644 apps/desktop/src-tauri/src/auth.rs delete mode 100644 apps/desktop/src-tauri/src/automation.rs delete mode 100644 apps/desktop/src-tauri/src/camera.rs delete mode 100644 apps/desktop/src-tauri/src/camera.wgsl delete mode 100644 apps/desktop/src-tauri/src/camera_legacy.rs delete mode 100644 apps/desktop/src-tauri/src/camera_native.rs delete mode 100644 apps/desktop/src-tauri/src/captions.rs delete mode 100644 apps/desktop/src-tauri/src/clean_capture.rs delete mode 100644 apps/desktop/src-tauri/src/cli.rs delete mode 100644 apps/desktop/src-tauri/src/clip_thumbnails.rs delete mode 100644 apps/desktop/src-tauri/src/crash_sentinel.rs delete mode 100644 apps/desktop/src-tauri/src/deeplink_actions.rs delete mode 100644 apps/desktop/src-tauri/src/diagnostics.rs delete mode 100644 apps/desktop/src-tauri/src/editor_preparing.rs delete mode 100644 apps/desktop/src-tauri/src/editor_preparing/audio.rs delete mode 100644 apps/desktop/src-tauri/src/editor_recording.rs delete mode 100644 apps/desktop/src-tauri/src/editor_window.rs delete mode 100644 apps/desktop/src-tauri/src/exit_shutdown.rs delete mode 100644 apps/desktop/src-tauri/src/export.rs delete mode 100644 apps/desktop/src-tauri/src/export/estimate_request.rs delete mode 100644 apps/desktop/src-tauri/src/fake_window.rs delete mode 100644 apps/desktop/src-tauri/src/flags.rs delete mode 100644 apps/desktop/src-tauri/src/frame_ws.rs delete mode 100644 apps/desktop/src-tauri/src/general_settings.rs delete mode 100644 apps/desktop/src-tauri/src/gpu_context.rs delete mode 100644 apps/desktop/src-tauri/src/gpu_device_health.rs delete mode 100644 apps/desktop/src-tauri/src/gpui_app.rs delete mode 100644 apps/desktop/src-tauri/src/hotkeys.rs delete mode 100644 apps/desktop/src-tauri/src/http_client.rs delete mode 100644 apps/desktop/src-tauri/src/import.rs delete mode 100644 apps/desktop/src-tauri/src/lib.rs delete mode 100644 apps/desktop/src-tauri/src/linux_instant_camera.rs delete mode 100644 apps/desktop/src-tauri/src/logging.rs delete mode 100644 apps/desktop/src-tauri/src/macos_save_panel.rs delete mode 100644 apps/desktop/src-tauri/src/main.rs delete mode 100644 apps/desktop/src-tauri/src/main_window_geometry.rs delete mode 100644 apps/desktop/src-tauri/src/notifications.rs delete mode 100644 apps/desktop/src-tauri/src/panel_manager.rs delete mode 100644 apps/desktop/src-tauri/src/permissions.rs delete mode 100644 apps/desktop/src-tauri/src/picker-benchmark.js delete mode 100644 apps/desktop/src-tauri/src/picker_benchmark.rs delete mode 100644 apps/desktop/src-tauri/src/platform/macos/delegates.rs delete mode 100644 apps/desktop/src-tauri/src/platform/macos/mod.rs delete mode 100644 apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs delete mode 100644 apps/desktop/src-tauri/src/platform/mod.rs delete mode 100644 apps/desktop/src-tauri/src/platform/win.rs delete mode 100644 apps/desktop/src-tauri/src/power_observer.rs delete mode 100644 apps/desktop/src-tauri/src/preparing_finalization.rs delete mode 100644 apps/desktop/src-tauri/src/presets.rs delete mode 100644 apps/desktop/src-tauri/src/recording.rs delete mode 100644 apps/desktop/src-tauri/src/recording_settings.rs delete mode 100644 apps/desktop/src-tauri/src/recording_telemetry.rs delete mode 100644 apps/desktop/src-tauri/src/recordings_locations.rs delete mode 100644 apps/desktop/src-tauri/src/recovery.rs delete mode 100644 apps/desktop/src-tauri/src/render_frame_event.rs delete mode 100644 apps/desktop/src-tauri/src/resource.rs delete mode 100644 apps/desktop/src-tauri/src/screenshot_editor.rs delete mode 100644 apps/desktop/src-tauri/src/startup.rs delete mode 100644 apps/desktop/src-tauri/src/stop-editor-benchmark.js delete mode 100644 apps/desktop/src-tauri/src/stop-editor-parity-dom.js delete mode 100644 apps/desktop/src-tauri/src/stop_editor_benchmark.rs delete mode 100644 apps/desktop/src-tauri/src/target_select_overlay.rs delete mode 100644 apps/desktop/src-tauri/src/target_select_overlay/lifecycle.rs delete mode 100644 apps/desktop/src-tauri/src/telemetry.rs delete mode 100644 apps/desktop/src-tauri/src/thumbnails/linux.rs delete mode 100644 apps/desktop/src-tauri/src/thumbnails/mac.rs delete mode 100644 apps/desktop/src-tauri/src/thumbnails/mod.rs delete mode 100644 apps/desktop/src-tauri/src/thumbnails/windows.rs delete mode 100644 apps/desktop/src-tauri/src/tray.rs delete mode 100644 apps/desktop/src-tauri/src/update_project_names.rs delete mode 100644 apps/desktop/src-tauri/src/updates.rs delete mode 100644 apps/desktop/src-tauri/src/upload.rs delete mode 100644 apps/desktop/src-tauri/src/upload/lifecycle.rs delete mode 100644 apps/desktop/src-tauri/src/upload/preparation.rs delete mode 100644 apps/desktop/src-tauri/src/upload/recovery_age.rs delete mode 100644 apps/desktop/src-tauri/src/upload/resume.rs delete mode 100644 apps/desktop/src-tauri/src/web_api.rs delete mode 100644 apps/desktop/src-tauri/src/window_exclusion.rs delete mode 100644 apps/desktop/src-tauri/src/window_position_persistence.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 delete mode 100644 apps/desktop/src-tauri/tests/crash_sentinel.rs delete mode 100644 apps/desktop/src-tauri/tests/exit_shutdown.rs delete mode 100644 apps/desktop/src-tauri/tests/export_estimate_request.rs delete mode 100644 apps/desktop/src-tauri/tests/web_api_startup.rs delete mode 100644 apps/desktop/src/app.tsx delete mode 100644 apps/desktop/src/assets/illustrations/cloud-1.png delete mode 100644 apps/desktop/src/assets/illustrations/cloud-2.png delete mode 100644 apps/desktop/src/assets/illustrations/cloud-3.png delete mode 100644 apps/desktop/src/assets/illustrations/color.webp delete mode 100644 apps/desktop/src/assets/illustrations/display.png delete mode 100644 apps/desktop/src/assets/illustrations/gradient.webp delete mode 100644 apps/desktop/src/assets/illustrations/image.webp delete mode 100644 apps/desktop/src/assets/illustrations/instant-mode-dark.png delete mode 100644 apps/desktop/src/assets/illustrations/instant-mode-light.png delete mode 100644 apps/desktop/src/assets/illustrations/studio-mode-dark.png delete mode 100644 apps/desktop/src/assets/illustrations/studio-mode-light.png delete mode 100644 apps/desktop/src/assets/illustrations/transparent.webp delete mode 100644 apps/desktop/src/assets/instant-toggle.jpg delete mode 100644 apps/desktop/src/assets/rive/pricing.riv delete mode 100644 apps/desktop/src/assets/theme-previews/auto.jpg delete mode 100644 apps/desktop/src/assets/theme-previews/dark.jpg delete mode 100644 apps/desktop/src/assets/theme-previews/legacy-auto.jpg delete mode 100644 apps/desktop/src/assets/theme-previews/legacy-dark.jpg delete mode 100644 apps/desktop/src/assets/theme-previews/legacy-light.jpg delete mode 100644 apps/desktop/src/assets/theme-previews/light.jpg delete mode 100644 apps/desktop/src/components/CameraPreviewChrome.tsx delete mode 100644 apps/desktop/src/components/CapErrorBoundary.tsx delete mode 100644 apps/desktop/src/components/Cropper.tsx delete mode 100644 apps/desktop/src/components/Loader.tsx delete mode 100644 apps/desktop/src/components/Mode.tsx delete mode 100644 apps/desktop/src/components/ModeSelect.tsx delete mode 100644 apps/desktop/src/components/RecoveryToast.tsx delete mode 100644 apps/desktop/src/components/SignInButton.tsx delete mode 100644 apps/desktop/src/components/SwitchTab.tsx delete mode 100644 apps/desktop/src/components/Toggle.tsx delete mode 100644 apps/desktop/src/components/Tooltip.tsx delete mode 100644 apps/desktop/src/components/TooltipIconButton.tsx delete mode 100644 apps/desktop/src/components/callback.template.ts delete mode 100644 apps/desktop/src/components/crop-alignment.test.ts delete mode 100644 apps/desktop/src/components/crop-alignment.ts delete mode 100644 apps/desktop/src/components/cropper.css delete mode 100644 apps/desktop/src/components/selection-hint.tsx delete mode 100644 apps/desktop/src/components/titlebar/Titlebar.tsx delete mode 100644 apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx delete mode 100644 apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx delete mode 100644 apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx delete mode 100644 apps/desktop/src/declaration.d.ts delete mode 100644 apps/desktop/src/entry-client.tsx delete mode 100644 apps/desktop/src/entry-server.tsx delete mode 100644 apps/desktop/src/global.d.ts delete mode 100644 apps/desktop/src/icons.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome).tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/Context.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/OptionsContext.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/icons.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/DeviceSelectOverlay.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/InfoPill.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/ModeInfoPanel.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/TargetDropdownButton.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/TargetMenuGrid.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/TargetSelectInfoPill.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/TargetTypeButton.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/deviceRowStyles.ts delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/index.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/use-request-permission.test.ts delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/useRequestPermission.ts delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/window-geometry.test.ts delete mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/window-geometry.ts delete mode 100644 apps/desktop/src/routes/(window-chrome)/onboarding.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/automations.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/cli.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/general.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/index.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/integrations/config-header.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/integrations/google-drive-config.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/integrations/index.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/license.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/quality.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/transcription.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/update.tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/upgrade.tsx delete mode 100644 apps/desktop/src/routes/camera.tsx delete mode 100644 apps/desktop/src/routes/capture-area.tsx delete mode 100644 apps/desktop/src/routes/debug.tsx delete mode 100644 apps/desktop/src/routes/editor/AnimatedGradientEditor.tsx delete mode 100644 apps/desktop/src/routes/editor/AspectRatioSelect.tsx delete mode 100644 apps/desktop/src/routes/editor/AudioLibrary.tsx delete mode 100644 apps/desktop/src/routes/editor/BrandColorsDropdown.tsx delete mode 100644 apps/desktop/src/routes/editor/CanvasElementsOverlay.tsx delete mode 100644 apps/desktop/src/routes/editor/CaptionOverlay.tsx delete mode 100644 apps/desktop/src/routes/editor/CaptionsRegenerateBadge.tsx delete mode 100644 apps/desktop/src/routes/editor/CaptionsTab.tsx delete mode 100644 apps/desktop/src/routes/editor/ClipsSidebar.tsx delete mode 100644 apps/desktop/src/routes/editor/ColorCorrectionSection.tsx delete mode 100644 apps/desktop/src/routes/editor/ConfigSidebar.tsx delete mode 100644 apps/desktop/src/routes/editor/CursorStylePicker.tsx delete mode 100644 apps/desktop/src/routes/editor/Editor.tsx delete mode 100644 apps/desktop/src/routes/editor/EditorErrorScreen.tsx delete mode 100644 apps/desktop/src/routes/editor/ExportPage.tsx delete mode 100644 apps/desktop/src/routes/editor/FontPicker.tsx delete mode 100644 apps/desktop/src/routes/editor/FrameButton.tsx delete mode 100644 apps/desktop/src/routes/editor/GradientEditor.tsx delete mode 100644 apps/desktop/src/routes/editor/Header.tsx delete mode 100644 apps/desktop/src/routes/editor/ImportProgress.tsx delete mode 100644 apps/desktop/src/routes/editor/KeyboardTab.tsx delete mode 100644 apps/desktop/src/routes/editor/MaskOverlay.tsx delete mode 100644 apps/desktop/src/routes/editor/OrganizationDropdown.tsx delete mode 100644 apps/desktop/src/routes/editor/PerformanceOverlay.tsx delete mode 100644 apps/desktop/src/routes/editor/Player.tsx delete mode 100644 apps/desktop/src/routes/editor/PresetsDropdown.tsx delete mode 100644 apps/desktop/src/routes/editor/ShadowSettings.tsx delete mode 100644 apps/desktop/src/routes/editor/ShareButton.tsx delete mode 100644 apps/desktop/src/routes/editor/SplitScreenOverlay.tsx delete mode 100644 apps/desktop/src/routes/editor/TextInput.tsx delete mode 100644 apps/desktop/src/routes/editor/TextOverlay.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/AudioTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/CaptionsTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/KeyboardTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/MaskTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/Minimap.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/SceneTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/TextTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/ThreeDTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/Track.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/TrackManager.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/context.ts delete mode 100644 apps/desktop/src/routes/editor/Timeline/image-track.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/index.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/playback-follow.test.ts delete mode 100644 apps/desktop/src/routes/editor/Timeline/playback-follow.ts delete mode 100644 apps/desktop/src/routes/editor/Timeline/sectionMarker.ts delete mode 100644 apps/desktop/src/routes/editor/Timeline/split-snapping.ts delete mode 100644 apps/desktop/src/routes/editor/Timeline/style-track.tsx delete mode 100644 apps/desktop/src/routes/editor/Timeline/styles.css delete mode 100644 apps/desktop/src/routes/editor/TranscriptPage.tsx delete mode 100644 apps/desktop/src/routes/editor/ZoomModeHelper.tsx delete mode 100644 apps/desktop/src/routes/editor/animated-gradient-drag.test.ts delete mode 100644 apps/desktop/src/routes/editor/animated-gradient-drag.ts delete mode 100644 apps/desktop/src/routes/editor/audio-import.test.ts delete mode 100644 apps/desktop/src/routes/editor/audio-import.ts delete mode 100644 apps/desktop/src/routes/editor/audio.ts delete mode 100644 apps/desktop/src/routes/editor/captions-export.ts delete mode 100644 apps/desktop/src/routes/editor/captions.ts delete mode 100644 apps/desktop/src/routes/editor/clip-audio.test.ts delete mode 100644 apps/desktop/src/routes/editor/clip-audio.ts delete mode 100644 apps/desktop/src/routes/editor/clip-merge.ts delete mode 100644 apps/desktop/src/routes/editor/clip-transitions.test.ts delete mode 100644 apps/desktop/src/routes/editor/clip-transitions.ts delete mode 100644 apps/desktop/src/routes/editor/color-utils.tsx delete mode 100644 apps/desktop/src/routes/editor/colorCorrection.ts delete mode 100644 apps/desktop/src/routes/editor/context.ts delete mode 100644 apps/desktop/src/routes/editor/cursor-style.test.ts delete mode 100644 apps/desktop/src/routes/editor/cursor-style.ts delete mode 100644 apps/desktop/src/routes/editor/editor-layout.test.ts delete mode 100644 apps/desktop/src/routes/editor/editor-layout.ts delete mode 100644 apps/desktop/src/routes/editor/editor-skeleton.tsx delete mode 100644 apps/desktop/src/routes/editor/existing-recording-picker.test.ts delete mode 100644 apps/desktop/src/routes/editor/existing-recording-picker.ts delete mode 100644 apps/desktop/src/routes/editor/export-estimates.test.ts delete mode 100644 apps/desktop/src/routes/editor/export-estimates.ts delete mode 100644 apps/desktop/src/routes/editor/image-overlay.tsx delete mode 100644 apps/desktop/src/routes/editor/image-segment-config.tsx delete mode 100644 apps/desktop/src/routes/editor/images.test.ts delete mode 100644 apps/desktop/src/routes/editor/images.ts delete mode 100644 apps/desktop/src/routes/editor/index.tsx delete mode 100644 apps/desktop/src/routes/editor/keyboard-timing.test.ts delete mode 100644 apps/desktop/src/routes/editor/keyboard-timing.ts delete mode 100644 apps/desktop/src/routes/editor/masks.test.ts delete mode 100644 apps/desktop/src/routes/editor/masks.ts delete mode 100644 apps/desktop/src/routes/editor/overlay-segments.test.ts delete mode 100644 apps/desktop/src/routes/editor/overlay-segments.ts delete mode 100644 apps/desktop/src/routes/editor/playback-intent-routing.test.ts delete mode 100644 apps/desktop/src/routes/editor/playback-intent-routing.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-editor-context.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-editor-context.tsx delete mode 100644 apps/desktop/src/routes/editor/preparing-editor-model.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-editor-model.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-editor-subscription.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-editor-subscription.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-frame-lease.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-frame-lease.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-frame-transport.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-frame-transport.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-frame.tsx delete mode 100644 apps/desktop/src/routes/editor/preparing-handoff.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-handoff.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-playback-handoff.test.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-playback-handoff.ts delete mode 100644 apps/desktop/src/routes/editor/preparing-timeline.tsx delete mode 100644 apps/desktop/src/routes/editor/preview-bounds.test.ts delete mode 100644 apps/desktop/src/routes/editor/preview-bounds.ts delete mode 100644 apps/desktop/src/routes/editor/project-config-save.test.ts delete mode 100644 apps/desktop/src/routes/editor/project-config-save.ts delete mode 100644 apps/desktop/src/routes/editor/projectConfig.ts delete mode 100644 apps/desktop/src/routes/editor/recording-title-save.test.ts delete mode 100644 apps/desktop/src/routes/editor/recording-title-save.ts delete mode 100644 apps/desktop/src/routes/editor/snapping.ts delete mode 100644 apps/desktop/src/routes/editor/style-segment-config.tsx delete mode 100644 apps/desktop/src/routes/editor/style.test.ts delete mode 100644 apps/desktop/src/routes/editor/style.ts delete mode 100644 apps/desktop/src/routes/editor/text-animations.css delete mode 100644 apps/desktop/src/routes/editor/text-presets.test.ts delete mode 100644 apps/desktop/src/routes/editor/text-presets.ts delete mode 100644 apps/desktop/src/routes/editor/text-segment-config.tsx delete mode 100644 apps/desktop/src/routes/editor/text-style.tsx delete mode 100644 apps/desktop/src/routes/editor/text.ts delete mode 100644 apps/desktop/src/routes/editor/three-d-panel.tsx delete mode 100644 apps/desktop/src/routes/editor/three-d.test.ts delete mode 100644 apps/desktop/src/routes/editor/three-d.ts delete mode 100644 apps/desktop/src/routes/editor/timeline-holds.test.ts delete mode 100644 apps/desktop/src/routes/editor/timeline-holds.ts delete mode 100644 apps/desktop/src/routes/editor/timeline-tracks.test.ts delete mode 100644 apps/desktop/src/routes/editor/timeline-utils.ts delete mode 100644 apps/desktop/src/routes/editor/timelineTracks.ts delete mode 100644 apps/desktop/src/routes/editor/transcript-edits.test.ts delete mode 100644 apps/desktop/src/routes/editor/transcript-edits.ts delete mode 100644 apps/desktop/src/routes/editor/ui.tsx delete mode 100644 apps/desktop/src/routes/editor/useEditorShortcuts.ts delete mode 100644 apps/desktop/src/routes/editor/utils.ts delete mode 100644 apps/desktop/src/routes/in-progress-recording.tsx delete mode 100644 apps/desktop/src/routes/mode-select.tsx delete mode 100644 apps/desktop/src/routes/notifications.tsx delete mode 100644 apps/desktop/src/routes/recordings-overlay.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/AnnotationConfig.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/AnnotationLayer.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/AnnotationTools.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/ColorPicker.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/Editor.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/Header.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/LayersPanel.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/OcrSelectionOverlay.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/Preview.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/TextInput.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/arrow.ts delete mode 100644 apps/desktop/src/routes/screenshot-editor/context.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/index.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/layout.ts delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/AnnotationPopover.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/AspectRatioSelect.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/BackgroundSettingsPopover.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/BorderPopover.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/PaddingPopover.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/RoundingPopover.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/ShadowPopover.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/popovers/ShadowSettings.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/screenshot-editor-skeleton.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/screenshotExport.ts delete mode 100644 apps/desktop/src/routes/screenshot-editor/ui.tsx delete mode 100644 apps/desktop/src/routes/screenshot-editor/useScreenshotExport.ts delete mode 100644 apps/desktop/src/routes/target-select-overlay.tsx delete mode 100644 apps/desktop/src/routes/teleprompter-utils.test.ts delete mode 100644 apps/desktop/src/routes/teleprompter-utils.ts delete mode 100644 apps/desktop/src/routes/teleprompter.tsx delete mode 100644 apps/desktop/src/routes/window-capture-occluder.tsx delete mode 100644 apps/desktop/src/store.ts delete mode 100644 apps/desktop/src/store/captions.ts delete mode 100644 apps/desktop/src/store/keyboard.ts delete mode 100644 apps/desktop/src/styles/theme.css delete mode 100644 apps/desktop/src/utils/analytics.ts delete mode 100644 apps/desktop/src/utils/area-selection.test.ts delete mode 100644 apps/desktop/src/utils/area-selection.ts delete mode 100644 apps/desktop/src/utils/auth.ts delete mode 100644 apps/desktop/src/utils/automations.ts delete mode 100644 apps/desktop/src/utils/camera-background.test.ts delete mode 100644 apps/desktop/src/utils/camera-presentation.test.ts delete mode 100644 apps/desktop/src/utils/camera-presentation.ts delete mode 100644 apps/desktop/src/utils/camera-window.ts delete mode 100644 apps/desktop/src/utils/composeEventHandlers.ts delete mode 100644 apps/desktop/src/utils/createEventListener.ts delete mode 100644 apps/desktop/src/utils/createPresets.ts delete mode 100644 apps/desktop/src/utils/currency.ts delete mode 100644 apps/desktop/src/utils/devices.ts delete mode 100644 apps/desktop/src/utils/env.ts delete mode 100644 apps/desktop/src/utils/events.ts delete mode 100644 apps/desktop/src/utils/export.ts delete mode 100644 apps/desktop/src/utils/fonts.ts delete mode 100644 apps/desktop/src/utils/frame-identity.ts delete mode 100644 apps/desktop/src/utils/frame-worker.ts delete mode 100644 apps/desktop/src/utils/general-settings.test.ts delete mode 100644 apps/desktop/src/utils/general-settings.ts delete mode 100644 apps/desktop/src/utils/hex-color.test.ts delete mode 100644 apps/desktop/src/utils/hex-color.ts delete mode 100644 apps/desktop/src/utils/hide-window.ts delete mode 100644 apps/desktop/src/utils/importMedia.ts delete mode 100644 apps/desktop/src/utils/macos-window-material.ts delete mode 100644 apps/desktop/src/utils/organization-branding.test.ts delete mode 100644 apps/desktop/src/utils/organization-branding.ts delete mode 100644 apps/desktop/src/utils/os-permissions.test.ts delete mode 100644 apps/desktop/src/utils/os-permissions.ts delete mode 100644 apps/desktop/src/utils/plans.ts delete mode 100644 apps/desktop/src/utils/pricing.ts delete mode 100644 apps/desktop/src/utils/queries.clean-capture.test.ts delete mode 100644 apps/desktop/src/utils/queries.hydration.test.ts delete mode 100644 apps/desktop/src/utils/queries.requested-inputs.test.ts delete mode 100644 apps/desktop/src/utils/queries.ts delete mode 100644 apps/desktop/src/utils/recording-menu.test.ts delete mode 100644 apps/desktop/src/utils/recording-menu.ts delete mode 100644 apps/desktop/src/utils/recording-thumbnail.test.ts delete mode 100644 apps/desktop/src/utils/recording-thumbnail.ts delete mode 100644 apps/desktop/src/utils/recording.test.ts delete mode 100644 apps/desktop/src/utils/recording.ts delete mode 100644 apps/desktop/src/utils/rive.ts delete mode 100644 apps/desktop/src/utils/serialized-store.test.ts delete mode 100644 apps/desktop/src/utils/serialized-store.ts delete mode 100644 apps/desktop/src/utils/server-url-routing.test.ts delete mode 100644 apps/desktop/src/utils/server-url-routing.ts delete mode 100644 apps/desktop/src/utils/shared-frame-buffer.ts delete mode 100644 apps/desktop/src/utils/socket.ts delete mode 100644 apps/desktop/src/utils/stride-correction-worker.ts delete mode 100644 apps/desktop/src/utils/tauri.ts delete mode 100644 apps/desktop/src/utils/tauriSpectaHack.ts delete mode 100644 apps/desktop/src/utils/teleprompter-window-options.test.ts delete mode 100644 apps/desktop/src/utils/teleprompter-window-options.ts delete mode 100644 apps/desktop/src/utils/teleprompter.test.ts delete mode 100644 apps/desktop/src/utils/teleprompter.ts delete mode 100644 apps/desktop/src/utils/titlebar-state.ts delete mode 100644 apps/desktop/src/utils/updater.test.ts delete mode 100644 apps/desktop/src/utils/updater.ts delete mode 100644 apps/desktop/src/utils/web-api.ts delete mode 100644 apps/desktop/src/utils/webgpu-renderer.test.ts delete mode 100644 apps/desktop/src/utils/webgpu-renderer.ts delete mode 100644 apps/desktop/src/vite-env.d.ts delete mode 100644 apps/desktop/tsconfig.json delete mode 100644 apps/desktop/vitest.config.ts delete mode 100644 apps/src/utils/tauri.ts delete mode 100644 packages/ui-solid/icons/adria-studio.svg delete mode 100644 packages/ui-solid/icons/apps.svg delete mode 100644 packages/ui-solid/icons/arrow-left-circle.svg delete mode 100644 packages/ui-solid/icons/arrow-right-circle.svg delete mode 100644 packages/ui-solid/icons/arrows.svg delete mode 100644 packages/ui-solid/icons/audio-off.svg delete mode 100644 packages/ui-solid/icons/audio-on.svg delete mode 100644 packages/ui-solid/icons/bg-blur.svg delete mode 100644 packages/ui-solid/icons/blur.svg delete mode 100644 packages/ui-solid/icons/camera.svg delete mode 100644 packages/ui-solid/icons/captions.svg delete mode 100644 packages/ui-solid/icons/caret-down.svg delete mode 100644 packages/ui-solid/icons/check.svg delete mode 100644 packages/ui-solid/icons/chevron-down.svg delete mode 100644 packages/ui-solid/icons/circle-check.svg delete mode 100644 packages/ui-solid/icons/circle-info.svg delete mode 100644 packages/ui-solid/icons/circle-plus.svg delete mode 100644 packages/ui-solid/icons/circle-x.svg delete mode 100644 packages/ui-solid/icons/circle.svg delete mode 100644 packages/ui-solid/icons/clapperboard.svg delete mode 100644 packages/ui-solid/icons/copy.svg delete mode 100644 packages/ui-solid/icons/corners.svg delete mode 100644 packages/ui-solid/icons/crop.svg delete mode 100644 packages/ui-solid/icons/cursor.svg delete mode 100644 packages/ui-solid/icons/download.svg delete mode 100644 packages/ui-solid/icons/editor.svg delete mode 100644 packages/ui-solid/icons/ellipsis-menu.svg delete mode 100644 packages/ui-solid/icons/enlarge.svg delete mode 100644 packages/ui-solid/icons/file.svg delete mode 100644 packages/ui-solid/icons/film-cut.svg delete mode 100644 packages/ui-solid/icons/filter.svg delete mode 100644 packages/ui-solid/icons/frame-first.svg delete mode 100644 packages/ui-solid/icons/frame-last.svg delete mode 100644 packages/ui-solid/icons/fullscreen.svg delete mode 100644 packages/ui-solid/icons/gauge.svg delete mode 100644 packages/ui-solid/icons/gear.svg delete mode 100644 packages/ui-solid/icons/grid.svg delete mode 100644 packages/ui-solid/icons/hotkeys.svg delete mode 100644 packages/ui-solid/icons/image-filled.svg delete mode 100644 packages/ui-solid/icons/image.svg delete mode 100644 packages/ui-solid/icons/info.svg delete mode 100644 packages/ui-solid/icons/inset.svg delete mode 100644 packages/ui-solid/icons/instant.svg delete mode 100644 packages/ui-solid/icons/layout.svg delete mode 100644 packages/ui-solid/icons/link.svg delete mode 100644 packages/ui-solid/icons/logo-full-dark.svg delete mode 100644 packages/ui-solid/icons/logo-full.svg delete mode 100644 packages/ui-solid/icons/logo.svg delete mode 100644 packages/ui-solid/icons/message-bubble.svg delete mode 100644 packages/ui-solid/icons/microphone.svg delete mode 100644 packages/ui-solid/icons/minimize.svg delete mode 100644 packages/ui-solid/icons/monitor.svg delete mode 100644 packages/ui-solid/icons/more-horizontal.svg delete mode 100644 packages/ui-solid/icons/more-vertical.svg delete mode 100644 packages/ui-solid/icons/move-left.svg delete mode 100644 packages/ui-solid/icons/move-right.svg delete mode 100644 packages/ui-solid/icons/next.svg delete mode 100644 packages/ui-solid/icons/padding.svg delete mode 100644 packages/ui-solid/icons/pause-circle.svg delete mode 100644 packages/ui-solid/icons/pause.svg delete mode 100644 packages/ui-solid/icons/pencil.svg delete mode 100644 packages/ui-solid/icons/play-circle.svg delete mode 100644 packages/ui-solid/icons/play.svg delete mode 100644 packages/ui-solid/icons/presets.svg delete mode 100644 packages/ui-solid/icons/prev.svg delete mode 100644 packages/ui-solid/icons/reaction-add.svg delete mode 100644 packages/ui-solid/icons/reactions.svg delete mode 100644 packages/ui-solid/icons/redo.svg delete mode 100644 packages/ui-solid/icons/restart.svg delete mode 100644 packages/ui-solid/icons/scissors.svg delete mode 100644 packages/ui-solid/icons/screen.svg delete mode 100644 packages/ui-solid/icons/screenshot.svg delete mode 100644 packages/ui-solid/icons/search.svg delete mode 100644 packages/ui-solid/icons/settings.svg delete mode 100644 packages/ui-solid/icons/shadow.svg delete mode 100644 packages/ui-solid/icons/sidebar-1.svg delete mode 100644 packages/ui-solid/icons/sidebar.svg delete mode 100644 packages/ui-solid/icons/sign-out.svg delete mode 100644 packages/ui-solid/icons/square.svg delete mode 100644 packages/ui-solid/icons/star.svg delete mode 100644 packages/ui-solid/icons/statistics.svg delete mode 100644 packages/ui-solid/icons/stop-circle.svg delete mode 100644 packages/ui-solid/icons/support.svg delete mode 100644 packages/ui-solid/icons/trash.svg delete mode 100644 packages/ui-solid/icons/undo.svg delete mode 100644 packages/ui-solid/icons/upload.svg delete mode 100644 packages/ui-solid/icons/views.svg delete mode 100644 packages/ui-solid/icons/window.svg delete mode 100644 packages/ui-solid/icons/x.svg delete mode 100644 packages/ui-solid/icons/zoom-in.svg delete mode 100644 packages/ui-solid/icons/zoom-out.svg delete mode 100644 packages/ui-solid/package.json delete mode 100644 packages/ui-solid/src/Button.tsx delete mode 100644 packages/ui-solid/src/ProgressCircle.tsx delete mode 100644 packages/ui-solid/src/SwitchTab.tsx delete mode 100644 packages/ui-solid/src/auto-imports.d.ts delete mode 100644 packages/ui-solid/src/index.tsx delete mode 100644 packages/ui-solid/src/main.css delete mode 100644 packages/ui-solid/src/types.d.ts delete mode 100644 packages/ui-solid/tsconfig.json delete mode 100644 packages/ui-solid/vite.js delete mode 100755 scripts/check-tauri-plugin-versions.js delete mode 100644 scripts/check-tauri-plugin-versions.test.mjs delete mode 100644 scripts/run-gpui-build.mjs delete mode 100644 scripts/run-gpui-build.test.mjs delete mode 100644 scripts/verify-gpui-release-inputs.mjs delete mode 100644 vendor/tao/.cargo-ok delete mode 100644 vendor/tao/.cargo_vcs_info.json delete mode 100644 vendor/tao/Cargo.lock delete mode 100644 vendor/tao/Cargo.toml delete mode 100644 vendor/tao/Cargo.toml.orig delete mode 100644 vendor/tao/LICENSE delete mode 100644 vendor/tao/LICENSE.spdx delete mode 100644 vendor/tao/README.md delete mode 100644 vendor/tao/examples/control_flow.rs delete mode 100644 vendor/tao/examples/cursor.rs delete mode 100644 vendor/tao/examples/cursor_grab.rs delete mode 100644 vendor/tao/examples/custom_events.rs delete mode 100644 vendor/tao/examples/decorations.rs delete mode 100644 vendor/tao/examples/drag_window.rs delete mode 100644 vendor/tao/examples/fullscreen.rs delete mode 100644 vendor/tao/examples/handling_close.rs delete mode 100644 vendor/tao/examples/min_max_size.rs delete mode 100644 vendor/tao/examples/minimize.rs delete mode 100644 vendor/tao/examples/monitor_list.rs delete mode 100644 vendor/tao/examples/mouse_wheel.rs delete mode 100644 vendor/tao/examples/multithreaded.rs delete mode 100644 vendor/tao/examples/multiwindow.rs delete mode 100644 vendor/tao/examples/overlay.rs delete mode 100644 vendor/tao/examples/parentwindow.rs delete mode 100644 vendor/tao/examples/progress_bar.rs delete mode 100644 vendor/tao/examples/reopen_event.rs delete mode 100644 vendor/tao/examples/request_redraw.rs delete mode 100644 vendor/tao/examples/request_redraw_threaded.rs delete mode 100644 vendor/tao/examples/resizable.rs delete mode 100644 vendor/tao/examples/set_ime_position.rs delete mode 100644 vendor/tao/examples/theme.rs delete mode 100644 vendor/tao/examples/timer.rs delete mode 100644 vendor/tao/examples/transparent.rs delete mode 100644 vendor/tao/examples/video_modes.rs delete mode 100644 vendor/tao/examples/window.rs delete mode 100644 vendor/tao/examples/window_debug.rs delete mode 100644 vendor/tao/examples/window_icon.rs delete mode 100644 vendor/tao/examples/window_run_return.rs delete mode 100644 vendor/tao/src/error.rs delete mode 100644 vendor/tao/src/event.rs delete mode 100644 vendor/tao/src/event_loop.rs delete mode 100644 vendor/tao/src/icon.rs delete mode 100644 vendor/tao/src/keyboard.rs delete mode 100644 vendor/tao/src/lib.rs delete mode 100644 vendor/tao/src/monitor.rs delete mode 100644 vendor/tao/src/platform/android.rs delete mode 100644 vendor/tao/src/platform/ios.rs delete mode 100644 vendor/tao/src/platform/linux.rs delete mode 100644 vendor/tao/src/platform/macos.rs delete mode 100644 vendor/tao/src/platform/mod.rs delete mode 100644 vendor/tao/src/platform/run_return.rs delete mode 100644 vendor/tao/src/platform/unix.rs delete mode 100644 vendor/tao/src/platform/windows.rs delete mode 100644 vendor/tao/src/platform_impl/android/mod.rs delete mode 100644 vendor/tao/src/platform_impl/android/ndk_glue.rs delete mode 100644 vendor/tao/src/platform_impl/ios/app_state.rs delete mode 100644 vendor/tao/src/platform_impl/ios/badge.rs delete mode 100644 vendor/tao/src/platform_impl/ios/event_loop.rs delete mode 100644 vendor/tao/src/platform_impl/ios/ffi.rs delete mode 100644 vendor/tao/src/platform_impl/ios/keycode.rs delete mode 100644 vendor/tao/src/platform_impl/ios/mod.rs delete mode 100644 vendor/tao/src/platform_impl/ios/monitor.rs delete mode 100644 vendor/tao/src/platform_impl/ios/view.rs delete mode 100644 vendor/tao/src/platform_impl/ios/window.rs delete mode 100644 vendor/tao/src/platform_impl/linux/device.rs delete mode 100644 vendor/tao/src/platform_impl/linux/event_loop.rs delete mode 100644 vendor/tao/src/platform_impl/linux/icon.rs delete mode 100644 vendor/tao/src/platform_impl/linux/keyboard.rs delete mode 100644 vendor/tao/src/platform_impl/linux/keycode.rs delete mode 100644 vendor/tao/src/platform_impl/linux/mod.rs delete mode 100644 vendor/tao/src/platform_impl/linux/monitor.rs delete mode 100644 vendor/tao/src/platform_impl/linux/taskbar.rs delete mode 100644 vendor/tao/src/platform_impl/linux/util.rs delete mode 100644 vendor/tao/src/platform_impl/linux/wayland/header.rs delete mode 100644 vendor/tao/src/platform_impl/linux/wayland/mod.rs delete mode 100644 vendor/tao/src/platform_impl/linux/window.rs delete mode 100644 vendor/tao/src/platform_impl/linux/x11/ffi.rs delete mode 100644 vendor/tao/src/platform_impl/linux/x11/mod.rs delete mode 100644 vendor/tao/src/platform_impl/linux/x11/xdisplay.rs delete mode 100644 vendor/tao/src/platform_impl/macos/app.rs delete mode 100644 vendor/tao/src/platform_impl/macos/app_delegate.rs delete mode 100644 vendor/tao/src/platform_impl/macos/app_state.rs delete mode 100644 vendor/tao/src/platform_impl/macos/badge.rs delete mode 100644 vendor/tao/src/platform_impl/macos/dock.rs delete mode 100644 vendor/tao/src/platform_impl/macos/event.rs delete mode 100644 vendor/tao/src/platform_impl/macos/event_loop.rs delete mode 100644 vendor/tao/src/platform_impl/macos/ffi.rs delete mode 100644 vendor/tao/src/platform_impl/macos/icon.rs delete mode 100644 vendor/tao/src/platform_impl/macos/keycode.rs delete mode 100644 vendor/tao/src/platform_impl/macos/mod.rs delete mode 100644 vendor/tao/src/platform_impl/macos/monitor.rs delete mode 100644 vendor/tao/src/platform_impl/macos/observer.rs delete mode 100644 vendor/tao/src/platform_impl/macos/progress_bar.rs delete mode 100644 vendor/tao/src/platform_impl/macos/util/async.rs delete mode 100644 vendor/tao/src/platform_impl/macos/util/cursor.rs delete mode 100644 vendor/tao/src/platform_impl/macos/util/mod.rs delete mode 100644 vendor/tao/src/platform_impl/macos/view.rs delete mode 100644 vendor/tao/src/platform_impl/macos/window.rs delete mode 100644 vendor/tao/src/platform_impl/macos/window_delegate.rs delete mode 100644 vendor/tao/src/platform_impl/mod.rs delete mode 100644 vendor/tao/src/platform_impl/windows/dark_mode.rs delete mode 100644 vendor/tao/src/platform_impl/windows/dpi.rs delete mode 100644 vendor/tao/src/platform_impl/windows/drop_handler.rs delete mode 100644 vendor/tao/src/platform_impl/windows/event.rs delete mode 100644 vendor/tao/src/platform_impl/windows/event_loop.rs delete mode 100644 vendor/tao/src/platform_impl/windows/event_loop/runner.rs delete mode 100644 vendor/tao/src/platform_impl/windows/icon.rs delete mode 100644 vendor/tao/src/platform_impl/windows/keyboard.rs delete mode 100644 vendor/tao/src/platform_impl/windows/keyboard_layout.rs delete mode 100644 vendor/tao/src/platform_impl/windows/keycode.rs delete mode 100644 vendor/tao/src/platform_impl/windows/minimal_ime.rs delete mode 100644 vendor/tao/src/platform_impl/windows/mod.rs delete mode 100644 vendor/tao/src/platform_impl/windows/monitor.rs delete mode 100644 vendor/tao/src/platform_impl/windows/raw_input.rs delete mode 100644 vendor/tao/src/platform_impl/windows/util.rs delete mode 100644 vendor/tao/src/platform_impl/windows/window.rs delete mode 100644 vendor/tao/src/platform_impl/windows/window_state.rs delete mode 100644 vendor/tao/src/window.rs delete mode 100644 vendor/tauri-nspanel/CAP-CHANGES.md delete mode 100644 vendor/tauri-nspanel/Cargo.toml delete mode 100644 vendor/tauri-nspanel/LICENSE_APACHE-2.0 delete mode 100644 vendor/tauri-nspanel/LICENSE_MIT delete mode 100644 vendor/tauri-nspanel/README.md delete mode 100644 vendor/tauri-nspanel/src/lib.rs delete mode 100644 vendor/tauri-nspanel/src/macros.rs delete mode 100644 vendor/tauri-nspanel/src/raw_nspanel.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index 03c9ecf86a0..dd302cb0e26 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -18,8 +18,8 @@ resolver = "2" # https://doc.rust-lang.org/rustc/platform-support.html # Every target desktop-release.yml ships. Leaving a target out here leaves its # platform-specific deps (windows-sys, libc, ...) un-unified, so the sidecar -# `cargo build -p cap` and Tauri's `cargo build -p cap-desktop` resolve -# different feature sets and recompile ~500 shared crates in the same job. +# `cargo build -p cap` and the desktop app's build of the same crates resolve +# different feature sets and recompile hundreds of shared crates in one job. platforms = [ "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", @@ -37,11 +37,11 @@ platforms = [ [traversal-excludes] workspace-members = ["cap-muxer", "cap-muxer-protocol", "cap-verify-expect"] -# hakari unifies dev-dependencies as well, so left alone it pins tauri with its -# `test` feature and tokio `test-util` into every release binary and pulls the -# Tauri/WebKit stack into the standalone GPUI workspace. Only crates whose -# feature drift actually cascades through the sidecar and desktop builds stay -# in the hack; everything else keeps its natural per-binary resolution. +# hakari unifies dev-dependencies as well, so left alone it pins tokio +# `test-util` into every release binary and drags dev-only stacks into the +# standalone GPUI workspace. Only crates whose feature drift actually cascades +# through the sidecar and desktop builds stay in the hack; everything else +# keeps its natural per-binary resolution. [final-excludes] third-party = [ { name = "softbuffer" }, @@ -79,8 +79,6 @@ third-party = [ { name = "smallvec" }, { name = "swift-rs" }, { name = "syn" }, - { name = "tauri" }, - { name = "tauri-build" }, { name = "tokio" }, { name = "tokio-util" }, { name = "toml" }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4527fba5879..a8aae26f56d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,7 @@ jobs: runs-on: blacksmith-8vcpu-ubuntu-2404 outputs: rust: ${{ steps.filter.outputs.rust }} - gpui: ${{ steps.filter.outputs.rust == 'true' || steps.filter.outputs.gpui == 'true' || steps.desktop-resources.outputs.desktop-resources == 'true' }} - tauri-plugins: ${{ steps.filter.outputs.tauri-plugins }} + gpui: ${{ steps.filter.outputs.rust == 'true' || steps.filter.outputs.gpui == 'true' }} gpui-patches: ${{ steps.filter.outputs.gpui-patches }} steps: - name: Checkout @@ -49,10 +48,7 @@ jobs: gpui: - '.gitattributes' - 'apps/desktop-gpui/**' - - 'apps/desktop/src/assets/**' - - 'apps/desktop/src-tauri/src/recording_start_sound.rs' - 'apps/media-server/src/__tests__/fixtures/**' - - 'packages/ui-solid/icons/**' - 'scripts/build-desktop-binaries*' - 'scripts/build-gpui-binary.sh' - 'scripts/build-linux-packages.mjs' @@ -62,33 +58,11 @@ jobs: - 'scripts/finalize-macos-*' - 'scripts/linux-bundle-config*' - 'scripts/prepare-gpui-dependency*' - - 'scripts/run-gpui-build*' - 'scripts/sync-desktop-versions.mjs' - - 'scripts/verify-gpui-release-inputs.mjs' - 'scripts/verify-windows-stack.mjs' gpui-patches: - 'apps/desktop-gpui/patches/**' - 'scripts/prepare-gpui-dependency.sh' - tauri-plugins: - - 'Cargo.lock' - - 'bun.lock' - - 'scripts/check-tauri-plugin-versions*' - - 'apps/desktop/package.json' - - # Cap ships as the GPUI app, which embeds icons, sounds, assets and test - # data from apps/desktop. Only the Tauri app's own sources are skipped. - - name: Check for GPUI resources in apps/desktop - uses: dorny/paths-filter@v3 - id: desktop-resources - with: - predicate-quantifier: every - filters: | - desktop-resources: - - 'apps/desktop/**' - - '!apps/desktop/src/**' - - '!apps/desktop/src-tauri/src/**' - - '!apps/desktop/src-tauri/tests/**' - - '!apps/desktop/src-tauri/gen/**' typecheck: name: Typecheck @@ -106,8 +80,7 @@ jobs: - name: Test workspace tooling run: | - node --test scripts/bun-scripts.test.mjs scripts/check-tauri-plugin-versions.test.mjs - bun run --cwd apps/desktop vitest run scripts/prepare.test.js + node --test scripts/bun-scripts.test.mjs - name: Test Loops profile sync run: | @@ -272,10 +245,8 @@ jobs: shell: bash run: cargo test --locked -p cap-utils -p cap-cli-install --lib - # cap-desktop is the Tauri app, which no longer ships; Cap is the GPUI app - # plus the cap-muxer and cap-cli sidecars built from this workspace. - name: Run Clippy - run: cargo clippy --workspace --exclude cap-desktop --all-features --locked -- -D warnings + run: cargo clippy --workspace --all-features --locked -- -D warnings lint-biome: name: Lint (Biome) @@ -500,18 +471,3 @@ jobs: else cargo +1.95.0 test -p gpui_windows --lib directx_atlas fi - - tauri-plugins: - name: Verify Tauri plugin versions - runs-on: blacksmith-8vcpu-ubuntu-2404 - needs: changes - if: needs.changes.outputs.tauri-plugins == 'true' - steps: - - name: Checkout - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-js - - - name: Run verify - shell: bash - run: node scripts/check-tauri-plugin-versions.js diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index f4c6d54a14a..7b93d117f9e 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -116,7 +116,7 @@ jobs: 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.`); + throw new Error(`Release '${tag}' is already published: ${releaseUrl}. Bump apps/desktop-gpui/Cargo.toml before publishing a new build.`); else core.notice(`Draft release '${tag}' already exists. Continuing build and upload.`); } @@ -287,20 +287,25 @@ jobs: shell: bash env: BUILD_VERSION: ${{ inputs.version }} + # promote-nightly can rebuild a commit from before the Tauri app was + # removed, where apps/desktop/src-tauri/Cargo.toml was the version + # source, so that manifest is stamped too when it exists. 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); + const manifests = ["apps/desktop-gpui/Cargo.toml", "apps/desktop/src-tauri/Cargo.toml"]; + for (const path of manifests.filter((path, index) => index === 0 || fs.existsSync(path))) { + 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); } - toml = toml.replace(re, "version = \"" + version + "\""); - fs.writeFileSync(path, toml); - console.log("Stamped " + path + " with " + version); ' node scripts/sync-desktop-versions.mjs @@ -359,7 +364,7 @@ jobs: free -h # The same link used to exhaust commit on the 16GB Windows runner - # ("rustc-LLVM ERROR: out of memory" while linking cap-desktop), so + # ("rustc-LLVM ERROR: out of memory" while linking the desktop app), so # raise the pagefile on the roomy D: disk, the Windows analog of the # Linux swapfile above. Kept for headroom after the move to thin LTO. - name: Increase pagefile size (Windows) @@ -890,8 +895,12 @@ jobs: - name: Re-sign Windows installer for the updater if: ${{ runner.os == 'Windows' }} shell: bash - working-directory: apps/desktop run: | + # Commits from before the Tauri app was removed (promote-nightly) + # keep the Tauri CLI in apps/desktop. + tauri_dir=apps/desktop-gpui + [[ -f "$tauri_dir/package.json" ]] || tauri_dir=apps/desktop + cd "$tauri_dir" BUNDLE_DIR="../../$CAP_BUNDLE/nsis" for exe in "$BUNDLE_DIR"/*.exe; do echo "Re-signing $(basename "$exe") for the updater..." diff --git a/.github/workflows/promote-nightly.yml b/.github/workflows/promote-nightly.yml index 99e1674eecb..3b056b06032 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-gpui/Cargo.toml 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..f4c89ba9ce2 100644 --- a/.github/workflows/publish-nightly.yml +++ b/.github/workflows/publish-nightly.yml @@ -58,7 +58,7 @@ jobs: uses: SebRollen/toml-action@v1.0.2 id: read_version with: - file: apps/desktop/src-tauri/Cargo.toml + file: apps/desktop-gpui/Cargo.toml field: "package.version" - name: Compute nightly version diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 77054493473..90c4c4d5b97 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,7 @@ jobs: uses: SebRollen/toml-action@v1.0.2 id: read_version with: - file: apps/desktop/src-tauri/Cargo.toml + file: apps/desktop-gpui/Cargo.toml field: "package.version" release: diff --git a/Cargo.lock b/Cargo.lock index 827ec1693a9..9b5243fb43e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,30 +403,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 +452,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", ] @@ -601,28 +573,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "async-task" version = "4.7.1" @@ -640,29 +590,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" @@ -705,7 +632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.4.5", + "axum-core", "base64 0.22.1", "bytes", "futures-util", @@ -715,7 +642,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit 0.7.3", + "matchit", "memchr", "mime", "percent-encoding", @@ -728,44 +655,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.24.0", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core 0.5.6", - "axum-macros", - "base64 0.22.1", - "bytes", - "form_urlencoded", - "futures-util", - "http 1.3.1", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit 0.8.4", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper", - "tokio", - "tokio-tungstenite 0.29.0", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -793,36 +683,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -838,12 +698,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -911,29 +765,6 @@ dependencies = [ "which", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.106", - "which", -] - [[package]] name = "bindgen" version = "0.70.1" @@ -1184,9 +1015,6 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -dependencies = [ - "serde", -] [[package]] name = "bytestring" @@ -1197,31 +1025,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" @@ -1277,7 +1080,7 @@ dependencies = [ "cidre", "clap", "clap_complete", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "dirs 6.0.0", "ffmpeg-next", "flume", @@ -1327,7 +1130,7 @@ dependencies = [ "cap-enc-ffmpeg", "cap-rnnoise", "cidre", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "libc", "serde", @@ -1474,7 +1277,7 @@ dependencies = [ name = "cap-cpal-ffmpeg" version = "0.1.0" dependencies = [ - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "workspace-hack", ] @@ -1513,141 +1316,11 @@ dependencies = [ "workspace-hack", ] -[[package]] -name = "cap-desktop" -version = "0.6.2" -dependencies = [ - "aho-corasick", - "anyhow", - "ashpd", - "async-stream", - "axum 0.8.9", - "base64 0.22.1", - "block2 0.6.1", - "bytemuck", - "bytes", - "cap-audio", - "cap-automation", - "cap-camera", - "cap-camera-effects", - "cap-camera-ffmpeg", - "cap-cli-install", - "cap-editor", - "cap-enc-ffmpeg", - "cap-export", - "cap-fail", - "cap-flags", - "cap-media", - "cap-media-info", - "cap-project", - "cap-recording", - "cap-rendering", - "cap-timestamp", - "cap-utils", - "chrono", - "cidre", - "clipboard-rs", - "cocoa", - "core-foundation 0.10.1", - "core-graphics 0.24.0", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", - "device_query", - "dirs 6.0.0", - "dotenvy_macro", - "ffmpeg-next", - "flume", - "futures", - "futures-intrusive", - "gdkwayland-sys", - "global-hotkey", - "gtk", - "image 0.25.8", - "kameo", - "keyed_priority_queue", - "lazy_static", - "libappindicator", - "log", - "lz4_flex", - "md5", - "nix 0.29.0", - "notify-rust", - "objc", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "parakeet-rs", - "png 0.17.16", - "pollster", - "rand 0.8.5", - "regex", - "relative-path", - "reqwest", - "rodio", - "sanitize-filename", - "scap-direct3d", - "scap-screencapturekit", - "scap-targets", - "semver", - "sentry", - "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", - "tokio-stream", - "tokio-tungstenite 0.24.0", - "tokio-util", - "tracing", - "tracing-appender", - "tracing-futures", - "tracing-opentelemetry", - "tracing-subscriber", - "uuid", - "wayland-sys", - "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-editor" version = "0.1.0" dependencies = [ - "axum 0.7.9", + "axum", "cap-audio", "cap-enc-ffmpeg", "cap-media", @@ -1655,7 +1328,7 @@ dependencies = [ "cap-project", "cap-rendering", "cap-utils", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "flume", "futures", @@ -1834,7 +1507,7 @@ dependencies = [ name = "cap-media-info" version = "0.1.0" dependencies = [ - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "thiserror 1.0.69", "workspace-hack", @@ -1941,7 +1614,7 @@ dependencies = [ "core-foundation 0.10.1", "core-graphics 0.24.0", "coreaudio-rs", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "device_query", "dhat", "either", @@ -2104,7 +1777,7 @@ dependencies = [ "cidre", "clap", "colored", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "flume", "indicatif", @@ -2131,7 +1804,7 @@ name = "cap-timestamp" version = "0.1.0" dependencies = [ "cidre", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "windows 0.60.0", "workspace-hack", ] @@ -2145,7 +1818,7 @@ dependencies = [ "flume", "futures", "libc", - "nix 0.29.0", + "nix", "serde", "serde_json", "tempfile", @@ -2178,7 +1851,7 @@ dependencies = [ "cap-timestamp", "cap-verify-expect", "cidre", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "flume", "futures", @@ -2210,7 +1883,7 @@ dependencies = [ "anyhow", "cap-verify", "cidre", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "futures", "objc2-foundation 0.3.1", "scap-screencapturekit", @@ -2264,25 +1937,6 @@ dependencies = [ "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cbc" version = "0.1.2" @@ -2356,12 +2010,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" @@ -2464,7 +2112,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", @@ -2477,48 +2125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - -[[package]] -name = "clipboard-rs" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afede46921767868c5c7f8f55202bdd8bec0bab6bc9605174200f45924f93c62" -dependencies = [ - "clipboard-win", - "image 0.25.8", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", - "windows 0.59.0", - "x11rb", -] - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", - "windows-win", -] - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "cocoa" -version = "0.26.1" +name = "cocoa" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ @@ -2587,21 +2195,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "serde", - "static_assertions", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2624,26 +2217,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 = "constant_time_eq" version = "0.4.2" @@ -2875,29 +2448,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpal" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs", - "dasp_sample", - "jni 0.21.1", - "js-sys", - "libc", - "mach2", - "ndk 0.8.0", - "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.54.0", -] - [[package]] name = "cpal" version = "0.15.3" @@ -2938,30 +2488,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" @@ -3167,15 +2693,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "dary_heap" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" -dependencies = [ - "serde", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -3224,48 +2741,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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.106", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -3445,38 +2920,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" @@ -3486,24 +2929,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "dotenvy_macro" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" -dependencies = [ - "dotenvy", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "downcast-rs" version = "1.2.1" @@ -3603,26 +3028,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" @@ -3712,18 +3117,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - -[[package]] -name = "esaxx-rs" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" - [[package]] name = "etagere" version = "0.2.15" @@ -3779,16 +3172,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -3857,16 +3240,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" @@ -3897,12 +3270,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" @@ -4047,12 +3414,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42da99970737c0150e3c5cd1cdc510735a2511739f5c3aa3c6bfc9f31441488d" -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futf" version = "0.1.5" @@ -4195,170 +3556,71 @@ dependencies = [ ] [[package]] -name = "gdk" -version = "0.18.2" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", + "typenum", + "version_check", ] [[package]] -name = "gdk-pixbuf" -version = "0.18.5" +name = "gethostname" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", + "rustix 1.1.2", + "windows-targets 0.52.6", ] [[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" +name = "getrandom" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "cfg-if", "libc", - "system-deps 6.2.2", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] -name = "gdk-sys" -version = "0.18.2" +name = "getrandom" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "cfg-if", + "js-sys", "libc", - "pango-sys", - "pkg-config", - "system-deps 6.2.2", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] -name = "gdkwayland-sys" -version = "0.18.2" +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", + "cfg-if", + "js-sys", "libc", - "pkg-config", - "system-deps 6.2.2", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", ] [[package]] -name = "gdkx11" -version = "0.18.2" +name = "gif" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" -dependencies = [ - "rustix 1.1.2", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gif" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" -dependencies = [ - "color_quant", - "weezl", + "color_quant", + "weezl", ] [[package]] @@ -4406,38 +3668,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" @@ -4449,77 +3679,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" @@ -4554,28 +3719,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" @@ -4627,58 +3770,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" @@ -4721,12 +3812,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" @@ -4738,12 +3823,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" @@ -4812,12 +3891,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "html5ever" version = "0.29.1" @@ -4875,12 +3948,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" @@ -4999,16 +4066,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" @@ -5223,12 +4280,6 @@ dependencies = [ "quote", ] -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - [[package]] name = "indexmap" version = "1.9.3" @@ -5403,44 +4454,12 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.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" @@ -5585,33 +4604,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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" -dependencies = [ - "indexmap 2.11.4", -] - [[package]] name = "keyring" version = "4.1.5" @@ -5713,43 +4712,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "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" +name = "libc" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" @@ -5763,16 +4727,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" @@ -5956,15 +4910,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lz4_flex" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" -dependencies = [ - "twox-hash", -] - [[package]] name = "lzma-rust2" version = "0.15.7" @@ -5977,18 +4922,6 @@ 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" @@ -6008,22 +4941,6 @@ dependencies = [ "core-foundation-sys", ] -[[package]] -name = "macro_rules_attribute" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" -dependencies = [ - "macro_rules_attribute-proc_macro", - "paste", -] - -[[package]] -name = "macro_rules_attribute-proc_macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" - [[package]] name = "malloc_buf" version = "0.0.6" @@ -6079,12 +4996,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "matrixmultiply" version = "0.3.10" @@ -6105,12 +5016,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "memchr" version = "2.7.5" @@ -6181,87 +5086,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" @@ -6302,28 +5132,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "monostate" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" -dependencies = [ - "monostate-impl", - "serde", - "serde_core", -] - -[[package]] -name = "monostate-impl" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "moxcms" version = "0.7.5" @@ -6348,27 +5156,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" @@ -6378,7 +5165,7 @@ dependencies = [ "arrayvec", "bit-set", "bitflags 2.13.1", - "cfg_aliases 0.2.1", + "cfg_aliases", "codespan-reporting", "half", "hashbrown 0.15.5", @@ -6524,18 +5311,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" @@ -6544,7 +5319,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.13.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] @@ -6579,20 +5354,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" @@ -6728,7 +5489,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", @@ -6750,17 +5511,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" @@ -7080,16 +5830,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" @@ -7114,18 +5854,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" @@ -7150,17 +5878,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" @@ -7192,18 +5909,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" @@ -7228,22 +5933,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-web-kit" -version = "0.3.1" -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" @@ -7253,15 +5942,6 @@ 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" @@ -7294,15 +5974,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -7315,28 +5986,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" -[[package]] -name = "onig" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" -dependencies = [ - "bitflags 2.13.1", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "open" version = "5.4.0" @@ -7393,82 +6042,8 @@ dependencies = [ ] [[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.16", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http 1.3.1", - "opentelemetry", - "reqwest", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" -dependencies = [ - "http 1.3.1", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest", - "thiserror 2.0.16", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", - "tonic-prost", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.2", - "thiserror 2.0.16", - "tokio", - "tokio-stream", -] - -[[package]] -name = "option-ext" -version = "0.2.0" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" @@ -7506,16 +6081,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" @@ -7563,30 +6128,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" @@ -7596,47 +6137,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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2c29bb70e3b63ddfa9af7cbb66f87a200550a5f6a5ac82fabf527b270c6615" -dependencies = [ - "eyre", - "hound", - "ndarray 0.17.2", - "ort", - "realfft", - "serde", - "serde_json", - "tokenizers", -] - [[package]] name = "parking" version = "2.2.1" @@ -7710,16 +6210,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" @@ -7934,12 +6424,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" @@ -8079,26 +6563,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" @@ -8108,30 +6572,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" @@ -8147,16 +6587,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" @@ -8176,29 +6606,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "prost" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "psl-types" version = "2.0.11" @@ -8264,7 +6671,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", @@ -8304,7 +6711,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", @@ -8443,15 +6850,6 @@ 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" @@ -8530,17 +6928,6 @@ dependencies = [ "rayon-core", ] -[[package]] -name = "rayon-cond" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" -dependencies = [ - "either", - "itertools 0.14.0", - "rayon", -] - [[package]] name = "rayon-core" version = "1.13.0" @@ -8592,15 +6979,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be105c72a1e6a5a1198acee3d5b506a15676b74a02ecd78060042a447f408d94" -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -8803,31 +7181,6 @@ dependencies = [ "rand 0.9.2", ] -[[package]] -name = "rfd" -version = "0.15.4" -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" @@ -8897,20 +7250,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "rodio" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" -dependencies = [ - "claxon", - "cpal 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", - "hound", - "lewton", - "symphonia", - "thiserror 1.0.69", -] - [[package]] name = "roxmltree" version = "0.20.0" @@ -8938,16 +7277,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" @@ -9115,20 +7444,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "sanitize-filename" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" -dependencies = [ - "regex", -] - [[package]] name = "scap-cpal" version = "0.1.0" dependencies = [ - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "thiserror 1.0.69", "tracing", "workspace-hack", @@ -9153,7 +7473,7 @@ name = "scap-ffmpeg" version = "0.1.0" dependencies = [ "cidre", - "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", + "cpal", "ffmpeg-next", "futures", "scap-cpal", @@ -9307,26 +7627,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" @@ -9541,17 +7841,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" @@ -9732,28 +8021,6 @@ dependencies = [ "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" @@ -9801,34 +8068,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" @@ -9920,7 +8165,7 @@ dependencies = [ "bindgen 0.71.1", "cc", "flate2", - "heck 0.5.0", + "heck", "lazy_static", "regex", "serde_json", @@ -9970,17 +8215,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" @@ -10054,7 +8288,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "as-raw-xcb-connection", "bytemuck", - "cfg_aliases 0.2.1", + "cfg_aliases", "core-graphics 0.24.0", "drm", "fastrand", @@ -10078,32 +8312,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" @@ -10130,27 +8338,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" @@ -10169,18 +8356,6 @@ dependencies = [ "bitflags 2.13.1", ] -[[package]] -name = "spm_precompiled" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" -dependencies = [ - "base64 0.13.1", - "nom 7.1.3", - "serde", - "unicode-segmentation", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -10263,7 +8438,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", @@ -10276,7 +8451,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", @@ -10326,55 +8501,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "symphonia" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" -dependencies = [ - "lazy_static", - "symphonia-bundle-mp3", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-core" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - [[package]] name = "syn" version = "1.0.109" @@ -10482,7 +8608,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", @@ -10495,61 +8621,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" @@ -10574,663 +8651,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] -name = "tauri" -version = "2.8.5" +name = "tauri-utils" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" dependencies = [ "anyhow", - "bytes", - "cookie", - "dirs 6.0.0", + "brotli", + "cargo_metadata", + "ctor 0.2.9", "dunce", - "embed_plist", - "getrandom 0.3.3", "glob", - "gtk", - "heck 0.5.0", + "html5ever", "http 1.3.1", - "http-range", - "image 0.25.8", - "jni 0.21.1", - "libc", + "infer", + "json-patch", + "kuchikiki", "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", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", "serde", + "serde-untagged", "serde_json", - "serde_repr", - "serialize-to-javascript", - "specta", + "serde_with", "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", "thiserror 2.0.16", - "tokio", - "tray-icon", + "toml 0.9.7", "url", "urlpattern", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows 0.61.3", + "uuid", + "walkdir", ] [[package]] -name = "tauri-build" -version = "2.4.1" +name = "tempfile" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 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", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] -name = "tauri-codegen" -version = "2.4.0" +name = "tendril" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 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", + "futf", + "mac", + "utf-8", ] [[package]] -name = "tauri-macros" -version = "2.4.0" +name = "termcolor" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", - "tauri-codegen", - "tauri-utils", + "winapi-util", ] [[package]] -name = "tauri-nspanel" -version = "2.0.1" -dependencies = [ - "bitflags 2.13.1", - "block", - "cocoa", - "core-foundation 0.10.1", - "core-graphics 0.25.0", - "objc", - "objc-foundation", - "objc_exception", - "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", -] - -[[package]] -name = "tauri-winrt-notification" -version = "0.7.2" -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", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] @@ -11336,15 +8831,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" @@ -11409,39 +8895,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokenizers" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" -dependencies = [ - "ahash", - "aho-corasick", - "compact_str", - "dary_heap", - "derive_builder", - "esaxx-rs", - "getrandom 0.3.3", - "itertools 0.14.0", - "log", - "macro_rules_attribute", - "monostate", - "onig", - "paste", - "rand 0.9.2", - "rayon", - "rayon-cond", - "regex", - "regex-syntax", - "serde", - "serde_json", - "spm_precompiled", - "thiserror 2.0.16", - "unicode-normalization-alignments", - "unicode-segmentation", - "unicode_categories", -] - [[package]] name = "tokio" version = "1.47.1" @@ -11494,18 +8947,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - [[package]] name = "tokio-tungstenite" version = "0.24.0" @@ -11515,19 +8956,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.24.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.29.0", + "tungstenite", ] [[package]] @@ -11613,17 +9042,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" @@ -11665,38 +9083,6 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" -[[package]] -name = "tonic" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http 1.3.1", - "http-body", - "http-body-util", - "percent-encoding", - "pin-project", - "sync_wrapper", - "tokio-stream", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" -dependencies = [ - "bytes", - "prost", - "tonic", -] - [[package]] name = "tower" version = "0.5.2" @@ -11785,19 +9171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "futures", - "futures-task", - "pin-project", - "tracing", + "valuable", ] [[package]] @@ -11811,25 +9185,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-opentelemetry" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" -dependencies = [ - "js-sys", - "opentelemetry", - "opentelemetry_sdk", - "rustversion", - "smallvec", - "thiserror 2.0.16", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - [[package]] name = "tracing-subscriber" version = "0.3.20" @@ -11858,40 +9213,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" @@ -11937,28 +9258,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "sha1", - "thiserror 2.0.16", -] - -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - [[package]] name = "typeid" version = "1.0.3" @@ -11971,15 +9270,6 @@ 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" @@ -12089,15 +9379,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization-alignments" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" -dependencies = [ - "smallvec", -] - [[package]] name = "unicode-properties" version = "0.1.3" @@ -12140,12 +9421,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.9.0" @@ -12332,26 +9607,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" @@ -12615,50 +9870,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" @@ -12677,42 +9888,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" @@ -12727,7 +9902,7 @@ checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", "bitflags 2.13.1", - "cfg_aliases 0.2.1", + "cfg_aliases", "document-features", "hashbrown 0.15.5", "js-sys", @@ -12757,7 +9932,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", @@ -12817,7 +9992,7 @@ dependencies = [ "block", "bytemuck", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "core-graphics-types 0.1.3", "glow", "glutin_wgl_sys", @@ -12876,27 +10051,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "whisper-rs" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7a191078e189d96d029244ab1dff159775adec71dc89a222e9bb9d21a7d161" -dependencies = [ - "whisper-rs-sys", -] - -[[package]] -name = "whisper-rs-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834f4ca6472b02748c6c282a60cea538f962428f79e685c3205a08efd711336" -dependencies = [ - "bindgen 0.69.5", - "cfg-if", - "cmake", - "fs_extra", -] - [[package]] name = "wild" version = "2.2.1" @@ -12937,21 +10091,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" @@ -12981,16 +10120,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" -dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.3", -] - [[package]] name = "windows" version = "0.60.0" @@ -13058,19 +10187,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.3", - "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.3", -] - [[package]] name = "windows-core" version = "0.60.1" @@ -13410,24 +10526,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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e23e33622b3b52f948049acbec9bcc34bf6e26d74176b88941f213c75cf2dc" -dependencies = [ - "error-code", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -13621,7 +10719,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", @@ -13634,7 +10732,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", @@ -13687,41 +10785,12 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - [[package]] name = "wit-bindgen" version = "0.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" @@ -13826,51 +10895,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" @@ -14065,7 +11089,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", @@ -14171,18 +11195,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" @@ -14234,7 +11246,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 fa189ab7752..a43675c14f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ resolver = "2" members = [ "apps/cli", - "apps/desktop/src-tauri", "crates/*", "crates/workspace-hack", ] @@ -24,7 +23,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", @@ -163,15 +161,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" } - -# tauri-nspanel v2 @ 18ffb9a with ownership fixes: `to_panel()` no longer steals -# tao's NSWindow retain (every repeat call was an over-release), converting the -# same window twice is a no-op, the store forgets destroyed windows, and the -# broken `dealloc` override (NSObject dealloc + jump through a garbage pointer) -# is gone. See vendor/tauri-nspanel/CAP-CHANGES.md. -[patch."https://github.com/ahkohd/tauri-nspanel"] -tauri-nspanel = { path = "vendor/tauri-nspanel" } diff --git a/apps/desktop-gpui/package.json b/apps/desktop-gpui/package.json new file mode 100644 index 00000000000..6d653fe24df --- /dev/null +++ b/apps/desktop-gpui/package.json @@ -0,0 +1,12 @@ +{ + "name": "@cap/desktop", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --cwd ../.. cap-setup && dotenv -e ../../.env -- node ../../scripts/dev-desktop.mjs", + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "2.8.4" + } +} diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore deleted file mode 100644 index 76add878f8d..00000000000 --- a/apps/desktop/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist \ No newline at end of file diff --git a/apps/desktop/.vscode/extensions.json b/apps/desktop/.vscode/extensions.json deleted file mode 100644 index 9f54332817c..00000000000 --- a/apps/desktop/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] -} diff --git a/apps/desktop/app.config.ts b/apps/desktop/app.config.ts deleted file mode 100644 index f7caac6cabc..00000000000 --- a/apps/desktop/app.config.ts +++ /dev/null @@ -1,84 +0,0 @@ -import capUIPlugin from "@cap/ui-solid/vite"; -import { defineConfig } from "@solidjs/start/config"; -import devtools from "solid-devtools/vite"; -import topLevelAwait from "vite-plugin-top-level-await"; -import wasm from "vite-plugin-wasm"; -import tsconfigPaths from "vite-tsconfig-paths"; - -const enableSolidDevtools = !!process.env.VITE_SOLID_DEVTOOLS; - -// Bundled fonts load from local assets near-instantly, so `block` is safe and -// avoids the fallback-font flash (FOUT) that `swap` causes on every window -// open. Desktop-only: web keeps `swap` for slow networks. -// No `enforce`: must run after vite:css has inlined the virtual module's -// @import statements (a `pre` transform only sees the un-inlined imports). -const fontDisplayBlock = { - name: "cap:font-display-block", - transform(code: string, id: string) { - // unplugin-fonts inlines the @fontsource CSS into its virtual - // "unfonts.css" module, so match that as well as direct imports. - const [file] = id.split("?"); - const isFontCss = - file.endsWith("unfonts.css") || - (file.includes("@fontsource") && file.endsWith(".css")); - if (!isFontCss) return; - return { - code: code.replace(/font-display:\s*swap;/g, "font-display: block;"), - map: null, - }; - }, -}; - -export default defineConfig({ - ssr: false, - 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/**"], - }, - 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_"], - assetsInclude: ["**/*.riv"], - plugins: [ - ...(enableSolidDevtools ? [devtools({ autoname: true })] : []), - fontDisplayBlock, - wasm(), - topLevelAwait(), - capUIPlugin, - tsconfigPaths({ - root: ".", - }), - ], - define: { - "import.meta.vitest": "undefined", - }, - 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/package.json b/apps/desktop/package.json deleted file mode 100644 index 1b7ebd5e966..00000000000 --- a/apps/desktop/package.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "name": "@cap/desktop", - "type": "module", - "scripts": { - "dev": "bun run --cwd ../.. cap-setup && bun run build:sidecar && bun run build:gpui:dev && dotenv -e ../../.env -- bun run preparescript && dotenv -e ../../.env -- node ../../scripts/dev-desktop.mjs", - "build:tauri": "bun run build:sidecar && bun run build:gpui && dotenv -e ../../.env -- bun run preparescript --release && node ../../scripts/verify-gpui-release-inputs.mjs && dotenv -e ../../.env -- bun run tauri build", - "build:sidecar": "node ../../scripts/build-desktop-binaries.mjs", - "build:gpui": "node ../../scripts/run-gpui-build.mjs release", - "build:gpui:dev": "dotenv -e ../../.env -- node ../../scripts/run-gpui-build.mjs debug", - "preparescript": "node scripts/prepare.js", - "localdev": "dotenv -e ../../.env -- vinxi dev --port 3002", - "build": "vinxi build", - "tauri": "tauri", - "test:display-transport": "node --experimental-websocket 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": { - "@aerofoil/rive-solid-canvas": "^2.1.4", - "@cap/database": "workspace:*", - "@cap/ui-solid": "workspace:*", - "@cap/utils": "workspace:*", - "@cap/web-api-contract": "workspace:*", - "@corvu/tooltip": "^0.2.1", - "@kobalte/core": "^0.13.11", - "@openpanel/web": "^1.4.1", - "@radix-ui/colors": "^3.0.0", - "@rive-app/canvas": "^2.32.2", - "@solid-primitives/bounds": "^0.0.122", - "@solid-primitives/broadcast-channel": "^0.1.1", - "@solid-primitives/context": "^0.2.3", - "@solid-primitives/date": "^2.0.23", - "@solid-primitives/deep": "^0.2.9", - "@solid-primitives/event-bus": "^1.1.1", - "@solid-primitives/event-listener": "^2.3.3", - "@solid-primitives/history": "^0.1.5", - "@solid-primitives/memo": "^1.4.2", - "@solid-primitives/mouse": "^2.1.2", - "@solid-primitives/refs": "^1.0.8", - "@solid-primitives/resize-observer": "^2.0.26", - "@solid-primitives/scheduled": "^1.4.3", - "@solid-primitives/storage": "^4.0.0", - "@solid-primitives/timer": "^1.3.9", - "@solid-primitives/websocket": "^1.2.2", - "@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", - "effect": "^3.18.4", - "lz4-wasm": "^0.9.2", - "mp4box": "^0.5.2", - "solid-js": "^1.9.3", - "solid-markdown": "^2.0.13", - "solid-presence": "^0.1.8", - "solid-toast": "^0.5.0", - "solid-transition-group": "^0.2.3", - "unplugin-auto-import": "^0.18.2", - "unplugin-icons": "^0.19.2", - "uuid": "^9.0.1", - "vinxi": "^0.5.6", - "webcodecs": "^0.1.0", - "zod": "^3.25.76" - }, - "devDependencies": { - "@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.8.4", - "@total-typescript/ts-reset": "^0.6.1", - "@types/dom-webcodecs": "^0.1.11", - "@types/uuid": "^9.0.8", - "@webgpu/types": "^0.1.44", - "autoprefixer": "^10.4.21", - "cross-env": "^7.0.3", - "solid-devtools": "^0.34.5", - "tailwind-scrollbar": "^3.1.0", - "typescript": "^5.8.3", - "vite": "^6.3.5", - "vite-plugin-top-level-await": "^1.4.4", - "vite-plugin-wasm": "^3.4.1", - "vite-tsconfig-paths": "^5.0.1", - "vitest": "~2.1.9" - } -} diff --git a/apps/desktop/postcss.config.cjs b/apps/desktop/postcss.config.cjs deleted file mode 100644 index eebbe271c7d..00000000000 --- a/apps/desktop/postcss.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - "@tailwindcss/postcss": {}, - autoprefixer: {}, - }, -}; diff --git a/apps/desktop/scripts/desktop-display-transport-benchmark.js b/apps/desktop/scripts/desktop-display-transport-benchmark.js deleted file mode 100644 index 162d2ef4980..00000000000 --- a/apps/desktop/scripts/desktop-display-transport-benchmark.js +++ /dev/null @@ -1,591 +0,0 @@ -import { spawn } from "node:child_process"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import net from "node:net"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { build } from "vite"; -import { resolveDesktopBenchmarkBrowser } from "./desktop-display-transport-browser.js"; - -const DESKTOP_ROOT = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", -); -const REPO_ROOT = path.resolve(DESKTOP_ROOT, "../.."); -const DEFAULT_RECORDING = - "/tmp/cap-performance-fixtures/reference-recording.cap"; - -function parseArgs(argv) { - const options = { - recordingPath: DEFAULT_RECORDING, - fps: 60, - frames: 300, - resolution: "half", - startupDelayMs: 5000, - allowNonWebGPU: false, - keepTemp: false, - }; - - for (let index = 0; index < argv.length; index++) { - const token = argv[index]; - if (!token.startsWith("--")) continue; - if (token === "--") continue; - if (token === "--allow-non-webgpu") { - options.allowNonWebGPU = true; - continue; - } - if (token === "--keep-temp") { - options.keepTemp = true; - continue; - } - - const rawValue = token.includes("=") - ? token.slice(token.indexOf("=") + 1) - : argv[index + 1]; - if (!token.includes("=")) index++; - - if (token.startsWith("--recording-path")) options.recordingPath = rawValue; - if (token.startsWith("--fps")) options.fps = Number(rawValue); - if (token.startsWith("--frames")) options.frames = Number(rawValue); - if (token.startsWith("--resolution")) options.resolution = rawValue; - if (token.startsWith("--startup-delay-ms")) - options.startupDelayMs = Number(rawValue); - } - - return options; -} - -async function findFreePort() { - const server = net.createServer(); - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", resolve); - }); - const address = server.address(); - await new Promise((resolve) => server.close(resolve)); - return address.port; -} - -async function buildHarness(tempDir) { - const socketPath = path - .resolve(DESKTOP_ROOT, "src/utils/socket.ts") - .replaceAll(path.sep, "/"); - const entryPath = path.join(tempDir, "entry.ts"); - - writeFileSync( - entryPath, - `import { createImageDataWS, getFpsStats } from "${socketPath}"; - -const params = new URLSearchParams(window.location.search); -const wsUrl = params.get("ws"); -if (!wsUrl) throw new Error("Missing ws query param"); - -const canvas = document.getElementById("canvas"); -const status = document.getElementById("status"); -const samples = []; -const animationFrameIntervals = []; -let frameNotifications = 0; -let latestFrame = null; -let lastRequestFrameCount = 0; -let previousAnimationFrameAt = null; -let animationFrameId = null; - -function observeAnimationFrame(timestamp) { - if (previousAnimationFrameAt !== null) { - animationFrameIntervals.push(timestamp - previousAnimationFrameAt); - } - previousAnimationFrameAt = timestamp; - animationFrameId = requestAnimationFrame(observeAnimationFrame); -} - -animationFrameId = requestAnimationFrame(observeAnimationFrame); - -const [ws, isConnected, isWorkerReady, controls] = createImageDataWS( - wsUrl, - (data) => { - frameNotifications++; - latestFrame = { - width: data.width, - height: data.height, - hasBitmap: Boolean(data.bitmap), - }; - }, - () => { - lastRequestFrameCount++; - }, -); - -controls.initDirectCanvas(canvas); - -function summarizeSamples(sampleSet) { - if (sampleSet.length === 0) return null; - const average = (field) => - sampleSet.reduce((sum, sample) => sum + sample.stats[field], 0) / - sampleSet.length; - const maximum = (field) => - Math.max(...sampleSet.map((sample) => sample.stats[field])); - const sum = (field) => - sampleSet.reduce((total, sample) => total + sample.stats[field], 0); - return { - sampleCount: sampleSet.length, - avgFps: average("fps"), - avgRenderFps: average("renderFps"), - avgMbPerSec: average("mbPerSec"), - avgFrameMs: average("avgFrameMs"), - maxFrameMs: maximum("maxFrameMs"), - avgRenderMs: average("avgRenderMs"), - maxRenderMs: maximum("maxRenderMs"), - avgUploadMs: average("avgUploadMs"), - maxUploadMs: maximum("maxUploadMs"), - avgReceiveToDisplayMs: average("avgReceiveToDisplayMs"), - maxReceiveToDisplayMs: maximum("maxReceiveToDisplayMs"), - sharedBufferWrites: sum("sharedBufferWrites"), - sharedBufferFallbacks: sum("sharedBufferFallbacks"), - frameCount: sum("frameCount"), - renderCount: sum("renderCount"), - uploadCount: sum("uploadCount"), - receiveToDisplayCount: sum("receiveToDisplayCount"), - transportModes: [...new Set(sampleSet.map((sample) => sample.stats.transportMode))], - }; -} - -function snapshot() { - const stats = getFpsStats(); - const sample = { - atMs: performance.now(), - connected: isConnected(), - workerReady: isWorkerReady(), - frameNotifications, - lastRequestFrameCount, - latestFrame, - stats, - }; - samples.push(sample); - if (status) status.textContent = JSON.stringify(sample, null, 2); - return sample; -} - -const interval = window.setInterval(snapshot, 250); -window.__capDisplayBenchmarkResult = () => { - const resultSamples = samples.filter((sample) => sample.stats); - const stableSamples = resultSamples.filter( - (sample) => sample.stats.windowMs >= 500 && sample.stats.renderCount >= 15, - ); - const summarizedSamples = - stableSamples.length > 0 ? stableSamples : resultSamples; - const best = resultSamples.reduce( - (current, sample) => - !current || sample.stats.renderFps > current.stats.renderFps - ? sample - : current, - null, - ); - const mostFrames = resultSamples.reduce( - (current, sample) => - !current || sample.frameNotifications > current.frameNotifications - ? sample - : current, - null, - ); - return { - best, - mostFrames, - lastWithStats: resultSamples.at(-1) ?? null, - aggregate: summarizeSamples(summarizedSamples), - stableSampleCount: stableSamples.length, - latest: samples.at(-1) ?? null, - sampleCount: samples.length, - frameNotifications, - lastRequestFrameCount, - readyState: ws.readyState, - animationFrames: { - sampleCount: animationFrameIntervals.length, - averageFps: - animationFrameIntervals.length === 0 - ? 0 - : (animationFrameIntervals.length * 1000) / - animationFrameIntervals.reduce((total, interval) => total + interval, 0), - maximumIntervalMs: Math.max(0, ...animationFrameIntervals), - visibilityState: document.visibilityState, - hasFocus: document.hasFocus(), - }, - }; -}; -window.__capDisplayBenchmarkDispose = () => { - window.clearInterval(interval); - if (animationFrameId !== null) cancelAnimationFrame(animationFrameId); - controls.dispose(); -}; -window.__capDisplayBenchmarkReady = true; -snapshot(); -`, - ); - - await build({ - root: tempDir, - configFile: false, - base: "./", - logLevel: "warn", - build: { - outDir: "dist", - emptyOutDir: true, - target: "esnext", - lib: { - entry: entryPath, - formats: ["es"], - fileName: "entry", - }, - }, - server: { - fs: { - allow: [DESKTOP_ROOT, tempDir], - }, - }, - }); - - writeFileSync( - path.join(tempDir, "dist/index.html"), - `Cap Display Benchmark
`,
-	);
-
-	return path.join(tempDir, "dist/index.html");
-}
-
-function spawnRustBenchmark(options) {
-	const args = [
-		"run",
-		"-p",
-		"cap-desktop",
-		"--example",
-		"desktop-display-transport-benchmark",
-		"--",
-		"--recording-path",
-		options.recordingPath,
-		"--fps",
-		String(options.fps),
-		"--frames",
-		String(options.frames),
-		"--resolution",
-		options.resolution,
-		"--startup-delay-ms",
-		String(options.startupDelayMs),
-	];
-	const child = spawn("cargo", args, {
-		cwd: REPO_ROOT,
-		stdio: ["ignore", "pipe", "pipe"],
-	});
-
-	let buffer = "";
-	let wsUrlResolve;
-	let wsUrlReject;
-	const wsUrlPromise = new Promise((resolve, reject) => {
-		wsUrlResolve = resolve;
-		wsUrlReject = reject;
-	});
-	const outputLines = [];
-
-	function handleOutput(chunk, streamName) {
-		const text = chunk.toString();
-		process[streamName].write(text);
-		buffer += text;
-		let newlineIndex = buffer.indexOf("\n");
-		while (newlineIndex >= 0) {
-			const line = buffer.slice(0, newlineIndex).trim();
-			buffer = buffer.slice(newlineIndex + 1);
-			if (line) outputLines.push(line);
-			const match = line.match(/DISPLAY_WS_URL=(ws:\/\/[^\s]+)/);
-			if (match) wsUrlResolve(match[1]);
-			newlineIndex = buffer.indexOf("\n");
-		}
-	}
-
-	child.stdout.on("data", (chunk) => handleOutput(chunk, "stdout"));
-	child.stderr.on("data", (chunk) => handleOutput(chunk, "stderr"));
-	child.on("error", wsUrlReject);
-
-	const exitPromise = new Promise((resolve, reject) => {
-		child.on("exit", (code, signal) => {
-			if (code === 0) {
-				resolve({ outputLines });
-			} else {
-				reject(new Error(`Rust benchmark exited with ${code ?? signal}`));
-			}
-		});
-	});
-
-	return { child, wsUrlPromise, exitPromise };
-}
-
-function spawnChrome(debuggingPort, tempDir) {
-	const child = spawn(
-		resolveDesktopBenchmarkBrowser(),
-		[
-			`--user-data-dir=${path.join(tempDir, "chrome-profile")}`,
-			`--remote-debugging-port=${debuggingPort}`,
-			"--no-first-run",
-			"--no-default-browser-check",
-			"--disable-background-networking",
-			"--disable-component-update",
-			"--disable-extensions",
-			"--allow-file-access-from-files",
-			"--enable-unsafe-webgpu",
-			...(process.platform === "linux" && process.getuid?.() === 0
-				? ["--no-sandbox"]
-				: []),
-			...(process.env.CAP_DESKTOP_BENCHMARK_HEADLESS === "1"
-				? ["--headless=new"]
-				: []),
-			...(process.env.CAP_DESKTOP_BENCHMARK_SOFTWARE_GPU === "1"
-				? ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"]
-				: []),
-			"about:blank",
-		],
-		{ stdio: ["ignore", "ignore", "pipe"] },
-	);
-	child.stderr.on("data", (chunk) => process.stderr.write(chunk));
-	return child;
-}
-
-async function waitForPage(debuggingPort, timeoutMs = 15000) {
-	const started = Date.now();
-	while (Date.now() - started < timeoutMs) {
-		try {
-			const response = await fetch(
-				`http://127.0.0.1:${debuggingPort}/json/list`,
-			);
-			const targets = await response.json();
-			const page = targets.find(
-				(target) => target.type === "page" && target.webSocketDebuggerUrl,
-			);
-			if (page) return page;
-		} catch {}
-		await new Promise((resolve) => setTimeout(resolve, 100));
-	}
-	throw new Error("Timed out waiting for Chrome page target");
-}
-
-async function connectCdp(wsUrl) {
-	const socket = new WebSocket(wsUrl);
-	const pending = new Map();
-	const eventHandlers = new Map();
-	let nextId = 1;
-
-	await new Promise((resolve, reject) => {
-		socket.addEventListener("open", resolve, { once: true });
-		socket.addEventListener("error", reject, { once: true });
-	});
-
-	socket.addEventListener("message", (event) => {
-		const message = JSON.parse(event.data);
-		if (!message.id) {
-			const handlers = eventHandlers.get(message.method);
-			if (handlers) {
-				for (const handler of handlers) handler(message.params);
-			}
-			return;
-		}
-		const entry = pending.get(message.id);
-		if (!entry) return;
-		pending.delete(message.id);
-		if (message.error) {
-			entry.reject(new Error(JSON.stringify(message.error)));
-		} else {
-			entry.resolve(message.result);
-		}
-	});
-
-	return {
-		send(method, params = {}, timeoutMs = 10000) {
-			const id = nextId++;
-			socket.send(JSON.stringify({ id, method, params }));
-			return new Promise((resolve, reject) => {
-				const timer = setTimeout(() => {
-					pending.delete(id);
-					reject(new Error(`Timed out waiting for ${method}`));
-				}, timeoutMs);
-				pending.set(id, {
-					resolve(value) {
-						clearTimeout(timer);
-						resolve(value);
-					},
-					reject(error) {
-						clearTimeout(timer);
-						reject(error);
-					},
-				});
-			});
-		},
-		on(method, handler) {
-			const handlers = eventHandlers.get(method) ?? new Set();
-			handlers.add(handler);
-			eventHandlers.set(method, handlers);
-		},
-		off(method, handler) {
-			const handlers = eventHandlers.get(method);
-			if (!handlers) return;
-			handlers.delete(handler);
-			if (handlers.size === 0) eventHandlers.delete(method);
-		},
-		close() {
-			socket.close();
-		},
-	};
-}
-
-function waitForCdpEvent(cdp, method, timeoutMs = 15000) {
-	return new Promise((resolve, reject) => {
-		const timer = setTimeout(() => {
-			cdp.off(method, handler);
-			reject(new Error(`Timed out waiting for ${method}`));
-		}, timeoutMs);
-		function handler(params) {
-			clearTimeout(timer);
-			cdp.off(method, handler);
-			resolve(params);
-		}
-		cdp.on(method, handler);
-	});
-}
-
-function sleep(ms) {
-	return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-async function stopProcess(child) {
-	if (!child || child.exitCode !== null || child.signalCode !== null) return;
-	child.kill();
-	await Promise.race([
-		new Promise((resolve) => child.once("exit", resolve)),
-		sleep(3000),
-	]);
-	if (child.exitCode === null && child.signalCode === null) {
-		child.kill("SIGKILL");
-		await Promise.race([
-			new Promise((resolve) => child.once("exit", resolve)),
-			sleep(1000),
-		]);
-	}
-}
-
-async function removeTempDir(tempDir) {
-	let lastError = null;
-	for (let attempt = 0; attempt < 5; attempt++) {
-		try {
-			rmSync(tempDir, { recursive: true, force: true });
-			return;
-		} catch (error) {
-			lastError = error;
-			await sleep(250);
-		}
-	}
-	if (lastError) {
-		console.error(`Failed to remove temp dir ${tempDir}: ${lastError.message}`);
-	}
-}
-
-async function waitForHarnessReady(cdp) {
-	const started = Date.now();
-	while (Date.now() - started < 10000) {
-		const result = await cdp.send("Runtime.evaluate", {
-			expression: "Boolean(window.__capDisplayBenchmarkReady)",
-			returnByValue: true,
-		});
-		if (result.result.value) return;
-		await new Promise((resolve) => setTimeout(resolve, 100));
-	}
-	throw new Error("Timed out waiting for display benchmark harness");
-}
-
-function assertResult(result, options) {
-	const displayStats = result.mostFrames?.stats ?? result.lastWithStats?.stats;
-	if (!displayStats) {
-		throw new Error("No desktop display stats captured");
-	}
-	if (result.frameNotifications <= 0) {
-		throw new Error("No displayed frame notifications captured");
-	}
-	const sawWebGPU =
-		displayStats.transportMode === "webgpu" ||
-		result.aggregate?.transportModes?.includes("webgpu");
-	if (!options.allowNonWebGPU && !sawWebGPU) {
-		throw new Error(
-			`Expected WebGPU transport, saw ${displayStats.transportMode}`,
-		);
-	}
-}
-
-async function main() {
-	const options = parseArgs(process.argv.slice(2));
-	const tempDir = mkdtempSync(path.join(tmpdir(), "cap-display-transport-"));
-	let chrome = null;
-	let rust = null;
-	let cdp = null;
-
-	try {
-		const indexPath = await buildHarness(tempDir);
-		rust = spawnRustBenchmark(options);
-		const wsUrl = await rust.wsUrlPromise;
-		const debuggingPort = await findFreePort();
-		const pageUrl = `${pathToFileURL(indexPath).href}?ws=${encodeURIComponent(wsUrl)}`;
-		chrome = spawnChrome(debuggingPort, tempDir);
-		const page = await waitForPage(debuggingPort);
-		cdp = await connectCdp(page.webSocketDebuggerUrl);
-		const runtimeIssues = [];
-		cdp.on("Runtime.exceptionThrown", (params) => {
-			runtimeIssues.push(params.exceptionDetails?.text ?? "Runtime exception");
-		});
-		cdp.on("Runtime.consoleAPICalled", (params) => {
-			const text = params.args
-				?.map((arg) => arg.value ?? arg.description ?? "")
-				.join(" ");
-			if (text) runtimeIssues.push(`${params.type}: ${text}`);
-		});
-		await cdp.send("Page.enable");
-		await cdp.send("Runtime.enable");
-		await cdp.send("Page.bringToFront");
-		await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true });
-		const loadPromise = waitForCdpEvent(cdp, "Page.loadEventFired").catch(
-			() => null,
-		);
-		await cdp.send("Page.navigate", { url: pageUrl });
-		await loadPromise;
-		try {
-			await waitForHarnessReady(cdp);
-		} catch (error) {
-			const snapshot = await cdp.send("Runtime.evaluate", {
-				expression:
-					"({ href: location.href, body: document.body?.innerText ?? null, ready: Boolean(window.__capDisplayBenchmarkReady) })",
-				returnByValue: true,
-			});
-			throw new Error(
-				`${error.message}; page=${JSON.stringify(snapshot.result.value)}; runtime=${JSON.stringify(runtimeIssues)}`,
-			);
-		}
-		await rust.exitPromise;
-		await new Promise((resolve) => setTimeout(resolve, 1000));
-		const evaluation = await cdp.send("Runtime.evaluate", {
-			expression: "window.__capDisplayBenchmarkResult()",
-			returnByValue: true,
-		});
-		const result = evaluation.result.value;
-		assertResult(result, options);
-		console.log(`DISPLAY_BROWSER_STATS=${JSON.stringify(result)}`);
-		await cdp.send("Runtime.evaluate", {
-			expression: "window.__capDisplayBenchmarkDispose()",
-		});
-	} finally {
-		if (cdp) cdp.close();
-		await stopProcess(chrome);
-		await stopProcess(rust?.child);
-		if (!options.keepTemp) {
-			await removeTempDir(tempDir);
-		} else {
-			console.log(`DISPLAY_BENCHMARK_TEMP=${tempDir}`);
-		}
-	}
-}
-
-main().catch((error) => {
-	console.error(error);
-	process.exit(1);
-});
diff --git a/apps/desktop/scripts/desktop-display-transport-browser.js b/apps/desktop/scripts/desktop-display-transport-browser.js
deleted file mode 100644
index 29f642a61b2..00000000000
--- a/apps/desktop/scripts/desktop-display-transport-browser.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { existsSync } from "node:fs";
-import path from "node:path";
-
-export function resolveDesktopBenchmarkBrowser({
-	platform = process.platform,
-	environment = process.env,
-	fileExists = existsSync,
-} = {}) {
-	const override = environment.CAP_DESKTOP_BENCHMARK_BROWSER?.trim();
-	if (override) {
-		if (fileExists(override)) return override;
-		throw new Error(`Benchmark browser does not exist: ${override}`);
-	}
-
-	const candidates = [];
-	if (platform === "darwin") {
-		candidates.push(
-			"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
-			"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
-			"/Applications/Chromium.app/Contents/MacOS/Chromium",
-		);
-	} else if (platform === "win32") {
-		const roots = [
-			environment["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)",
-			environment.PROGRAMFILES ?? "C:\\Program Files",
-			environment.LOCALAPPDATA,
-		].filter(Boolean);
-		for (const root of roots) {
-			candidates.push(
-				path.win32.join(root, "Google", "Chrome", "Application", "chrome.exe"),
-				path.win32.join(root, "Microsoft", "Edge", "Application", "msedge.exe"),
-			);
-		}
-	} else if (platform === "linux") {
-		candidates.push(
-			"/usr/bin/google-chrome-stable",
-			"/usr/bin/google-chrome",
-			"/usr/bin/chromium",
-			"/usr/bin/chromium-browser",
-			"/usr/bin/microsoft-edge",
-		);
-	}
-
-	const browser = candidates.find(fileExists);
-	if (browser) return browser;
-
-	throw new Error(
-		`No Chromium browser found for ${platform}; set CAP_DESKTOP_BENCHMARK_BROWSER to its executable path`,
-	);
-}
diff --git a/apps/desktop/scripts/desktop-display-transport-browser.test.js b/apps/desktop/scripts/desktop-display-transport-browser.test.js
deleted file mode 100644
index 3304ee6e4f2..00000000000
--- a/apps/desktop/scripts/desktop-display-transport-browser.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { resolveDesktopBenchmarkBrowser } from "./desktop-display-transport-browser.js";
-
-describe("resolveDesktopBenchmarkBrowser", () => {
-	it("preserves the existing macOS Google Chrome selection", () => {
-		const chrome =
-			"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
-		expect(
-			resolveDesktopBenchmarkBrowser({
-				platform: "darwin",
-				environment: {},
-				fileExists: (candidate) => candidate === chrome,
-			}),
-		).toBe(chrome);
-	});
-
-	it("finds Microsoft Edge in Windows Program Files", () => {
-		const edge = "D:\\Programs\\Microsoft\\Edge\\Application\\msedge.exe";
-		expect(
-			resolveDesktopBenchmarkBrowser({
-				platform: "win32",
-				environment: {
-					"PROGRAMFILES(X86)": "D:\\Programs",
-				},
-				fileExists: (candidate) => candidate === edge,
-			}),
-		).toBe(edge);
-	});
-
-	it("finds a Linux Chromium installation", () => {
-		expect(
-			resolveDesktopBenchmarkBrowser({
-				platform: "linux",
-				environment: {},
-				fileExists: (candidate) => candidate === "/usr/bin/chromium",
-			}),
-		).toBe("/usr/bin/chromium");
-	});
-
-	it("uses an explicitly configured browser before platform discovery", () => {
-		const browser = "/opt/browser/chrome";
-		expect(
-			resolveDesktopBenchmarkBrowser({
-				platform: "linux",
-				environment: { CAP_DESKTOP_BENCHMARK_BROWSER: browser },
-				fileExists: (candidate) => candidate === browser,
-			}),
-		).toBe(browser);
-	});
-
-	it("rejects a missing explicitly configured browser", () => {
-		expect(() =>
-			resolveDesktopBenchmarkBrowser({
-				platform: "linux",
-				environment: { CAP_DESKTOP_BENCHMARK_BROWSER: "/missing/browser" },
-				fileExists: () => false,
-			}),
-		).toThrow("Benchmark browser does not exist: /missing/browser");
-	});
-
-	it("explains how to configure unsupported browser installations", () => {
-		expect(() =>
-			resolveDesktopBenchmarkBrowser({
-				platform: "linux",
-				environment: {},
-				fileExists: () => false,
-			}),
-		).toThrow("set CAP_DESKTOP_BENCHMARK_BROWSER");
-	});
-});
diff --git a/apps/desktop/scripts/desktop-memory-soak-lib.js b/apps/desktop/scripts/desktop-memory-soak-lib.js
deleted file mode 100644
index 66efbe64d4b..00000000000
--- a/apps/desktop/scripts/desktop-memory-soak-lib.js
+++ /dev/null
@@ -1,299 +0,0 @@
-import { existsSync } from "node:fs";
-import { fileURLToPath } from "node:url";
-
-const DEV_APP_COMMAND_PREFIX = fileURLToPath(
-	new URL("../../../target/debug/cap-desktop", import.meta.url),
-);
-const INSTALLED_APP_COMMAND_PREFIX = "/Applications/Cap.app/Contents/MacOS/Cap";
-
-export const DEFAULTS = {
-	appCommandPrefix:
-		process.env.CAP_APP_COMMAND_PREFIX ||
-		(existsSync(DEV_APP_COMMAND_PREFIX)
-			? DEV_APP_COMMAND_PREFIX
-			: INSTALLED_APP_COMMAND_PREFIX),
-	baselineSeconds: 30,
-	cycles: 3,
-	initialWebKitWindowSeconds: 180,
-	intervalSeconds: 10,
-	maxCycleRatchetMb: 128,
-	maxTotalGrowthMb: 768,
-	newWebKitWindowSeconds: 180,
-	recordingSeconds: 20,
-	displaySleepSeconds: 20,
-	launchTimeoutSeconds: 30,
-	stopTimeoutSeconds: 10,
-	storePath:
-		process.env.CAP_STORE_PATH ||
-		`${process.env.HOME}/Library/Application Support/so.cap.desktop.dev/store`,
-	settleSeconds: 120,
-	trailingSamples: 3,
-};
-
-const MEDIA_PROCESS_PATTERNS = [
-	["coreaudiod", /\/usr\/sbin\/coreaudiod$/],
-	[
-		"cmio-host",
-		/com\.apple\.cmio\.videodriverkithostextension\.systemextension/,
-	],
-	["cameracaptured", /\/usr\/libexec\/cameracaptured$/],
-	["continuity-agent", /\/usr\/libexec\/ContinuityCaptureAgent(?:\s|$)/],
-	["audioaccessoryd", /\/System\/Library\/CoreServices\/audioaccessoryd$/],
-	["avconferenced", /\/usr\/libexec\/avconferenced$/],
-	["vdcassistant", /\/VDC\.plugin\/Contents\/Resources\/VDCAssistant$/],
-];
-
-function toKb(mb) {
-	return Math.round(mb * 1024);
-}
-
-function parseElapsedSeconds(etime) {
-	const parts = etime.split("-");
-	const dayPart = parts.length === 2 ? Number(parts[0]) : 0;
-	const timePart = parts.length === 2 ? parts[1] : parts[0];
-	const segments = timePart.split(":").map(Number);
-
-	let hours = 0;
-	let minutes = 0;
-	let seconds = 0;
-
-	if (segments.length === 3) {
-		[hours, minutes, seconds] = segments;
-	} else if (segments.length === 2) {
-		[minutes, seconds] = segments;
-	}
-
-	return dayPart * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds;
-}
-
-export function parsePsOutput(output) {
-	return output
-		.split("\n")
-		.map((line) => line.trim())
-		.filter(Boolean)
-		.map((line) => {
-			const match = line.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
-
-			if (!match) return null;
-
-			const [, pid, rssKb, vszKb, etime, command] = match;
-
-			return {
-				command,
-				etimes: parseElapsedSeconds(etime),
-				pid: Number(pid),
-				rssKb: Number(rssKb),
-				vszKb: Number(vszKb),
-			};
-		})
-		.filter(Boolean);
-}
-
-export function classifyProcess(
-	command,
-	appCommandPrefix = DEFAULTS.appCommandPrefix,
-) {
-	if (
-		command.startsWith(
-			"/System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer",
-		)
-	) {
-		return {
-			group: "system",
-			kind: "window-server",
-			label: "windowserver",
-		};
-	}
-
-	if (command === appCommandPrefix) {
-		return { group: "cap", kind: "cap-main", label: "cap-main" };
-	}
-
-	if (command.startsWith(`${appCommandPrefix} --crash-reporter-server=`)) {
-		return { group: "cap", kind: "cap-crash", label: "cap-crash" };
-	}
-
-	if (command.includes("com.apple.WebKit.GPU.xpc")) {
-		return { group: "cap", kind: "webkit-gpu", label: "wk-gpu" };
-	}
-
-	if (command.includes("com.apple.WebKit.Networking.xpc")) {
-		return { group: "cap", kind: "webkit-networking", label: "wk-net" };
-	}
-
-	if (command.includes("com.apple.WebKit.WebContent.xpc")) {
-		return { group: "cap", kind: "webkit-webcontent", label: "wk-webcontent" };
-	}
-
-	for (const [label, pattern] of MEDIA_PROCESS_PATTERNS) {
-		if (pattern.test(command)) {
-			return { group: "media", kind: label, label };
-		}
-	}
-
-	return null;
-}
-
-export function findCapMainProcess(
-	processes,
-	appCommandPrefix = DEFAULTS.appCommandPrefix,
-) {
-	return processes
-		.filter((processInfo) => processInfo.command === appCommandPrefix)
-		.sort((left, right) => right.etimes - left.etimes)[0];
-}
-
-export function discoverTrackedProcesses(
-	processes,
-	tracked,
-	options = DEFAULTS,
-) {
-	const nextTracked = new Map(tracked);
-	const capMain = findCapMainProcess(processes, options.appCommandPrefix);
-
-	if (!capMain) {
-		throw new Error(
-			`No running Cap process found for ${options.appCommandPrefix}`,
-		);
-	}
-
-	for (const processInfo of processes) {
-		const classification = classifyProcess(
-			processInfo.command,
-			options.appCommandPrefix,
-		);
-
-		if (!classification) continue;
-
-		const isAlreadyTracked = nextTracked.has(processInfo.pid);
-		const isLongLivedCapWebKit =
-			classification.group === "cap" &&
-			classification.kind.startsWith("webkit-") &&
-			Math.abs(processInfo.etimes - capMain.etimes) <=
-				options.initialWebKitWindowSeconds;
-		const isFreshCapWebKit =
-			classification.group === "cap" &&
-			classification.kind.startsWith("webkit-") &&
-			processInfo.etimes <= options.newWebKitWindowSeconds;
-		const shouldTrack =
-			classification.kind === "cap-main" ||
-			classification.kind === "cap-crash" ||
-			classification.group === "system" ||
-			classification.group === "media" ||
-			isLongLivedCapWebKit ||
-			isFreshCapWebKit;
-
-		if (!shouldTrack) continue;
-
-		nextTracked.set(processInfo.pid, {
-			...processInfo,
-			...classification,
-			isNew: !isAlreadyTracked,
-		});
-	}
-
-	return nextTracked;
-}
-
-export function buildSample(processes, tracked, options = DEFAULTS) {
-	const discoveredTracked = discoverTrackedProcesses(
-		processes,
-		tracked,
-		options,
-	);
-	const liveTracked = new Map();
-	let capTotalKb = 0;
-	let mediaTotalKb = 0;
-	let windowServerTotalKb = 0;
-	const processRows = [];
-	const newPids = [];
-
-	for (const [pid, trackedProcess] of discoveredTracked.entries()) {
-		const liveProcess = processes.find(
-			(processInfo) => processInfo.pid === pid,
-		);
-
-		if (!liveProcess) continue;
-
-		const row = {
-			...trackedProcess,
-			etimes: liveProcess.etimes,
-			rssKb: liveProcess.rssKb,
-			vszKb: liveProcess.vszKb,
-		};
-
-		liveTracked.set(pid, row);
-		processRows.push(row);
-
-		if (row.group === "cap") capTotalKb += row.rssKb;
-		if (row.group === "media") mediaTotalKb += row.rssKb;
-		if (row.group === "system") windowServerTotalKb += row.rssKb;
-		if (row.isNew) newPids.push(pid);
-	}
-
-	processRows.sort((left, right) => right.rssKb - left.rssKb);
-
-	return {
-		capTotalKb,
-		grandTotalKb: capTotalKb + mediaTotalKb + windowServerTotalKb,
-		mediaTotalKb,
-		newPids: newPids.sort((left, right) => left - right),
-		processRows,
-		tracked: liveTracked,
-		windowServerTotalKb,
-	};
-}
-
-export function pickSettledSample(
-	samples,
-	trailingSamples = DEFAULTS.trailingSamples,
-) {
-	if (samples.length === 0) {
-		throw new Error("Cannot pick a settled sample from an empty set");
-	}
-
-	const window = samples.slice(-Math.min(trailingSamples, samples.length));
-
-	return window.reduce((best, sample) =>
-		sample.grandTotalKb < best.grandTotalKb ? sample : best,
-	);
-}
-
-export function evaluateCycles(
-	baselineKb,
-	settledCycleKbs,
-	options = DEFAULTS,
-) {
-	const maxCycleRatchetKb = toKb(options.maxCycleRatchetMb);
-	const maxTotalGrowthKb = toKb(options.maxTotalGrowthMb);
-	const failures = [];
-
-	for (let index = 0; index < settledCycleKbs.length; index += 1) {
-		const current = settledCycleKbs[index];
-		const previous = index === 0 ? baselineKb : settledCycleKbs[index - 1];
-		const cycleDeltaKb = current - previous;
-		const totalDeltaKb = current - baselineKb;
-
-		if (cycleDeltaKb > maxCycleRatchetKb) {
-			failures.push({
-				cycle: index + 1,
-				type: "cycle-ratchet",
-				valueKb: cycleDeltaKb,
-			});
-		}
-
-		if (totalDeltaKb > maxTotalGrowthKb) {
-			failures.push({
-				cycle: index + 1,
-				type: "total-growth",
-				valueKb: totalDeltaKb,
-			});
-		}
-	}
-
-	return failures;
-}
-
-export function formatMb(kb) {
-	return `${(kb / 1024).toFixed(1)} MB`;
-}
diff --git a/apps/desktop/scripts/desktop-memory-soak.js b/apps/desktop/scripts/desktop-memory-soak.js
deleted file mode 100644
index 1cfd23ae91c..00000000000
--- a/apps/desktop/scripts/desktop-memory-soak.js
+++ /dev/null
@@ -1,583 +0,0 @@
-import { execFileSync, spawn } from "node:child_process";
-import { existsSync, readFileSync, writeFileSync } from "node:fs";
-import process from "node:process";
-import { createInterface } from "node:readline/promises";
-import { setTimeout as sleep } from "node:timers/promises";
-import {
-	buildSample,
-	DEFAULTS,
-	evaluateCycles,
-	findCapMainProcess,
-	formatMb,
-	parsePsOutput,
-	pickSettledSample,
-} from "./desktop-memory-soak-lib.js";
-
-const HOTKEYS = {
-	startStudioRecording: {
-		code: "KeyR",
-		meta: true,
-		ctrl: true,
-		alt: true,
-		shift: true,
-	},
-	stopRecording: {
-		code: "KeyT",
-		meta: true,
-		ctrl: true,
-		alt: true,
-		shift: true,
-	},
-};
-
-const APPLE_KEY_CODES = {
-	KeyR: 15,
-	KeyT: 17,
-};
-
-const INSTALLED_APP_COMMAND_PREFIX = "/Applications/Cap.app/Contents/MacOS/Cap";
-
-function parseArgs(argv) {
-	const options = {
-		...DEFAULTS,
-		manual: false,
-		skipDisplaySleep: false,
-	};
-
-	for (let index = 0; index < argv.length; index += 1) {
-		const token = argv[index];
-		const next = argv[index + 1];
-
-		if (token === "--help" || token === "-h") {
-			options.help = true;
-			continue;
-		}
-
-		if (!token.startsWith("--")) continue;
-
-		if (token === "--manual") {
-			options.manual = true;
-			continue;
-		}
-
-		if (token === "--skip-display-sleep") {
-			options.skipDisplaySleep = true;
-			continue;
-		}
-
-		const rawValue = token.includes("=")
-			? token.slice(token.indexOf("=") + 1)
-			: next;
-
-		if (token.startsWith("--app-command-prefix")) {
-			options.appCommandPrefix = rawValue;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--store-path")) {
-			options.storePath = rawValue;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		const value = Number(rawValue);
-
-		if (token.startsWith("--cycles")) {
-			options.cycles = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--baseline-seconds")) {
-			options.baselineSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--settle-seconds")) {
-			options.settleSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--interval-seconds")) {
-			options.intervalSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--max-cycle-ratchet-mb")) {
-			options.maxCycleRatchetMb = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--max-total-growth-mb")) {
-			options.maxTotalGrowthMb = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--new-webkit-window-seconds")) {
-			options.newWebKitWindowSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--initial-webkit-window-seconds")) {
-			options.initialWebKitWindowSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--recording-seconds")) {
-			options.recordingSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--display-sleep-seconds")) {
-			options.displaySleepSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--launch-timeout-seconds")) {
-			options.launchTimeoutSeconds = value;
-			if (!token.includes("=")) index += 1;
-			continue;
-		}
-
-		if (token.startsWith("--stop-timeout-seconds")) {
-			options.stopTimeoutSeconds = value;
-			if (!token.includes("=")) index += 1;
-		}
-	}
-
-	return options;
-}
-
-function printHelp() {
-	console.log("Cap desktop memory soak test");
-	console.log("");
-	console.log("Usage:");
-	console.log("  bun run --cwd apps/desktop test:memory [options]");
-	console.log("");
-	console.log("Options:");
-	console.log(
-		`  --cycles                        default ${DEFAULTS.cycles}`,
-	);
-	console.log(
-		`  --baseline-seconds              default ${DEFAULTS.baselineSeconds}`,
-	);
-	console.log(
-		`  --settle-seconds                default ${DEFAULTS.settleSeconds}`,
-	);
-	console.log(
-		`  --interval-seconds              default ${DEFAULTS.intervalSeconds}`,
-	);
-	console.log(
-		`  --max-cycle-ratchet-mb          default ${DEFAULTS.maxCycleRatchetMb}`,
-	);
-	console.log(
-		`  --max-total-growth-mb           default ${DEFAULTS.maxTotalGrowthMb}`,
-	);
-	console.log(
-		`  --new-webkit-window-seconds     default ${DEFAULTS.newWebKitWindowSeconds}`,
-	);
-	console.log(
-		`  --initial-webkit-window-seconds  default ${DEFAULTS.initialWebKitWindowSeconds}`,
-	);
-	console.log(
-		`  --recording-seconds             default ${DEFAULTS.recordingSeconds}`,
-	);
-	console.log(
-		`  --display-sleep-seconds         default ${DEFAULTS.displaySleepSeconds}`,
-	);
-	console.log(
-		`  --launch-timeout-seconds        default ${DEFAULTS.launchTimeoutSeconds}`,
-	);
-	console.log(
-		`  --stop-timeout-seconds          default ${DEFAULTS.stopTimeoutSeconds}`,
-	);
-	console.log(
-		`  --app-command-prefix         default ${DEFAULTS.appCommandPrefix}`,
-	);
-	console.log(
-		`  --store-path                 default ${DEFAULTS.storePath}`,
-	);
-	console.log(
-		"  --manual                           keep the old prompt-driven flow",
-	);
-	console.log(
-		"  --skip-display-sleep               skip pmset displaysleepnow",
-	);
-	console.log("");
-	console.log(
-		"The script launches the target Cap binary, rewrites the selected store with temporary hotkeys, tracks Cap, WindowServer, related WebKit helpers, and camera/audio daemons, and restores the store afterward.",
-	);
-	console.log(
-		"Quit any other Cap instance before running this so WindowServer noise stays attributable to the dev build.",
-	);
-}
-
-function readProcesses() {
-	const output = execFileSync(
-		"ps",
-		["-axo", "pid=,rss=,vsz=,etime=,command="],
-		{ encoding: "utf8" },
-	);
-
-	return parsePsOutput(output);
-}
-
-function sampleCount(durationSeconds, intervalSeconds) {
-	return Math.max(1, Math.ceil(durationSeconds / intervalSeconds));
-}
-
-function renderTopProcesses(sample) {
-	return sample.processRows
-		.slice(0, 5)
-		.map((row) => `${row.label}:${row.pid}:${formatMb(row.rssKb)}`)
-		.join("  ");
-}
-
-async function collectPhase(name, seconds, intervalSeconds, tracked, options) {
-	const samples = [];
-	let currentTracked = tracked;
-	const totalSamples = sampleCount(seconds, intervalSeconds);
-
-	for (let index = 0; index < totalSamples; index += 1) {
-		const sample = buildSample(readProcesses(), currentTracked, options);
-		currentTracked = sample.tracked;
-		samples.push(sample);
-
-		const newPidSummary =
-			sample.newPids.length > 0 ? ` new:${sample.newPids.join(",")}` : "";
-
-		console.log(
-			`${name} ${index + 1}/${totalSamples} cap=${formatMb(sample.capTotalKb)} ws=${formatMb(sample.windowServerTotalKb)} media=${formatMb(sample.mediaTotalKb)} total=${formatMb(sample.grandTotalKb)}${newPidSummary}`,
-		);
-		console.log(`  ${renderTopProcesses(sample)}`);
-
-		if (index < totalSamples - 1) {
-			await sleep(intervalSeconds * 1000);
-		}
-	}
-
-	return {
-		samples,
-		settled: pickSettledSample(samples, options.trailingSamples),
-		tracked: currentTracked,
-	};
-}
-
-function restoreStore(path, originalStoreContents) {
-	writeFileSync(path, originalStoreContents);
-}
-
-function writeAutomationStore(path) {
-	if (!existsSync(path)) {
-		throw new Error(`Store not found: ${path}`);
-	}
-
-	const originalStoreContents = readFileSync(path, "utf8");
-	const originalStore = JSON.parse(originalStoreContents);
-	const nextStore = {
-		...originalStore,
-		hotkeys: {
-			hotkeys: {
-				...(originalStore.hotkeys?.hotkeys ?? {}),
-				startStudioRecording: HOTKEYS.startStudioRecording,
-				stopRecording: HOTKEYS.stopRecording,
-			},
-		},
-		recording_settings: {
-			...(originalStore.recording_settings ?? {}),
-			mode: "studio",
-			target: null,
-		},
-	};
-
-	writeFileSync(path, `${JSON.stringify(nextStore, null, 2)}\n`);
-
-	return originalStoreContents;
-}
-
-function modifiersForHotkey(hotkey) {
-	return [
-		hotkey.meta ? "command down" : null,
-		hotkey.ctrl ? "control down" : null,
-		hotkey.alt ? "option down" : null,
-		hotkey.shift ? "shift down" : null,
-	]
-		.filter(Boolean)
-		.join(", ");
-}
-
-function pressHotkey(hotkey) {
-	const keyCode = APPLE_KEY_CODES[hotkey.code];
-
-	if (keyCode === undefined) {
-		throw new Error(`Unsupported hotkey code: ${hotkey.code}`);
-	}
-
-	execFileSync("osascript", [
-		"-e",
-		`tell application "System Events" to key code ${keyCode} using {${modifiersForHotkey(hotkey)}}`,
-	]);
-}
-
-function sleepDisplay() {
-	execFileSync("pmset", ["displaysleepnow"]);
-}
-
-function listOtherCapProcesses(options) {
-	return readProcesses().filter(
-		(processInfo) =>
-			processInfo.command !== options.appCommandPrefix &&
-			(processInfo.command === INSTALLED_APP_COMMAND_PREFIX ||
-				processInfo.command ===
-					`${INSTALLED_APP_COMMAND_PREFIX} --crash-reporter-server=/tmp/socket` ||
-				processInfo.command.startsWith(
-					`${INSTALLED_APP_COMMAND_PREFIX} --crash-reporter-server=`,
-				) ||
-				processInfo.command.endsWith("/target/debug/cap-desktop")),
-	);
-}
-
-async function waitForApp(options) {
-	const deadline = Date.now() + options.launchTimeoutSeconds * 1000;
-
-	while (Date.now() < deadline) {
-		const app = findCapMainProcess(readProcesses(), options.appCommandPrefix);
-
-		if (app) return app;
-
-		await sleep(500);
-	}
-
-	throw new Error(
-		`Cap did not launch within ${options.launchTimeoutSeconds} seconds: ${options.appCommandPrefix}`,
-	);
-}
-
-async function waitForAppExit(child, timeoutSeconds) {
-	const deadline = Date.now() + timeoutSeconds * 1000;
-
-	while (Date.now() < deadline) {
-		if (child.exitCode !== null) return;
-		await sleep(250);
-	}
-}
-
-async function stopChild(child, timeoutSeconds) {
-	if (!child || child.exitCode !== null) return;
-
-	child.kill("SIGTERM");
-	await waitForAppExit(child, timeoutSeconds);
-
-	if (child.exitCode !== null) return;
-
-	child.kill("SIGKILL");
-	await waitForAppExit(child, 2);
-}
-
-async function runAutomatedCycle(cycle, tracked, options) {
-	console.log(`Cycle ${cycle}/${options.cycles}: start recording`);
-	pressHotkey(HOTKEYS.startStudioRecording);
-	await sleep(options.recordingSeconds * 1000);
-
-	if (!options.skipDisplaySleep) {
-		console.log(
-			`Cycle ${cycle}/${options.cycles}: sleep display for ${options.displaySleepSeconds}s`,
-		);
-		sleepDisplay();
-		await sleep(options.displaySleepSeconds * 1000);
-	}
-
-	console.log(`Cycle ${cycle}/${options.cycles}: stop recording`);
-	pressHotkey(HOTKEYS.stopRecording);
-	await sleep(1000);
-	pressHotkey(HOTKEYS.stopRecording);
-	await sleep(3000);
-
-	return collectPhase(
-		`cycle-${cycle}`,
-		options.settleSeconds,
-		options.intervalSeconds,
-		tracked,
-		options,
-	);
-}
-
-async function main() {
-	if (process.platform !== "darwin") {
-		console.error("This soak test only supports macOS.");
-		process.exit(1);
-	}
-
-	const options = parseArgs(process.argv.slice(2));
-
-	if (options.help) {
-		printHelp();
-		return;
-	}
-
-	const existingTargetProcess = findCapMainProcess(
-		readProcesses(),
-		options.appCommandPrefix,
-	);
-	if (existingTargetProcess) {
-		throw new Error(
-			`Quit the target app before running the soak test: ${options.appCommandPrefix}`,
-		);
-	}
-
-	const otherCapProcesses = listOtherCapProcesses(options);
-	if (otherCapProcesses.length > 0) {
-		throw new Error(
-			`Quit other Cap processes before running the soak test: ${otherCapProcesses.map((processInfo) => processInfo.command).join(", ")}`,
-		);
-	}
-
-	let originalStoreContents = null;
-	let child = null;
-	let cleanupStarted = false;
-	const rl = options.manual
-		? createInterface({
-				input: process.stdin,
-				output: process.stdout,
-			})
-		: null;
-
-	const cleanup = async () => {
-		if (cleanupStarted) return;
-		cleanupStarted = true;
-		await stopChild(child, options.stopTimeoutSeconds);
-		if (originalStoreContents !== null) {
-			restoreStore(options.storePath, originalStoreContents);
-		}
-	};
-
-	for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
-		process.on(signal, () => {
-			cleanup()
-				.then(() => process.exit(1))
-				.catch((error) => {
-					console.error(error instanceof Error ? error.message : String(error));
-					process.exit(1);
-				});
-		});
-	}
-
-	try {
-		originalStoreContents = writeAutomationStore(options.storePath);
-
-		if (!existsSync(options.appCommandPrefix)) {
-			throw new Error(`App binary not found: ${options.appCommandPrefix}`);
-		}
-
-		child = spawn(options.appCommandPrefix, [], {
-			stdio: "ignore",
-		});
-
-		await waitForApp(options);
-		await sleep(3000);
-
-		let tracked = new Map();
-
-		console.log("Collecting baseline...");
-		const baselinePhase = await collectPhase(
-			"baseline",
-			options.baselineSeconds,
-			options.intervalSeconds,
-			tracked,
-			options,
-		);
-		tracked = baselinePhase.tracked;
-
-		const baseline = baselinePhase.settled;
-		console.log(
-			`Baseline settled total=${formatMb(baseline.grandTotalKb)} cap=${formatMb(baseline.capTotalKb)} ws=${formatMb(baseline.windowServerTotalKb)} media=${formatMb(baseline.mediaTotalKb)}`,
-		);
-
-		const cycleSettledTotals = [];
-
-		for (let cycle = 1; cycle <= options.cycles; cycle += 1) {
-			const phase = options.manual
-				? await rl
-						.question(
-							`Cycle ${cycle}/${options.cycles}: start and stop a recording in Cap, then press Enter.`,
-						)
-						.then(() =>
-							collectPhase(
-								`cycle-${cycle}`,
-								options.settleSeconds,
-								options.intervalSeconds,
-								tracked,
-								options,
-							),
-						)
-				: await runAutomatedCycle(cycle, tracked, options);
-			tracked = phase.tracked;
-
-			const settled = phase.settled;
-			cycleSettledTotals.push(settled.grandTotalKb);
-
-			const previousSettled =
-				cycle === 1
-					? baseline.grandTotalKb
-					: cycleSettledTotals[cycleSettledTotals.length - 2];
-			const cycleDeltaKb = settled.grandTotalKb - previousSettled;
-			const totalDeltaKb = settled.grandTotalKb - baseline.grandTotalKb;
-			const newWebContent = settled.processRows
-				.filter((row) => row.kind === "webkit-webcontent" && row.isNew)
-				.map((row) => row.pid);
-
-			console.log(
-				`Settled cycle ${cycle}: total=${formatMb(settled.grandTotalKb)} cap=${formatMb(settled.capTotalKb)} ws=${formatMb(settled.windowServerTotalKb)} media=${formatMb(settled.mediaTotalKb)} delta-prev=${formatMb(cycleDeltaKb)} delta-baseline=${formatMb(totalDeltaKb)}`,
-			);
-
-			if (newWebContent.length > 0) {
-				console.log(`  new-webcontent=${newWebContent.join(",")}`);
-			}
-		}
-
-		const failures = evaluateCycles(
-			baseline.grandTotalKb,
-			cycleSettledTotals,
-			options,
-		);
-
-		console.log("");
-		console.log("Summary");
-		console.log(
-			`  baseline=${formatMb(baseline.grandTotalKb)} final=${formatMb(cycleSettledTotals[cycleSettledTotals.length - 1] ?? baseline.grandTotalKb)}`,
-		);
-
-		if (failures.length === 0) {
-			console.log("  result=PASS");
-			return;
-		}
-
-		console.log("  result=FAIL");
-
-		for (const failure of failures) {
-			console.log(
-				`  cycle=${failure.cycle} type=${failure.type} value=${formatMb(failure.valueKb)}`,
-			);
-		}
-
-		process.exitCode = 1;
-	} finally {
-		await cleanup();
-		rl?.close();
-	}
-}
-
-main().catch((error) => {
-	console.error(error instanceof Error ? error.message : String(error));
-	process.exit(1);
-});
diff --git a/apps/desktop/scripts/desktop-memory-soak.test.js b/apps/desktop/scripts/desktop-memory-soak.test.js
deleted file mode 100644
index d367a153692..00000000000
--- a/apps/desktop/scripts/desktop-memory-soak.test.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
-	buildSample,
-	classifyProcess,
-	DEFAULTS,
-	evaluateCycles,
-	parsePsOutput,
-	pickSettledSample,
-} from "./desktop-memory-soak-lib.js";
-
-describe("desktop-memory-soak", () => {
-	it("classifies Cap and media processes", () => {
-		expect(classifyProcess(DEFAULTS.appCommandPrefix)).toMatchObject({
-			group: "cap",
-			kind: "cap-main",
-		});
-		expect(
-			classifyProcess(
-				`${DEFAULTS.appCommandPrefix} --crash-reporter-server=/tmp/socket`,
-			),
-		).toMatchObject({
-			group: "cap",
-			kind: "cap-crash",
-		});
-		expect(
-			classifyProcess(
-				"/System/Library/Frameworks/WebKit.framework/Versions/A/XPCServices/com.apple.WebKit.WebContent.xpc/Contents/MacOS/com.apple.WebKit.WebContent",
-			),
-		).toMatchObject({
-			group: "cap",
-			kind: "webkit-webcontent",
-		});
-		expect(classifyProcess("/usr/sbin/coreaudiod")).toMatchObject({
-			group: "media",
-			kind: "coreaudiod",
-		});
-		expect(
-			classifyProcess(
-				"/System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer -daemon",
-			),
-		).toMatchObject({
-			group: "system",
-			kind: "window-server",
-		});
-	});
-
-	it("parses ps output and totals tracked processes", () => {
-		const processes = parsePsOutput(`
-1000 1200000 435000000 00:16:39 /System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer -daemon
-2004 400000 441000000 00:02:00 ${DEFAULTS.appCommandPrefix}
-2005 25000 435000000 00:02:00 ${DEFAULTS.appCommandPrefix} --crash-reporter-server=/tmp/socket
-2008 90000 440000000 00:02:00 /System/Library/Frameworks/WebKit.framework/Versions/A/XPCServices/com.apple.WebKit.GPU.xpc/Contents/MacOS/com.apple.WebKit.GPU
-2075 300000 507000000 00:01:55 /System/Library/Frameworks/WebKit.framework/Versions/A/XPCServices/com.apple.WebKit.WebContent.xpc/Contents/MacOS/com.apple.WebKit.WebContent
-431 50000 435000000 00:16:39 /usr/sbin/coreaudiod
-532 20000 435000000 00:16:39 /usr/libexec/cameracaptured
-`);
-
-		const sample = buildSample(processes, new Map(), DEFAULTS);
-
-		expect(sample.capTotalKb).toBe(815000);
-		expect(sample.mediaTotalKb).toBe(70000);
-		expect(sample.windowServerTotalKb).toBe(1200000);
-		expect(sample.grandTotalKb).toBe(2085000);
-		expect(sample.newPids).toEqual([431, 532, 1000, 2004, 2005, 2008, 2075]);
-	});
-
-	it("picks the lowest sample from the trailing settle window", () => {
-		const settled = pickSettledSample(
-			[
-				{ grandTotalKb: 1200 },
-				{ grandTotalKb: 1000 },
-				{ grandTotalKb: 900 },
-				{ grandTotalKb: 950 },
-			],
-			3,
-		);
-
-		expect(settled.grandTotalKb).toBe(900);
-	});
-
-	it("flags cycle ratchet and total growth failures", () => {
-		const failures = evaluateCycles(1000, [1200, 1400, 2200], {
-			...DEFAULTS,
-			maxCycleRatchetMb: 0.1,
-			maxTotalGrowthMb: 0.5,
-		});
-
-		expect(failures).toEqual([
-			{ cycle: 1, type: "cycle-ratchet", valueKb: 200 },
-			{ cycle: 2, type: "cycle-ratchet", valueKb: 200 },
-			{ cycle: 3, type: "cycle-ratchet", valueKb: 800 },
-			{ cycle: 3, type: "total-growth", valueKb: 1200 },
-		]);
-	});
-});
diff --git a/apps/desktop/scripts/desktop-picker-performance.md b/apps/desktop/scripts/desktop-picker-performance.md
deleted file mode 100644
index a8a7f2af909..00000000000
--- a/apps/desktop/scripts/desktop-picker-performance.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# Desktop picker latency
-
-The display and window pickers should reuse healthy selected devices. Restoring saved inputs must not open error dialogs for disconnected devices. Recording still waits for input setup, and missing selections remain saved for reconnection.
-
-## Benchmarks
-
-The ignored native benchmark runs real device discovery, microphone configuration lookups, camera setup, and camera frame delivery. It compares repeated camera setup with reuse and compares microphone lookup strategies in the same process. It opens the specified camera without creating recordings.
-
-```sh
-cd apps/desktop-gpui
-CAP_PICKER_BENCH_CAMERA_ID= cargo test -p cap-desktop-gpui --bin cap-gpui picker_benchmark::native_picker_latency -- --ignored --nocapture
-```
-
-Both debug apps also support `CAP_PICKER_BENCHMARK_OUTPUT=` and `CAP_PICKER_BENCHMARK_DELAY_MS=0`. They open and dismiss six alternating display/window pickers, write timings, and exit. The default delay is 2000 ms. Run them sequentially after compilation has finished.
-
-The Tauri app also takes `CAP_PICKER_BENCHMARK_SAMPLES=` to run more than six picker cycles and `CAP_PICKER_BENCHMARK_CAMERA_CYCLES=` to follow them with camera-window close/reopen cycles (written to `.camera.json`), and `CAP_PICKER_BENCHMARK_ESCAPE=direct` (or `wait:`, for an external driver sending a real Escape keypress) to check the picker is dismissed through the Escape path (written to `.escape.json`). Together with `NSZombieEnabled=YES` this is the overlay soak that guards the vendored `tauri-nspanel` ownership fix: the picker reveal converts its overlay windows to NSPanels on every open, and the camera window is destroyed and recreated under one label, so an over-release in either path shows up as a `message sent to deallocated instance` line or a crash within a few cycles instead of in a user's session.
-
-```sh
-NSZombieEnabled=YES CAP_PICKER_BENCHMARK_OUTPUT=/tmp/picker.json CAP_PICKER_BENCHMARK_SAMPLES=200 CAP_PICKER_BENCHMARK_CAMERA_CYCLES=30 target/debug/cap-desktop
-```
-
-- Tauri measures from the target-mode request through native window creation, frontend initialization, input restoration, and two animation frames with the recording button enabled.
-- GPUI includes pending device discovery and selected-input readiness, then waits for two rendered overlay frames.
-- Sample zero measures the first picker in a fresh process. Later samples measure reopening. These are picker timings, not total process launch times. They are not directly interchangeable across UI implementations.
-
-Use isolated settings for these app runs. GPUI accepts `CAP_GPUI_APP_DATA_DIR`. Build Tauri with a separate `TAURI_CONFIG` identifier and seed only that identifier's application-data `store`. Set the recording mode to `studio` and select the test inputs in that store. Never point a benchmark at customer recordings or an active recording session.
-
-For a Tauri build with bundled frontend assets and no development server:
-
-```sh
-pnpm --filter @cap/desktop build
-TAURI_CONFIG='{"identifier":"so.cap.desktop.picker-benchmark","productName":"Cap Picker Benchmark","build":{"devUrl":null}}' cargo build -p cap-desktop --features tauri/custom-protocol
-```
-
-## What changed
-
-- Startup restoration bypasses the global mutation handler that displays native error dialogs. Explicit device selections retain their normal error handling.
-- Matching simultaneous Tauri requests share pending setup. A different device, configuration, or camera-window request supersedes it.
-- Repeated Tauri selections reuse a feed only when its identity and settings match and frames or audio samples arrived within 250 ms. Stalled feeds continue through setup and recovery.
-- Camera-only mode retains its normal camera setup path, which also handles its different preview routing.
-- GPUI startup lists microphone names without opening every device's audio configuration. Detailed configuration remains available when opening the microphone menu.
-- Tauri microphone details configure only the requested device, avoiding a complete configuration scan for each row.
-- The picker keeps its action label while readiness checks run.
-
-## Local comparison, 2026-09-09
-
-On macOS with five cameras, five microphones, one display, and twelve listed windows; the built-in camera and microphone were selected. The baseline used the original Rust selection/discovery paths from `0c403be8a57f7ba4de67140273708a392c8b4050`. Both variants used the same frontend, timing hooks, settings, and build profile. Runs were sequential with no compilation in progress.
-
-| UI measurement | Before | After |
-| --- | ---: | ---: |
-| Tauri first display picker | 1,921 ms | 955 ms |
-| Tauri reopening, median of five alternating display/window openings | 815 ms | 65 ms |
-| GPUI first display picker | 1,046 ms | 679 ms |
-| GPUI reopening, median of five alternating display/window openings | 28 ms | 24 ms |
-
-All 24 openings reached readiness. GPUI reopening was already fast; its improvement is in discovery during startup. Across the optimized Tauri run, native logs showed one microphone setup and one camera setup for all six openings.
-
-The final native comparison measured median microphone metadata lookup time of 1,579 ms for repeated full scans versus 187 ms for named lookups. Repeated camera setup took 433 ms; checking an already-streaming matching camera took 0.02 ms, with subsequent frames received in every sample. That last number is the feed acknowledgment time, not UI latency or first-frame startup.
-
-An additional Tauri run restored the unavailable `Shure MV7+` and `046d:08e5` selections. All six pickers reached readiness without blocking dialogs, and both selections remained persisted. Cold samples are individual observations, not percentile estimates.
-
-## Validation boundaries
-
-The local measurements exercise macOS hardware and debug Rust builds with bundled production frontend assets. They do not establish Windows hardware latency or release-package behavior. Keep cold samples separate from reopening samples, and preserve device counts and first-frame checks when comparing runs.
diff --git a/apps/desktop/scripts/prepare.js b/apps/desktop/scripts/prepare.js
deleted file mode 100644
index 66df72366f2..00000000000
--- a/apps/desktop/scripts/prepare.js
+++ /dev/null
@@ -1,194 +0,0 @@
-// @ts-check
-
-import * as fs from "node:fs/promises";
-import * as path from "node:path";
-import { fileURLToPath } from "node:url";
-import { shouldBundleGpui } from "../../../scripts/run-gpui-build.mjs";
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-/**
- * Creates a Microsoft Windows Installer (TM) compatible version from the provided crate's semver version.
- * `major.minor.patch.build`
- *
- * @see {@link https://tauri.app/reference/config/#version-1}
- * @param {string} cargoFilePath
- * @returns {Promise}
- */
-async function semverToWIXCompatibleVersion(cargoFilePath) {
-	const config = await fs.readFile(cargoFilePath, "utf-8");
-	const match = /version\s*=\s*"([\w.-]+)"/.exec(config);
-	if (!match)
-		throw new Error(
-			'Failed to extract version from "Cargo.toml". Have you removed the main crate version by accident?',
-		);
-
-	const ver = match[1];
-	const [core, buildOrPrerelease] = ver.includes("+")
-		? ver.split("+")
-		: ver.split("-");
-	const [major, minor, patch] = core.split(".");
-	let build = 0;
-	if (buildOrPrerelease) {
-		const numMatch = buildOrPrerelease.match(/\d+$/);
-		build = numMatch ? parseInt(numMatch[0], 10) : 0;
-	}
-	const wixVersion = `${major}.${minor}.${patch}${
-		build === 0 ? "" : `.${build}`
-	}`;
-	if (wixVersion !== ver)
-		console.log(`Using wix-compatible version ${ver} --> ${wixVersion}`);
-	return wixVersion;
-}
-/**
- * Deeply merges two objects
- *
- * @param {Object} target
- * @param {Object} source
- * @returns {Object}
- */
-export function deepMerge(target, source) {
-	for (const key of Object.keys(source)) {
-		if (
-			source[key] instanceof Object &&
-			key in target &&
-			target[key] instanceof Object
-		) {
-			Object.assign(source[key], deepMerge(target[key], source[key]));
-		}
-	}
-	return { ...target, ...source };
-}
-
-/**
- * Writes platform-specific tauri configs
- *
- * @param {NodeJS.Platform} platform
- * @param {{} | undefined} configOptions
- */
-export async function createTauriPlatformConfigs(
-	platform,
-	configOptions = undefined,
-) {
-	const srcTauri = path.join(__dirname, "../src-tauri/");
-	const profile = process.argv.includes("--release") ? "release" : "debug";
-	const hostArchitecture = process.arch === "arm64" ? "aarch64" : "x86_64";
-	const hostTargets = {
-		darwin: `${hostArchitecture}-apple-darwin`,
-		win32: `${hostArchitecture}-pc-windows-msvc`,
-		linux: `${hostArchitecture}-unknown-linux-gnu`,
-	};
-	const target = process.env.RUST_TARGET_TRIPLE ?? hostTargets[platform];
-	const extension = platform === "win32" ? ".exe" : "";
-	const sidecarAvailable = target
-		? await fs
-				.access(
-					path.join(srcTauri, "binaries", `cap-gpui-${target}${extension}`),
-				)
-				.then(() => true)
-				.catch(() => false)
-		: false;
-	const developmentWorkspaceAvailable = await fs
-		.access(path.join(__dirname, "../../desktop-gpui/dev.sh"))
-		.then(() => true)
-		.catch(() => false);
-	const includeGpui = shouldBundleGpui(
-		platform,
-		process.env,
-		profile,
-		sidecarAvailable,
-		developmentWorkspaceAvailable,
-	);
-	const externalBin = [
-		"binaries/cap-muxer",
-		"binaries/cap-exporter",
-		"binaries/cap-cli",
-		...(includeGpui ? ["binaries/cap-gpui"] : []),
-	];
-	let baseConfig = {};
-	let configFileName = null;
-
-	console.log(`Updating Platform (${platform}) Tauri config...`);
-	if (platform === "win32") {
-		configFileName = "tauri.windows.conf.json";
-		baseConfig = {
-			...baseConfig,
-			bundle: {
-				externalBin,
-				resources: {
-					"../../../target/ffmpeg/bin/*.dll": "./",
-					"../../../target/native-deps/dxc/*.dll": "./",
-					"../../../target/native-deps/dxc/LICENSE-*.txt": "licenses/dxc/",
-					"../../../target/native-deps/onnxruntime/lib/*.dll": "./",
-				},
-				windows: {
-					wix: {
-						version: await semverToWIXCompatibleVersion(
-							path.join(srcTauri, "Cargo.toml"),
-						),
-					},
-				},
-			},
-		};
-	}
-
-	if (platform === "darwin") {
-		configFileName = "tauri.macos.conf.json";
-		baseConfig = {
-			...baseConfig,
-			bundle: {
-				externalBin,
-				resources: {
-					"../../../target/native-deps/onnxruntime/lib/libonnxruntime.dylib":
-						"onnxruntime/lib/libonnxruntime.dylib",
-				},
-			},
-		};
-	}
-
-	if (platform === "linux") {
-		configFileName = "tauri.linux.conf.json";
-		const existingConfig = await fs
-			.readFile(path.join(srcTauri, configFileName), "utf-8")
-			.then(JSON.parse)
-			.catch((error) => {
-				if (error.code === "ENOENT") return {};
-				throw error;
-			});
-		baseConfig = deepMerge(existingConfig, { bundle: { externalBin } });
-	}
-
-	if (!configFileName) return;
-
-	const mergedConfig = configOptions
-		? deepMerge(baseConfig, configOptions)
-		: baseConfig;
-	await writeFileIfChanged(
-		`${srcTauri}/${configFileName}`,
-		JSON.stringify(mergedConfig, null, 2),
-	);
-}
-
-async function main() {
-	console.log("--- Preparing sidecars and configs...");
-	await createTauriPlatformConfigs(process.platform);
-	console.log("--- Preparation finished");
-}
-
-if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
-	main().catch((err) => {
-		console.error("\n--- Preparation Failed");
-		console.error(err);
-		console.error("---");
-		process.exitCode = 1;
-	});
-}
-
-async function writeFileIfChanged(filePath, contents) {
-	const currentContents = await fs
-		.readFile(filePath, "utf-8")
-		.catch(() => undefined);
-
-	if (currentContents !== contents) await fs.writeFile(filePath, contents);
-}
diff --git a/apps/desktop/scripts/prepare.test.js b/apps/desktop/scripts/prepare.test.js
deleted file mode 100644
index e43e2816d52..00000000000
--- a/apps/desktop/scripts/prepare.test.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { createLinuxBundleConfig } from "../../../scripts/linux-bundle-config.mjs";
-import { deepMerge } from "./prepare.js";
-
-describe("Tauri platform release configuration", () => {
-	it("preserves generated Linux shared-library mappings when adding GPUI", () => {
-		const existing = createLinuxBundleConfig(["libavcodec.so.61"]);
-
-		const merged = deepMerge(existing, {
-			bundle: { externalBin: ["binaries/cap-gpui"] },
-		});
-
-		expect(merged.bundle.externalBin).toEqual(["binaries/cap-gpui"]);
-		for (const format of ["deb", "rpm", "appimage"]) {
-			expect(merged.bundle.linux[format]).toEqual(
-				existing.bundle.linux[format],
-			);
-		}
-	});
-
-	it("preserves Windows resource mappings when platform overrides are applied", () => {
-		const merged = deepMerge(
-			{
-				bundle: {
-					externalBin: ["binaries/cap-cli", "binaries/cap-gpui"],
-					resources: { "ffmpeg/*.dll": "./" },
-				},
-			},
-			{ bundle: { windows: { wix: { version: "0.6.0" } } } },
-		);
-
-		expect(merged.bundle.externalBin).toContain("binaries/cap-gpui");
-		expect(merged.bundle.resources).toEqual({ "ffmpeg/*.dll": "./" });
-		expect(merged.bundle.windows.wix.version).toBe("0.6.0");
-	});
-});
diff --git a/apps/desktop/scripts/prodBeforeBundle.js b/apps/desktop/scripts/prodBeforeBundle.js
deleted file mode 100644
index ca477fd70fd..00000000000
--- a/apps/desktop/scripts/prodBeforeBundle.js
+++ /dev/null
@@ -1,74 +0,0 @@
-// @ts-check
-
-import { exec as execCb } from "node:child_process";
-import * as fs from "node:fs/promises";
-import * as path from "node:path";
-import { fileURLToPath } from "node:url";
-import { promisify } from "node:util";
-
-const exec = promisify(execCb);
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const targetDir = path.join(__dirname, "../../../target");
-
-async function main() {
-	if (process.platform === "darwin") {
-		const releaseBinary = await findReleaseBinary();
-		if (!releaseBinary) {
-			console.warn("No release binary found for dSYM generation; skipping.");
-			return;
-		}
-
-		await exec(
-			`dsymutil "${releaseBinary.binaryPath}" -o "${path.join(
-				targetDir,
-				releaseBinary.releaseFile,
-			)}.dSYM"`,
-		);
-	}
-}
-
-main();
-
-async function findReleaseBinary() {
-	const dirs = [
-		path.join(targetDir, "release"),
-		process.env.TAURI_ENV_TARGET_TRIPLE
-			? path.join(targetDir, `${process.env.TAURI_ENV_TARGET_TRIPLE}/release`)
-			: null,
-	].filter(Boolean);
-
-	for (let attempt = 0; attempt < 20; attempt++) {
-		for (const releaseDir of dirs) {
-			const releaseFile = await findReleaseFile(releaseDir);
-			if (releaseFile)
-				return { binaryPath: path.join(releaseDir, releaseFile), releaseFile };
-		}
-		await new Promise((resolve) => setTimeout(resolve, 250));
-	}
-
-	return null;
-}
-
-async function findReleaseFile(releaseDir) {
-	if (!(await fileExists(releaseDir))) return null;
-	const releaseFiles = await fs.readdir(releaseDir);
-	for (const name of ["Cap", "cap-desktop"]) {
-		if (releaseFiles.includes(name)) return name;
-	}
-
-	for (const releaseFile of releaseFiles) {
-		if (releaseFile.startsWith("Cap")) return releaseFile;
-	}
-
-	return null;
-}
-
-async function fileExists(path) {
-	return await fs
-		.access(path)
-		.then(() => true)
-		.catch(() => false);
-}
diff --git a/apps/desktop/scripts/stop-editor-performance.md b/apps/desktop/scripts/stop-editor-performance.md
deleted file mode 100644
index 0ddc230c363..00000000000
--- a/apps/desktop/scripts/stop-editor-performance.md
+++ /dev/null
@@ -1,712 +0,0 @@
-# Stop recording to usable editor
-
-Status on 2026-09-10: the user stopped further testing and requested sandbox shutdown and PR publication with the remaining gaps recorded. This is **not** an all-tests-passed result. GPUI v4 native suites passed on macOS (993), Linux (1,052) and Windows (970), with two existing ignores each and passing compilation checks. macOS passed 161 editor tests, 597 recording/recovery tests, 470 Tauri tests plus binding generation; desktop frontend 473, web frontend 3,047 and tooling 64 tests passed, with both frontend TypeScript checks passing. Linux root library/integration scopes, 536 Tauri function tests and three canonical executions passed. Windows completed 1,314 scoped library tests across eight crates; its remaining protocol tests, Tauri, muxer, integration, explicit GPU, canonical, hardware and strict-lint scopes were not run after the capacity guard stopped the lane. Strict GPUI and Linux export Clippy retain unchanged baseline findings. Linux ordinary static-library packaging failed for lack of disk space despite passing direct main-binary function tests. Active test sandboxes were stopped after evidence retention. Cleanup of older stalled restore requests is recorded separately and is not claimed complete here.
-
-The candidate now keeps eligible preparing playback alive after successful publication while ordinary loading completes, then lets the ordinary editor adopt the existing audio generation and consumed-audio clock. Native function tests cover that ownership path, completed PCM reuse, cancellation and stale-candidate disposal. Current application Stop-to-first-audible timing and physically uninterrupted audio/image handoff remain unproven. The user will test the application manually after automated verification. Earlier screenshots and benchmarks below apply to their recorded source snapshots; no new UI run or expanded two-hour benchmark was performed for this final verification.
-
-## Acceptance criteria
-
-Measure from the actual Stop action to capture shutdown, the first presented editor frame, first audible playback, and a successful seek near the end. A window or spinner appearing is not editor readiness.
-
-The earlier under-500-ms p95 target is retained as historical context, not an achieved measurement or the current optimization priority. The current goal is immediate editor feedback with truthful media readiness, including multi-hour recordings. Track capture frame drops, A/V alignment, audio sample counts, output quality, CPU, memory, and disk activity alongside latency. No finalizer percentage or elapsed-time animation may stand in for a confirmed playable prefix.
-
-## Measurements on 2026-09-09
-
-The experiments below record successive source snapshots from September 9–10. “Current” or “final” within a historical stage refers to that stage. Later implementation and verification updates are listed at the end; earlier counts and timings are not added to the final results.
-
-Evidence is retained locally in `.git/stop-editor-20260909`. Originals and disposable benchmark copies are separate. No customer recordings were modified.
-
-The GPUI runs used actual macOS screen capture at 3024 by 1964 and 60 fps. The native microphone was the MacBook Pro Microphone. The five-minute run also enabled system audio.
-
-| Native GPUI baseline                             |   Capture pipelines stopped | Stop to first editor frame |
-| ------------------------------------------------ | --------------------------: | -------------------------: |
-| 15 seconds, screen                               | Not separately instrumented |                 1,001.7 ms |
-| 60 seconds, screen and microphone                |                     38.9 ms |                 1,552.9 ms |
-| 300 seconds, screen, microphone and system audio |                    122.7 ms |                 6,013.5 ms |
-
-A native 60-second candidate recording reached the first editor frame in 937.9 ms, with capture pipelines stopped in 59.1 ms. These native runs captured changing foreground content and are flow observations, not controlled paired percentage comparisons. GPUI's frame marker observes delivery to the editor, not a measured display scanout.
-
-The native Tauri screen-only driver measured 1,686.6 ms baseline and 1,684.7 ms candidate from Stop to a rendered, visible canvas followed by two animation frames. Stop command completion was 139.9 ms and 177.3 ms respectively; neither value represents editor readiness. These are separate 15-second recordings using the root workspace's unoptimized debug build, so they are not release timings and are not comparable to the GPUI build profile.
-
-A Tauri candidate run with both microphone and system audio also completed recording and opened a rendered editor. Its 15-second debug run measured 2,622.0 ms to the presented-frame observer. This exercises the preserved-AAC path in Tauri, but has no matched native audio baseline.
-
-The controlled comparison replays identical preserved recording bytes through `RecoveryManager::remux_if_needed`, alternating baseline and candidate order. Each table entry is the median of three runs. Both executable versions use the GPUI development workspace's optimized dependencies. These are component benchmarks, not packaged release or full Stop-to-editor benchmarks.
-
-| Shared finalization fixture                   |    Baseline |   Candidate | Reduction |
-| --------------------------------------------- | ----------: | ----------: | --------: |
-| Native screen recording, 14.9 seconds         |    548.1 ms |    534.2 ms |      2.6% |
-| Native screen and microphone, 60 seconds      |  1,177.9 ms |    850.5 ms |     27.8% |
-| Two-hour replay, video and AAC audio, 2.40 GB | 47,616.9 ms | 19,499.9 ms |     59.0% |
-
-The two-hour fixture contains actual encoded screen and microphone media replayed to two-hour timestamps. It is not a two-hour capture or hardware soak. Its video is 1280 by 832 at 30 fps. It excludes long cursor and keyboard histories. Three samples do not establish p95 or p99 performance.
-
-An earlier overlapping run reported 18.1 seconds inside finalization and 19.6 seconds for the whole component call. Use the later alternating comparison above for the candidate assessment.
-
-## Initial candidate changes, September 9
-
-- Preserve single-track AAC media by remuxing it to M4A instead of decoding it and encoding it again as Ogg. Detect the actual codec; old files with misleading extensions retain the existing conversion path.
-- On macOS, attempt an independent copy-on-write staging clone, then use the existing exclusive copy fallback. This is not a hard link. Other operating systems retain the copy path.
-- Hash separate files with at most one additional scoped worker when the recovery inputs total at least 32 MiB and multiple CPUs are available. Keep full byte hashes, directory entries, symlink rejection, and the serial fallback if a worker cannot be created.
-- Reuse the editor audio resampler's output allocation across packets. Restore the requested sample capacity for every conversion and flush; keep the same decoder, resampling configuration, samples, and range behaviour.
-- Read fragmented video through a seekable concatenated input when creating a new finalized output. Avoid writing and rereading a recording-sized temporary aggregate. Existing destinations retain the original aggregate path because they can alias an input.
-- Add stage timing and opt-in native benchmark drivers. Retain source snapshots, staged validation, source rechecks, synchronization, transactional publication, and preserved originals.
-
-The recovery unit suite passed 58 tests. The integration suite passed 50 tests, with one existing ignored test. Added coverage verifies decoded AAC samples, microphone/system-audio offsets and device metadata, preserved original bytes, independent cloned files, exclusive destination creation, and large-file changes with restored timestamps. The audio unit suite passed 42 tests, including bit-exact comparisons against fresh resampling buffers at 16, 44.1, 48 and 96 kHz with one, two and six source channels and varying packet sizes. These results do not establish platform or release parity.
-
-Scoped Rust checks passed for `cap-audio`, `cap-recording`, `cap-desktop`, and `cap-desktop-gpui`. Rust formatting and the benchmark JavaScript's Biome check passed. Biome did not process Markdown, so this document was checked manually. Native baseline/candidate Tauri builds and the GPUI candidate build completed.
-
-## Further matched optimization benchmarks
-
-The bounded hashing experiment compares the preceding AAC/clone candidate with the same candidate plus concurrent hashing. Each result is the median of three alternating runs on identical preserved bytes, with builds and capture runs stopped. Evidence is in `optimization-stage-2/results.json`.
-
-| Shared finalization fixture              | Previous candidate | Concurrent hashing | Reduction |
-| ---------------------------------------- | -----------------: | -----------------: | --------: |
-| Native screen recording, 14.9 seconds    |           559.6 ms |           528.0 ms |      5.6% |
-| Native screen and microphone, 60 seconds |           950.4 ms |           843.5 ms |     11.2% |
-| Two-hour replay, video and AAC audio     |        19,943.9 ms |        15,329.0 ms |     23.1% |
-
-An instrumented long run still spent about 1.87 seconds validating staged inputs, 4.13 seconds finalizing video, and 1.81 seconds validating output tracks. Those validations remain. The entire component call took 15.58 seconds in that run. These results do not demonstrate duration-independent stopping or p95 performance.
-
-The resampler allocation experiment compares identical audio bytes through the full production audio loader, using five alternating runs per variant. Hashing the decoded samples happens after the timed load. Every before/after run matched both decoded byte count and SHA-256. Evidence is in `optimization-stage-3/results.json`.
-
-| Full audio loading     | Fresh buffers | Reused buffers | Reduction |
-| ---------------------- | ------------: | -------------: | --------: |
-| Native AAC, 60 seconds |      19.47 ms |       17.97 ms |      7.7% |
-| Legacy Ogg, 60 seconds |      56.29 ms |       55.02 ms |      2.2% |
-| AAC replay, two hours  |   2,063.26 ms |    1,988.66 ms |      3.6% |
-
-The long track still allocates 1,382,703,104 bytes for decoded samples. Buffer reuse reduces temporary allocation work; it does not implement bounded audio loading. The full decoded SHA-256 for that track was `5c5c67688b348b9e4d0e54a42e5021c630a7ab8c49c078c892d620e8f7fcd8c9` in both variants.
-
-The combined candidate completed fresh native screen, microphone and system-audio recordings in GPUI and Tauri. GPUI's 60-second run stopped its capture pipelines in 120.3 ms and delivered the first editor frame in 1,254.5 ms. Its five-second playback run reported 300 frames at 59.8 fps with zero dropped frames. Tauri's 15-second run reported stop-command completion in 329.2 ms and a rendered-editor marker at 2,081.6 ms. These are individual development-build flow checks, with changing capture content and different build profiles; they do not establish a matched native speedup or p95.
-
-## Avoiding the temporary combined video
-
-The finalized output remains the same progressive MP4. The clean finalization path now supplies the original fragment bytes through FFmpeg's seekable custom-input interface instead of writing an intermediate concatenated file. The reader uses a 64 KiB buffer and at most two source file handles during a file transition. It retains read failures even if the demuxer subsequently reports EOF. Custom I/O lifetime and buffer ownership follow the [FFmpeg example](https://ffmpeg.org/doxygen/trunk/avio_read_callback_8c-example.html).
-
-Existing output files retain the previous implementation. This preserves overwrites where the output aliases an input, including hard links; opening that destination directly while reading fragments could otherwise truncate its own input. Interrupted-recording recovery retains its established full-validation path.
-
-The final candidate, including the overwrite fallback, was compared with the previous concurrent-hashing candidate using three alternating runs per fixture. Temporary benchmark copies were removed after each completed subprocess; the original fixtures, logs and binaries remain. The last long candidate output was retained for native playback verification. Evidence is in `optimization-stage-4/guarded-results.json`.
-
-| Shared finalization fixture              | Previous candidate | Seekable fragment input | Reduction |
-| ---------------------------------------- | -----------------: | ----------------------: | --------: |
-| Native screen recording, 14.9 seconds    |          528.35 ms |               489.48 ms |      7.4% |
-| Native screen and microphone, 60 seconds |          763.30 ms |               678.32 ms |     11.1% |
-| Two-hour replay, video and AAC audio     |       15,245.86 ms |            13,593.52 ms |     10.8% |
-
-These remain component medians in development builds, not p95 or full Stop-to-editor measurements. All three final media sets matched the preceding candidate byte for byte in a retained comparison, including the complete MP4 containers. Recording metadata and project configuration also matched. Evidence is in `optimization-stage-4/media-equivalence.json` and `metadata-equivalence.json`.
-
-The encoder suite passed 112 tests, including concatenated read/seek equivalence, large seek offsets, truncated and missing fragments, sticky I/O failures, exact packets/timestamps, and existing-output aliases. Scoped clippy passed with warnings denied. Recovery checks passed 58 unit tests and 50 integration tests, with one existing ignored integration test. Root and GPUI application checks and native builds passed.
-
-With the final candidate, a fresh native 60-second GPUI recording with microphone and system audio stopped its capture pipelines in 110.45 ms and delivered the first editor frame in 923.81 ms. Playback from an explicit 30-second seek reported 300 frames at 60.0 fps with zero dropped frames. A native Tauri recording with the same track types, lasting 15 seconds, reported stop-command completion in 285.06 ms and a rendered-editor marker at 1,686.22 ms in its unoptimized debug build. The retained two-hour output also reopened in GPUI, sought explicitly to 7,194.5 seconds, and played for five seconds at 60.0 fps with zero dropped frames. These are individual native flow observations, not controlled percentage comparisons or p95 measurements. Evidence is in `optimization-stage-4/native-results.json`.
-
-The guarded candidate's retained two-hour media files, recording metadata and project configuration also matched the prior candidate byte for byte. See `optimization-stage-4/guarded-equivalence.json`.
-
-One native driver run initially reused the stage 3 GPUI stdout filename. The completed new log was moved to stage 4, and the original stage 3 events were recovered from the independently written daily diagnostic log. This recovered excerpt is named `optimization-stage-3/gpui-mic-system-60.diagnostic.log`; provenance and the distinction from the overwritten stdout stream are recorded in `optimization-stage-4/log-recovery-provenance.json`. Subsequent native log creation is exclusive. Recording media was unaffected.
-
-## Direct media experiment
-
-A separate two-hour fragmented MP4 was created from the replay's encoded video without re-encoding. Compressed video payload SHA-256 matched the progressive input:
-
-`b194eecf662b211a05c0f55724ad31a2425cb1d1d3dca172e4760add6234a95a`
-
-The production decoder was then opened directly on that file:
-
-| Decoder on macOS       | Probe and first frame | Seek at 3,600 seconds | Seek at 7,199 seconds |
-| ---------------------- | --------------------: | --------------------: | --------------------: |
-| AVAssetReader hardware |              440.4 ms |               46.6 ms |               25.6 ms |
-| FFmpeg hardware        |              163.9 ms |               51.7 ms |               21.8 ms |
-
-Neither required a post-recording remux for reading. This tests already-created media and existing decoders, not a new recorder, editor UI presentation, crash recovery, Windows Media Foundation, or Linux hardware/software decoding.
-
-The same fragmented two-hour file was also opened in the actual GPUI editor and played for five seconds. The app reported 301 rendered frames, 60.0 fps, and zero dropped frames. Audio output and the microphone waveform loaded. The initial `CAP_GPUI_AUTO_SEEK=0.999` run was incorrectly interpreted as a seek near the end of the recording: that driver targets the visible timeline viewport, which can show only the beginning of a long recording. It proves native playback but not native tail playback. The direct decoder and trim/export experiments use explicit timestamps and are unaffected by this correction.
-
-The corrected driver explicitly sought to 7,194.5 seconds, then played for five seconds in the native GPUI editor. Both the progressive output from the concurrent-hashing candidate and the fragmented media experiment reported 300 frames at 59.9 fps with zero dropped frames. Logs confirm the exact requested time. Evidence is in `optimization-stage-3/native-tail-results.json`; this verifies tail playback on macOS, not a two-hour native capture or a p95 latency.
-
-The current Rust exporter successfully rendered a three-second trim from the preserved-AAC candidate and another from 7,195–7,198 seconds of the fragmented two-hour project. Each output contained 90 H.264 frames and a three-second AAC track. Both outputs fully decoded without errors. This is short trim/export coverage, not a complete multi-hour export or visual/aural equivalence test.
-
-After the resampler change, both trims were exported again with the rebuilt exporter and identical settings. Full decoded video and audio SHA-256 hashes matched the preceding candidate's exports in both cases, with no decode errors. Evidence is in `optimization-stage-3/export-validation.json`.
-
-Full audio loading still took approximately 2.05 seconds and the process reached approximately 1.46 GB RSS. A trial using the existing range decoder failed a sample-count comparison near the end of the long replay. Range decoding is not enabled for recorded-track playback by this change. Timestamp discontinuities, AAC priming/padding, and resampling alignment must be resolved before introducing bounded audio loading.
-
-The two-second audio-window experiment took approximately 5 ms per window and used 384 KB of decoded samples, but middle and end windows did not match the full reference on the replay. The same experiment matched every sample at the beginning, middle and end of the original native 60-second AAC track, at 1.2–1.5 ms per window. This distinguishes feasibility from correctness across timestamp discontinuities; it is not a passing long-recording result.
-
-## Longer-term implementation direction
-
-These are design goals, not a claim that arbitrary indexed seeking, bounded total PCM storage or the full recording matrix has been implemented and verified.
-
-The accepted experience is to open the editor as soon as capture has safely stopped and make the beginning playable while remaining preparation continues. A subtle timeline boundary should represent the actual contiguous range ready across display, camera, microphone and system audio, accounting for track offsets. Progress must not be estimated from elapsed time. Playback reaching the boundary must wait without losing audio, advancing the media clock, or presenting incomplete output as complete. Seeking into an unprepared range should prioritize that range or show preparation while preserving the requested position. Full export and recovery requirements still apply.
-
-Produce durable, indexed, editor-readable media during capture. Keep the original encoded media in stable locations. On a clean stop, drain every requested track, flush the remaining media and event tails, commit the final index and project metadata, and open the editor directly on those originals. Whole-recording copying, hashing, transcoding, and remuxing must not remain prerequisites to editing.
-
-Use bounded audio decoding with a small look-ahead buffer and an index that preserves the current sample timeline. Seeking must work anywhere in a long recording without decoding everything before it. Failure to load requested audio must not become a silent successful playback result.
-
-Build cursor/keyboard indexes, thumbnails, and waveform data incrementally. The current stop path serializes complete cursor histories, so the media-only two-hour replay is insufficient to prove that Stop is independent of duration. GPU resources and the editor shell can be prepared during recording; this must be measured for capture contention and frame drops.
-
-Keep clean-stop publication separate from interrupted-recording recovery. A failed drain or incomplete tail must retain the existing failed/recoverable state. Old recording formats continue through their established reader and recovery paths. No crash-recovery validation is removed to obtain a faster timing.
-
-Background jobs must use immutable inputs and must not rename, replace, or delete media underneath an open editor or export. Closing or reopening the app must preserve both the recording and saved edits. Optional consolidation for external compatibility can operate on a separate output when required.
-
-## Progressive audio feasibility
-
-The existing sequential `AudioStream` decoder was compared with the current full `AudioData` decoder over the native 60-second AAC track, its legacy Ogg output, and the two-hour AAC replay. Every chunk's channel count, cumulative sample position and sample bits matched the full reference through EOF. The two-hour decoded SHA-256 remains `5c5c67688b348b9e4d0e54a42e5021c630a7ab8c49c078c892d620e8f7fcd8c9`.
-
-Across three runs, the median time to open and decode the first 250 ms was 0.61 ms for native AAC, 0.74 ms for legacy Ogg, and 4.43 ms for the two-hour AAC replay. The largest returned chunk was 48,000 bytes for these mono tracks. These are warm component observations: full decoding ran first in each process, and sample comparison/hash work is excluded from the accumulated streaming decode time. They do not measure the audio device, editor startup, cold storage, or a production progressive playback path. Evidence is in `progressive-audio-probe/results.json`.
-
-Use `CAP_BENCH_STREAM_AUDIO=1 CAP_BENCH_FULL_AUDIO_ONLY=1` with the audio readiness driver to reproduce this comparison. This sequential decoder avoids the timestamp-to-sample seek mismatch found by the earlier range experiment. It does not yet provide arbitrary seeking or a bounded editor cache. Existing export code also uses `AudioStream`; at this feasibility stage, editor playback still waited for whole-track `AudioLoader` completion.
-
-The same three fixtures were also tested on native Linux, three runs each. Every streamed sample matched the full decoder on that platform. Median first-chunk times were 0.82 ms for native AAC, 1.02 ms for legacy Ogg and 12.31 ms for the two-hour replay, with the same 48,000-byte maximum chunk size. These use the same warm component methodology and do not establish editor startup time. Decoded sample hashes differ between macOS and Linux builds; equivalence is checked against each platform's own full decoder. Linux evidence is in `daytona/long-audio-results.json`.
-
-## Direct fragmented-video decoding
-
-The FFmpeg directory reader previously read every fragment into one allocation, wrote a temporary combined file, and opened that file. It now owns the same seekable virtual input already used by remuxing. Opening retains paths, lengths and one active file handle instead of copying all encoded media. Custom I/O failures remain errors through packet reads and seeks, including when FFmpeg interprets a truncated input as EOF. The input context is destroyed before its custom I/O buffer and callback state.
-
-Shared video metadata loading also accepts fragment directories. On macOS, directories select the existing FFmpeg hardware decoder directly; ordinary files retain their existing AVAssetReader selection. Ordinary project readiness, failure and recovery guards are unchanged. At this stage, direct reading was available, but neither editor yet opened unfinished recordings or moved finalization into the background.
-
-Matched debug-profile decoder runs alternated old and new readers, three runs per variant and fixture. macOS tested both software and actual VideoToolbox decoding; Linux tested software decoding. Each run compared 30 decoded frames at the beginning, middle, near the end and after a backward seek. Visible pixel bytes, timestamps, frame counts and requested hardware selection matched in all 36 macOS runs and all 12 Linux runs. This is sampled frame equivalence, not a complete two-hour decode comparison. A separate generated multi-fragment recording test compares every returned frame and timestamp through EOF and after four seeks.
-
-| Platform and fixture                | Decoder      | Old first frame median | Direct first frame median | Old / direct peak RSS median |
-| ----------------------------------- | ------------ | ---------------------: | ------------------------: | ---------------------------: |
-| macOS, native screen 15 seconds     | Software     |              106.31 ms |                  64.93 ms |            124.86 / 73.60 MB |
-| macOS, native screen 15 seconds     | VideoToolbox |              135.26 ms |                 125.55 ms |           156.04 / 103.79 MB |
-| macOS, native microphone 60 seconds | Software     |              238.71 ms |                  69.34 ms |            205.52 / 71.55 MB |
-| macOS, native microphone 60 seconds | VideoToolbox |              269.64 ms |                 130.43 ms |           236.54 / 104.12 MB |
-| macOS, two-hour replay              | Software     |            1,916.36 ms |                 123.47 ms |          2,307.05 / 57.29 MB |
-| macOS, two-hour replay              | VideoToolbox |            1,463.52 ms |                 190.70 ms |          2,319.83 / 67.99 MB |
-| Linux, native screen 15 seconds     | Software     |              136.11 ms |                 105.14 ms |                 Not measured |
-| Linux, native microphone 60 seconds | Software     |              183.55 ms |                 109.91 ms |                 Not measured |
-
-Timing starts before decoder construction and ends at the first decoded frame, excluding frame hashing. Inputs are warm and builds finish before measurement; three runs do not establish p95 or cold-storage performance. Memory uses decimal MB. Linux two-hour video decoding and Linux hardware decoding were not measured in this comparison.
-
-With the final shared metadata/decoder integration, the GPUI media-readiness driver also read the original fragment directories directly. Metadata loading plus decoder setup plus first frame took 171.80 ms for the 15-second recording, 171.79 ms for the 60-second recording and 264.44 ms for the two-hour replay. All four seek requests returned frames in each case, and durations matched the finalized-file controls. These are single component runs, without an editor window, GPU composition, audio readiness or Stop. Ordinary finalized-file controls continued selecting AVAssetReader hardware decoding.
-
-Evidence is retained in `optimization-stage-5/macos-results.json`, `optimization-stage-5/macos-shared-media-results.json`, and `daytona/stage5/`, with build logs, source manifests and binary hashes. The matched decoder binaries precede the helper's public visibility change and shared metadata integration; the shared-media driver and final checks include those additions.
-
-Final macOS validation passed 114 encoder/remux tests, two decoder tests and 235 rendering tests. Linux passed 118 encoder/remux tests, two decoder tests and 209 rendering tests. Each rendering suite retained five existing ignored tests. Scoped checks and all-targets Clippy with denied warnings passed on both platforms using the repository's Rust 1.88 toolchain; the GPUI dependency check and shared-media driver build also passed using stable Rust. Linux initially lacked the Clippy component; it was installed before the successful run. A separate stable-Rust Clippy attempt reported newly introduced lints in five unchanged encoder tests, so that toolchain result is retained separately from the passing pinned-toolchain checks.
-
-The final macOS binary also finalized another two-hour disposable project in 13,609 ms. Its complete video, AAC audio, recording metadata and project configuration were byte-identical to the preceding guarded candidate. This confirms the shared reader ownership change preserves those outputs; it is a single regression run, not evidence of further finalization speedup. Evidence is in `optimization-stage-5/finalization-replay-2h.json`.
-
-## Required regression matrix
-
-Run the same fixture and native-action drivers in Tauri and GPUI on macOS Intel/Apple Silicon, Windows hardware/software decoder configurations, and Linux X11/Wayland with the supported capture and decoder paths.
-
-Cover short, 5-minute, 1-hour, 2-hour and 8-hour recordings; screen-only, webcam, microphone and system audio; pause/resume, device changes and dropped-input gaps; first and last frames, A/V sync, sample counts, trim/export/reopen; and repeated stop/start cycles.
-
-Inject interruption at media flush, index persistence and metadata publication. Also cover encoder errors, truncated media, a full disk, unavailable output storage, and app close/reopen while ancillary work is running. Originals and diagnostics must survive every failure. Exercise Windows file-sharing restrictions on actual Windows.
-
-Use matched release-equivalent builds, fixed visible capture content, identical settings, repeated cold and warm runs, and concurrent capture-load measurements. Record exact revisions and binary hashes. Any quality, correctness, recovery, or supported-platform regression blocks enabling the new path.
-
-## Daytona function validation
-
-macOS validation ran alongside the Daytona work using the local Apple Silicon build and locked dependencies with Rust stable 1.98.1. The first eight suites passed 864 tests with zero failures: encoder/remux (112), audio (42), recording (477), recovery integration (50), Instant Mode scenarios (61), editor (70), export (32) and GPUI recording/stop (20). There were three existing ignored tests. The project suite added 126 passes, and the A/V matrix added four passing test functions. The first matrix run reported 41 passing scenarios, but one random scenario skipped its video timing check after a 1.19-second runner stall; its audio check passed. A same-seed repeat also skipped that video check after a 1.10-second runner stall, with all other checks passing. Thus 40 scenarios have complete checks and one has audio-only verification. These skipped checks are not counted as verified video behavior; both reports remain retained. Full logs and commands are retained in `macos-function-validation/`. Platform-conditional tests account for differing macOS and Linux suite counts.
-
-The Linux candidate ran on Debian 13 x86_64 in a Daytona container with 4 vCPUs, 8 GiB RAM and a 10 GiB writable filesystem. The source archive contains HEAD `0c403be8a57f7ba4de67140273708a392c8b4050` plus the local candidate; every source file was hashed before upload. Tests used Rust 1.88.0, the committed dependency lockfile, and the repository's FFmpeg/native dependencies installed by `scripts/setup.js`. Debug symbols and incremental compilation were disabled to fit the sandbox. These are native Linux function tests, not UI, physical capture, or release latency tests.
-
-| Suite                                     | Passed | Failed | Ignored |
-| ----------------------------------------- | -----: | -----: | ------: |
-| Encoder/remux library                     |    116 |      0 |       0 |
-| Audio library                             |     42 |      0 |       0 |
-| Project library                           |    126 |      0 |       0 |
-| Recording library on container filesystem |    629 |      3 |       2 |
-| Recovery integration                      |     50 |      0 |       1 |
-| Instant Mode scenarios                    |     61 |      0 |       0 |
-| Editor library                            |     71 |      0 |       0 |
-| Export library                            |     32 |      0 |       0 |
-| A/V sync matrix tests                     |      4 |      0 |       0 |
-| Transactional recovery subset on tmpfs    |     28 |      0 |       0 |
-
-The A/V matrix reported 41 of 41 scenarios passing with no skipped scenarios. Do not add the tmpfs subset to the other rows as if it contained distinct tests.
-
-Three interrupted-publication/rollback cases failed in the candidate's initial recording-library run. Rebuilding with the five modified shared source files replaced by their unchanged HEAD versions also produced recovery failures, although the failing cases varied. Temporary diagnostic instrumentation captured a directory's reported size changing from 21 to 29 during rename; its entry names, file metadata and Unix identities matched. The existing receipt digest includes directory size, so reconciliation conservatively rejected the state and retained the files. The same candidate transactional group passed all 28 tests on tmpfs. No production guard was relaxed to obtain that pass. At that snapshot, the container-filesystem issue remained an inherited compatibility limitation, not a fully green suite or a reason to omit the failures. A later guarded recovery-stamp fix is recorded in the final verification section. Candidate source hashes were checked after both baseline comparison and temporary instrumentation were restored.
-
-The native 15-second screen and 60-second microphone recordings were then finalized on Linux using matched aggregate-input and virtual-input binaries. All other candidate optimizations stayed identical. Builds completed before measurement, the variant order alternated, and each fixture ran three times per variant. All 12 runs succeeded with byte-identical finalized video, audio, recording metadata and project configuration. Each retained original segment tree also matched its input hash manifest, and the source fixtures stayed unchanged.
-
-| Linux finalization fixture    | Aggregate input median | Virtual input median |
-| ----------------------------- | ---------------------: | -------------------: |
-| Native screen, 15 seconds     |              745.10 ms |            723.92 ms |
-| Native microphone, 60 seconds |            1,089.72 ms |          1,068.34 ms |
-
-These are modest observed component improvements on a container with warm inputs, using debug-profile builds with debug symbols disabled. Three runs do not establish p95 or a statistically reliable speedup. They exclude capture stop, editor rendering and playback, and do not measure Linux two-hour finalization. Full output hashes, binary hashes, logs and restored source hashes are retained in `daytona/finalization/`.
-
-Windows creation initially returned a tier-access denial through the API and authenticated dashboard despite the earlier support-confirmed grant. Access subsequently became available on September 10, and the user requested parallel validation on all three systems. The earlier errors and Linux function evidence remain under `daytona/`; the new Windows results are recorded below.
-
-## Shared fixture matrix and progressive audio, September 10
-
-Three platform agents ran concurrently. All used the same immutable unfinished `.cap` inputs, reconstructed from a common chunk archive and verified by path, byte length and SHA-256 before testing. Finalization operated on separate disposable copies.
-
-| Canonical input               | Files |         Bytes |
-| ----------------------------- | ----: | ------------: |
-| Native screen, 15 seconds     |    19 |    46,099,193 |
-| Native microphone, 60 seconds |    40 |   121,129,994 |
-| Two-hour encoded replay       | 3,388 | 2,396,017,929 |
-
-The final shared runtime candidate is `stage6-32768-track-mixer-v3`. Each platform verified all 4,208 source hashes in `platform-matrix-20260910/candidate-v3-manifest.json`. Its overlay archive SHA-256 is `4c6ca640b2879dbebde40d22cbee12c34e6b299345fbac2b9db600618969b61b`. Earlier v2 media runs remain applicable to the unchanged finalizer and decoder. V3 changes only defensive integer arithmetic in the mixer; affected suites and audio hash checks were rerun on every platform. Subsequent benchmark-driver, documentation and GPUI shell changes are separate from this frozen runtime identity.
-
-### Runtime changes and correctness
-
-Recorded audio now decodes sequentially into immutable blocks of 32,768 sample frames. Consumers can request a ready window while full-track consumers await a separate completion notification. Completion reuses the blocks without making another full PCM copy. Waveforms iterate those same samples in their original order. The normal playback path still awaits complete recorded audio, so these window results do not yet establish early audible playback.
-
-The common mixer now handles one track at a time, hoists unchanged gain/channel calculations out of the per-sample loop, and reads contiguous slices where available. It retains the original track accumulation order, offsets, stereo modes and clamp behavior. A comparison against the original algorithm covered 3,024 cases and 2,007,936 output bytes on macOS and Linux. All matched exactly. A separate extreme-cursor probe found an overflow in the slice fast path; v3 fixes it with checked arithmetic and retains the original silent result. The regression test runs on all three systems.
-
-The macOS codec-failure matrix compared empty files, header prefixes, half truncation, final-byte and final-1,024-byte truncation, and a payload-bit change for both native AAC and legacy Ogg. All 12 old/new cases agreed: six successful pairs with identical full decoded samples, and six matching failure pairs. Original inputs stayed unchanged.
-
-| Final v3 affected suites | Audio | Editor | Export | Failed |
-| ------------------------ | ----: | -----: | -----: | -----: |
-| macOS                    |    51 |     70 |     32 |      0 |
-| Linux                    |    51 |     71 |     32 |      0 |
-| Windows                  |    55 |     70 |     30 |      0 |
-
-Scoped checks and all-targets, no-dependency Clippy passed for the affected shared crates on all three systems. The Windows native matrix totals 1,187 distinct passing tests with zero failures and eight existing ignored tests, including unchanged v2 encoder, decoder, project, recording, recovery, Instant and rendering suites. Windows used Server 2025 x64, Rust 1.88, MSVC and the repository's native dependencies. These headless results do not test physical Windows capture, a GPU decoder or its desktop UI. The Linux dependency-lint and container-filesystem failures described above remain separate unresolved baseline limitations.
-
-### Matched audio benchmarks
-
-Both sides use the same current-thread Tokio runtime and include worker startup and completion notification in their decode timer. The baseline reproduces the old `spawn_blocking` plus `Arc`/watch loader. Runtime construction, full PCM hashing and mixing happen outside the decode timer. Each mixing repetition has a `black_box` barrier. The corrected shared evidence harness SHA-256 is `bf526600f10feccf2186d186309ffb6cea2d7b42ea0d96e7ef6e6c98c96082d1`.
-
-These are three alternating pairs using development builds with optimization level 2. macOS retains debug symbols; Linux and Windows omit them. Compare each platform against its own baseline. Older direct-main-thread baseline measurements and unoptimized Linux measurements are retained for provenance but are not substituted into this comparison.
-
-| Two-hour AAC component              |       macOS |       Linux |     Windows |
-| ----------------------------------- | ----------: | ----------: | ----------: |
-| Old background loader, entire track | 1,943.59 ms | 3,287.10 ms | 2,526.91 ms |
-| Block loader, entire track          | 1,956.39 ms | 3,241.20 ms | 2,432.86 ms |
-| First 250 ms of audio available     |     4.97 ms |    16.35 ms |    13.57 ms |
-
-Every paired full PCM count/hash and mixed-output hash matched within its platform. The entire macOS decode was 0.66% slower in this small sample. Peak process RSS was 1,422.43 MB before and 1,434.35 MB after, an observed increase of 11.93 MB. The roughly 1.38 GB decoded source still remains resident; this is not bounded-memory loading. A 48,000-frame block experiment increased RSS by about 45 MB and was rejected. The extra 11.93 MB peak RSS is an explicitly accepted tradeoff. The full-decode timing difference remains visible; neither result is presented as proof of zero regressions.
-
-On native AAC, the original renderer versus the new block renderer mixed 60 seconds in 7.20 versus 1.21 ms on macOS, over five alternating pairs. On Linux the equivalent two-hour-source experiment measured 28.25 versus 1.80 ms over three pairs. Both matched original PCM and mixed-output hashes. The macOS evidence embeds the original helper in the driver; Linux rebuilds the original function in the library, so their ratios are not cross-platform comparisons. With the same optimized renderer on both storage types, blocks have a small access cost: 1.14 versus 1.23 ms on macOS and 1.72 versus 1.83 ms on Linux.
-
-Windows ran all 18 audio comparisons over native AAC, legacy Ogg and the long AAC replay with exact full PCM and mixed-output matches. Its short full-load medians were 24.71 versus 24.11 ms for AAC and 68.53 versus 67.49 ms for Ogg; first decoded windows arrived at 3.42 and 2.96 ms. Its baseline library restores the original mixer body, matching the Linux original-renderer method. Mixing 60 seconds from the long track measured 25.67 versus 1.42 ms, outside the decode timer. Windows process peak memory and CPU were not collected; decoded-buffer size is not a substitute for those metrics.
-
-### Full project media checks
-
-The updated Linux matrix ran 18 paired decoder probes across all three inputs. Decoded frame pixels and timestamps, including middle and tail seeks, matched. First-frame medians for aggregate versus direct input were 137.30 versus 105.08 ms for 15 seconds, 193.86 versus 111.62 ms for 60 seconds, and 2,665.91 versus 112.35 ms for two hours. These builds used the root workspace's unoptimized development profile; they exclude stop, GPU rendering and presentation.
-
-Twelve paired Linux short-project finalizations succeeded with exact output and retained-source checks. The two-hour candidate finalized in 13,317.68 ms on Linux and 14,388.35 ms on macOS in single regression runs. Each retained all 3,386 original segment files exactly. Published video, audio, project configuration and recording metadata matched across these platforms. All public cursor images/events and keyboard files were included; unordered JSON object keys were compared semantically where serialization order differed. There is no new matched two-hour Linux finalization baseline in this matrix.
-
-The finalized two-hour recording also played in the native macOS GPUI editor after an explicit seek to 7,194.5 seconds: 300 frames over five seconds at 59.8 fps, with zero dropped, skipped or starved frames. Logs confirm the real MacBook Pro Speakers output and no mute override. Audible A/V offset was not independently measured.
-
-Windows passed all 18 paired software-decoder probes and all 14 finalization runs. It used a two-vCPU VM and the unoptimized root development profile. First-frame medians were 188.95 versus 93.14 ms for 15 seconds, 315.83 versus 96.61 ms for 60 seconds, and 7,188.98 versus 628.15 ms for two hours. The three long candidate observations ranged from 190.19 to 650.28 ms, so the median alone must not be used as a latency guarantee.
-
-Windows short finalization medians were 945.44 versus 926.54 ms for 15 seconds and 1,414.75 versus 1,247.67 ms for 60 seconds. Its single long pair measured 42,940.06 versus 38,389.64 ms. All paired published segment bytes and parsed metadata/configuration matched; original segment files and original metadata/configuration remained exact. Supplementary untimed checks also compared every public output against macOS, including cursor images and normalized metadata/configuration, for all three fixtures. The long result reinforces that finalization is still too slow to gate a responsive editor.
-
-Evidence is retained in `macos-validation-20260910/`, `daytona/linux-validation-20260910/` and `windows-validation-20260910/`.
-
-The next integration must preserve source reads across publication, gate editor writes during publication, and prevent the audio/video clock from advancing past unprepared samples. No source validation, finalization requirement or recovery guard has been removed to get the component timings above.
-
-### Rejected Windows preview ownership proposal
-
-An evidence-only source-directory lease synchronized file opens with directory relocation. All five standalone tests passed on macOS and Linux, but Windows passed four and rejected the held-handle relocation with `ERROR_ACCESS_DENIED (5)`. The current FFmpeg `AudioStream` likewise blocked parent-directory rename while open, continued to produce exact samples, and released the restriction when dropped. Explicit `FILE_SHARE_DELETE` did not fix the directory rename, although renaming an individual shared file worked.
-
-This agrees with Microsoft's documented restriction on renaming a directory containing open files. The replacement-file POSIX flag does not establish a workaround for that directory rule. See [FILE_RENAME_INFORMATION](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information). The prototype was never integrated into the application. Its failure rejects that proposed mechanism; it does not change the passing v3 media results. Production publication semantics remain unchanged.
-
-The next design must either keep preview sources in a stable location throughout publication or explicitly release every affected reader while preserving buffered playback and exact decoder state. A path redirect alone is insufficient. Evidence includes `optimization-stage-7/source-lease-probe.rs`, the per-platform results, and Windows `rename-probe.log`/`source-lease-tests.log`.
-
-A second standalone prototype registers cached file handles with the source owner. Before relocating the directory, it closes all registered handles under an exclusive source lock; subsequent reads reopen at the preserved byte position. The identical `revocable-source-probe.rs`, SHA-256 `d62a69df330dff6d1fba36bf37e453f429552422c7a5245efed85dc15f3f8008`, passed all seven tests on macOS, Windows and Linux with Rust 1.88, optimization level 2, overflow checks and denied unused results. Coverage includes four simultaneous readers through 600 moves, successful and failed publication, rollback, seek positions, and changed-length rejection. This supports the filesystem mechanism only. FFmpeg/AVIO integration, exact decoded-output comparisons, publication latency and playback buffering are still required.
-
-## GPUI early loading shell
-
-This early shell change opened the existing `Loading project...` editor after a successful, generation-matched capture acknowledgement. `Phase::Stopping` and its detached task still own finalization and shutdown. Linux first completes the existing capture-visibility restoration protocol. At this shell-only stage, preflight, media readers, editing and saving remained behind successful full finalization, including the existing thumbnail and project configuration work.
-
-Presentation is tracked by recording generation, finalization job and actual window ID. Closing a preparing editor suppresses automatic reopening; explicitly reopening it uses the same readiness gate. Completion races and window-creation failures retain the existing fallback. Failed tracks, low storage, Instant recordings, non-editor preferences and in-editor recording targets keep their existing paths.
-
-The review found and fixed a camera-preview cleanup omission in the initial shell change. Finalization completion now parks the feed even though it no longer reopens or refocuses the editor. GPUI's binary test target also exposed a Stage6 test compilation regression: the waveform refactor removed a constant still used by two tests. The constant is restored inside the test module; runtime audio remains unchanged.
-
-macOS GPUI package check and 128 targeted tests passed: recording 45, session 15, app windows 26, timeline 42. Native extraction harnesses ran the same relevant production function bodies with scaffolding on Linux (35 passed) and Windows (22 passed plus check). They cover readiness, dismissal/reopen, stop outcomes, terminal ownership and restoration acknowledgements. They do not execute a real GPUI observer, compositor or typed capture device. Full native GPUI checks outside macOS were still pending at this shell-only stage; later frozen app suites are recorded below.
-
-Strict stable-toolchain GPUI Clippy with tests reported 30 issues in other code regions, including new `chunks_exact` recommendations and existing documentation/argument-count issues. No warning was suppressed, and no passing full-application Clippy result is claimed. The scoped shared-runtime checks described above remain separate.
-
-The first native shell attempt caught a panic in newly added timing instrumentation: `request_animation_frame` is invalid outside GPUI's render phases. That run is rejected and its isolated source and logs retained. A disposable copy recovered successfully, preserving all 36 original source files exactly and producing a complete decodable video. The instrumentation now uses the supported `window.refresh` call.
-
-A fresh native macOS run captured a synthetic AppKit window for 60 seconds at 60 fps, then used the ordinary Stop and editor playback paths. Microphone and system audio capture were disabled. The binary SHA-256 is `c98f054a08334e029801e94d5de1e07915caf994434d2f820d69a54e0dee994b`.
-
-| Milestone after Stop           | Elapsed time |
-| ------------------------------ | -----------: |
-| Capture shutdown acknowledged  |     37.41 ms |
-| Editor shell created           |     49.68 ms |
-| First frame-cycle upper bound  |     62.60 ms |
-| Finalization completed         |    317.51 ms |
-| Project validated              |    323.68 ms |
-| Editor instance created        |  1,130.83 ms |
-| First recording frame received |  1,180.02 ms |
-
-Playback delivered 301 frames over five seconds at 60.1 fps with zero drops. Full video decoding passed, metadata and configuration parsed, and all 36 originals totaling 13,576,177 bytes were retained. The screenshot shows the existing loading editor with editing/export gated by `project_ready`; recording controls have closed.
-
-This is one native flow, not a matched old/new comparison or a p95 result. The two-frame callback gives a frame-cycle upper bound, not a compositor scanout timestamp. Default audio output was enabled, but this screen-only recording does not verify audible A/V synchronization. The early shell is visible quickly; playable content still waits for finalization and editor setup. Evidence and exact source hashes are in `optimization-stage-7/gpui-shell/implementation-manifest.json`, `native-stop-fixed/verification.json` and `interrupted-recovery-verification.json`.
-
-## Managed source input and publication
-
-The Stage8 input keeps FFmpeg decoder state while the finalizer moves original segments. It closes registered file handles under the source owner lock, then reopens them at their preserved byte positions. Each file retains its identity, length and modification-time checks; ordinary file readers retain their existing paths. Cancellation callbacks run outside that lock. This mechanism is still an optional API; the preparing editor does not use it yet.
-
-Frozen `native-io-v1` passed 190 native media-library tests on macOS, 194 on Linux and 190 on Windows, with scoped check and strict Clippy. Native AAC60, legacy Ogg60 and AAC2h matched every decoded sample through publication, failed moves and rollback. All three canonical video fixtures matched decoded pixels and timestamps, including tail seeks, while the managed decoder stayed alive. macOS included actual VideoToolbox; Windows and Linux used software decoding. Original canonical files remained byte-exact. These component tests do not establish synchronized preparing-editor playback.
-
-The first video timings had a cache-preparation asymmetry: the reference was hash-read before construction, while the disposable candidate copy was only hash-read afterward. Those results remain in the evidence but cannot isolate API overhead. Corrected probes hash-read both inputs outside the timer, use the same probe with both library versions, and balance which decoder opens first. Native hardware cold-start and warm-start samples remain separate.
-
-The v2 reader registers at most 64 files per lock acquisition, sharing repeated parent checks inside each batch while preserving every file snapshot and all reopen checks. On Linux with optimization level 2 and four balanced repetitions, corrected two-hour managed constructor-to-first-frame median improved from 194.21 to 163.22 ms. The ordinary control stayed at 113.43 versus 115.67 ms. Short managed medians were 100.30 versus 101.01 ms for 15 seconds and 108.28 versus 106.29 ms for 60 seconds. These are warm-input component timings, not Stop latency or a p95 result. The measured cancellation response after a separate thread requested cancellation was below 0.72 ms with batching, versus below 0.03 ms before batching.
-
-The optional managed finalizer changes only the original-segment move and its rollback. Its exclusive recovery lock refuses a pending publication receipt without reconciling it behind active readers. Existing publication checks, original hashes, staging, configuration and metadata commit order remain unchanged. Six new tests cover a live 61-second audio decoder through actual finalization, successful publication, every failed publication rename, failed rollback, the wrong project source and interrupted-publication handoff. At this stage, all preview decoder workers had to acknowledge shutdown before ordinary receipt reconciliation or the ordinary editor took ownership. The later successful-publication handoff adds an explicit continuation path for already-admitted readers. Native v2 validation is complete, with the platform limitations below retained.
-
-macOS v2 passed 260 native tests (64 recovery, 54 audio, 139 encoder and 3 video), scoped checks and strict all-target Clippy. Its 48 balanced constructor trials and six full video lifecycles included software and actual VideoToolbox. Full audio samples, video pixels/timestamps and all 3,447 canonical fixture files matched. The two-hour software managed constructor-to-first-frame median improved from 195.29 to 166.78 ms. VideoToolbox opened first improved from 270.41 to 248.19 ms; opened second improved from 206.60 to 165.71 ms. These hardware strata must remain separate.
-
-Linux v2 passed its audio, encoder and video suites; recovery passed 64/64 on tmpfs and 61/64 on the sandbox root filesystem. The root-filesystem failures are triggered by a pre-existing publication receipt guard: a directory allocation size changes from 21 to 29 bytes during rename while its identity, times and recursive children remain unchanged. The original repository HEAD has byte-identical guard functions, and its retained baseline also fails interrupted-publication cases on that filesystem. The new managed handoff test exposes the same guard. No check was relaxed at that stage; this was then an unresolved filesystem-specific recovery limitation, not a clean all-filesystem sign-off. The subsequent guarded stamp fix has its own native results below. Detailed provenance is in `managed-recovery-linux/receipt-guard-provenance.json`.
-
-Windows v2 passed 255 native tests (60 recovery, 58 audio, 131 encoder and 6 video), all four scoped crate checks, media all-target Clippy and recording library Clippy. Full-target recording Clippy hits an unchanged `items_after_test_module` finding in `capture_pipeline.rs`, verified byte-identical to HEAD. The three full video lifecycles and three full audio comparisons passed; the two-hour audio contained 345,675,776 exact samples. All canonical inputs and 4,212 frozen source files remained unchanged.
-
-Windows used one prepared copy per fixture, with both complete input trees hash-verified before and after each batch of eight fresh processes. The same probe and root dev profile (Rust 1.88 MSVC, optimization level 0, debug info 0) ran both runtime versions, balancing library and decoder-opening order. Four repetitions per variant gave managed constructor-to-first-frame medians of 115.52 to 92.21 ms (15 seconds), 130.84 to 113.90 ms (60 seconds), and 479.31 to 363.15 ms (two hours). The two-hour ordinary control varied from 217.18 to 246.64 ms, so cache and small-sample qualifications remain material. These numbers cannot be compared directly with macOS/Linux optimization-level-2 timings. Windows cancellation request-to-return was 0.696–0.911 ms with batching versus 0.048–0.057 ms before; the maximum callback interval was 1.898 ms. This measures input-constructor cancellation, not full worker shutdown. Evidence: `windows-validation-20260910/stage8-managed-recovery-v2/{terminal,prepared-timing-summary,cancellation-results}.json`.
-
-Evidence: `optimization-stage-8/native-io-v1-manifest.json`, `native-io-macos/summary.json`, `native-linux-io/`, `enc-input-v2/`, `managed-recovery-v2-manifest.json`, `managed-recovery-v2-macos/summary.json`, `managed-recovery-linux/linux-stage8-summary.json`, and the native Windows results. At that snapshot, the separate early-editor and progressive-playback contracts described remaining integration work; later sections record their implementation.
-
-### Editor startup diagnostic
-
-The startup driver can now profile stages in a fresh process before normal startup (`--profile-stages-only`) and separately measure font preparation (`--prewarm-fonts`). A missing first preview frame now fails the driver instead of contributing an infinite value that was excluded from summaries.
-
-One initial stage profile took 882.3 ms, including a remaining renderer-layer wait of 663.5 ms. A subsequent ordinary process took 118.3 ms without font prewarming. Four balanced fresh-process comparisons on the same finalized 60-second project measured constructor-to-first-preview medians of 137.15 ms ordinarily and 134.45 ms with fonts prepared beforehand; that separate font preparation took about 12 ms. OS and driver caches were not reset. This does not establish fonts as the initial delay's cause, so no production font-prewarm change was accepted. These measurements exclude Stop, finalization, UI presentation and playback. Evidence: `optimization-stage-8/font-startup/summary.json`.
-
-## Managed decoder worker and lifetime
-
-Stage9 adds an optional managed decoder worker around the shared existing FFmpeg frame-selection loop. Startup returns an owner before waiting for readiness. Cancellation wakes the worker; `stop_and_wait` acknowledges an actual native thread join after its decoder, custom input and cached frames are dropped. Managed decode/seek errors are terminal, and cancelled readiness or shutdown waiters retain cleanup ownership. Ordinary decoder entrypoints preserve their existing frame, cache, offset, hardware fallback and VFR hold behavior.
-
-`RelocatableSource::new_with_owner` retains an external lifetime guard through every source clone and reader. Reader field drop order closes cached files before releasing the last guard. Seven tests include Windows exclusive file opens inside the guard destructor, which would fail if handles remained open. Recording-owned recovery-lock binding was still pending at this stage; this generic source API did not connect the preparing editor by itself. Metadata extraction now exposes the same existing `Video::from_input` and `Audio::from_input` calculations for an already-open input, preserving ordinary path behavior.
-
-The frozen seven-file overlay is `optimization-stage-9/managed-worker-v1-overlay.tar.gz`, SHA-256 `7845c712628f9ad99f8d9bc882bc14e3c1bf1d2539f9c1d4e863bd4b958a18a2`. Both Cargo locks were regenerated by Cargo with no package version, source or checksum drift; only rendering dependency edges changed.
-
-- macOS: 518 tests passed, five existing rendering tests ignored; formatting, six scoped crate checks and strict all-target Clippy passed. The actual worker matched ordinary software and VideoToolbox pixels through a three-second PTS gap, a 0.25-second handle offset, repeated publication and forward/backward/tail/cache requests. Logs confirm hardware was actually used.
-- Windows: 486 tests passed, five existing rendering tests ignored; scoped checks, media/rendering all-target Clippy and recording library Clippy passed. All owner-lifetime, interruptible-constructor and managed-worker cases passed, including the Windows exclusive-sharing teardown assertion. The inherited recording all-target lint remains documented above. Source and canonical fixture hashes match; the owned VM was stopped with caches and evidence preserved.
-- Linux: 431 media/worker tests passed, five existing rendering tests ignored; scoped check and strict all-target Clippy passed. Recovery remained 61/64 on the root filesystem and 64/64 on tmpfs, with the same three documented pre-existing receipt-guard cases. Native Rust formatting also passed after installing the pinned component. Final source and binary provenance is retained in `optimization-stage-9/native-linux/linux-stage9-summary.json`; both owned Linux sandboxes are stopped with their files and caches preserved.
-
-The first missing-fragment test used an incorrect hard-coded target: 9.5 seconds was intact media, while macOS's deleted last fragment began at 9.8 seconds. That failure is retained. The corrected test derives its target from the packet's physical fragment position, PTS and actual renderer frame mapping, and independently asserts the underlying ENOENT/EIO. It then checks terminal decode/seek failure, the same error on a later frame-zero request, and the shutdown acknowledgement. Linux's encoded fragment boundary differs, validating the need for packet-based targeting.
-
-Evidence: `optimization-stage-9/native-macos/summary.json`, `windows-validation-20260910/stage9-summary.json`, `optimization-stage-9/native-linux/`, and `optimization-stage-9/missing-fragment/`. This stage verifies functions and worker lifetimes; it adds no new Stop-to-editor latency or p95 claim. The recording-owned source observer, restricted editor projection and synchronized progressive playback remain to be connected and validated.
-
-## Recording-owned preparing sources
-
-Stage10 adds a private, single-use clean-stop receipt and a retained observer for read-only source access during finalization. Receipt eligibility is bounded and does not change persisted recording data or make a successful Stop fail. The projection checks the exact stopped metadata/configuration, fragment inventory, required tracks, diagnostics and cursor assets. Unsupported layouts fall back to ordinary finalization. Its source readers retain the real recovery lock, follow owned publication/rollback moves, and are revoked when finalization ends. In the Stage10 snapshot, already-running readers still had to be cancelled, joined and dropped before ordinary reconciliation.
-
-The production application did not consume this observer in the initial Stage10 snapshot. The full initial source snapshot, staging, validation, source rechecks, synchronization and transactional publication remain in their existing order. Moving availability ahead of the first snapshot would permit previewing one version of a source while finalization accepts a later version; that reorder was rejected during review. Existing renderer construction also has configuration-loading fallbacks that cannot be used by a restricted preparing preview.
-
-Frozen v1 native validation used identical source hashes and canonical recordings:
-
-| Platform | Recording tests                                 | Native canonical preparing coverage                                                                                                                            |
-| -------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| macOS    | 90 recovery and 8 receipt tests passed          | All four video requests on each short fixture; all 2,883,584 microphone frames on the 60-second fixture matched exactly                                        |
-| Windows  | 85 recovery and 8 receipt tests passed          | All four video requests on each short fixture; the same complete microphone sample count matched exactly                                                       |
-| Linux    | 90 recovery and 8 receipt tests passed on tmpfs | Two video requests on the 15-second fixture and three on the 60-second fixture before finalization ended; the complete microphone sample count matched exactly |
-
-Every observed video frame was compared against an independently finalized MP4 through the same software decoder. Final public media bytes, normalized metadata/configuration, retained originals and canonical inputs matched. The actual recovery lock could be reacquired after worker teardown. Linux's remaining video requests were cancelled at finalization completion and are recorded as unobserved, not passed early-preview coverage. These fixed-order functional probes do not measure Stop-to-editor latency, audible playback, speedup or p95.
-
-Scoped compiler and formatting checks passed. macOS passed strict recording Clippy across all targets and a standalone GPUI check. Windows retained its inherited all-target lint qualification while the scoped strict gates passed. Linux strict library/tests Clippy passed; its all-target example failure is byte-identical to HEAD. On the Linux sandbox's regular root filesystem, five existing interrupted-publication receipt cases failed, while the identical binary passed all 90 recovery tests on tmpfs. The exact test bodies and receipt guards are byte-identical to HEAD; this run does not establish a new five-failure baseline or remove that platform qualification.
-
-The original two-hour canonical recording finalized successfully on all three systems, preserving its sources and producing all four finalized-file reference frames. It then declined the test receipt before preparing playback began: its explicit empty `cursors` object deserializes as the legacy cursor representation. A live Stop holds the current representation and its writer omits that empty field. The v1 failure and zero long preparing coverage remain recorded. Test-only v2 removes that one empty field through the actual writer on both disposable comparison copies, verifies unchanged typed metadata/configuration and every media/sidecar byte, and retains a separate hash-linked legacy control. Four new tests passed natively on each platform; they verify the transformation and reject nonempty legacy cursors or unknown fields without writes. No production validation predicate changed.
-
-The normalized two-hour v2 probes passed on all three platforms. Each compared all four requested video seeks (0, 3600, 7194.5 and 1 seconds) against an independently finalized MP4 and all 345,675,776 audio frames bit-for-bit through EOF. Published media, normalized documents, retained originals and canonical hashes matched. Actual worker joins, source release and recovery-lock reacquisition passed. macOS and Linux also produced identical visible pixel hashes and published media/document bytes.
-
-These unoptimized, ordinary-first functional runs are not performance comparisons: source availability was observed at 15,592.17 ms on macOS, 948.55 ms on Linux and 1,546.92 ms on Windows; first returned video frames were at 16,141.58, 1,145.54 and 2,580.97 ms respectively. Cache state, hardware and concurrent preview work differ. No rendered editor frame, audible synchronization, product Stop latency or p95 is established. The full source snapshot still precedes availability; removing that protection has not been accepted.
-
-V2 evidence is retained in `optimization-stage-10/native-macos/assessment-v2-long.json`, `optimization-stage-10/native-linux-v2/assessment-v2-long.json`, and `windows-validation-20260910/stage10/summary-v2.json`. Both Windows test executables and the macOS/Linux v2 executables are retained. The Windows archive contains 1,032 size/hash-verified members, and its owned sandbox is stopped with source, fixtures, outputs and cache preserved.
-
-Evidence: `optimization-stage-10/native-macos/assessment-v1.json`, `windows-validation-20260910/stage10/`, `optimization-stage-10/native-linux/linux-stage10-v1-summary.json`, `optimization-stage-10/canonical-probe-v2/`, and `optimization-stage-10/early-availability-review/`. The original macOS v1 test executable was overwritten by its test-only rebuild; its binary hash, dependency hashes, source snapshot and raw results remain, but that exact executable was not separately retained.
-
-## Owned preparing frame controller
-
-Stage11 adds a separate, read-only frame controller to `cap-editor`. It returns a lifecycle owner before setup awaits; a retained stop handle waits for its actual Tokio task and all native decoder threads. The controller reuses ordinary segment timing, cursor smoothing, zoom uniforms, frame layout and immediate render methods. It never constructs an ordinary `EditorInstance` or loads/writes project configuration. Unsupported presentation, unresolved calibration offsets or inconsistent metadata decline preparation.
-
-Requests are coalesced into one latest pending frame with one decode/render in flight. Lazy initialization is cancelled only when the entire preview stops; an obsolete scrub cannot cancel its decoder. Completed render output is checked for frame number, dimensions and request sequence before delivery. The ordinary three-attempt render retry policy is retained, with ordinary `get_frames` behavior on retry. Source/decoder/cursor errors remain terminal. At most two display/camera segment pairs stay open; eviction waits for actual teardown, and late failures are retained rather than lost with an evicted worker.
-
-Frozen cursor assets share immutable image bytes and retain the ordinary SVG/PNG priority and image-format selection. Existing file loaders delegate to the same new cursor-reader and keyboard-byte parsers; no ordinary parsing or fallback order changed. Keyboard-enabled presentation and camera background blur currently require ordinary loading. The Stage11 validated bundle has no app consumer; the subsequent Stage12 integration is described separately below.
-
-The corrected common v2 candidate passed the native scoped gates on all three systems:
-
-| Platform       | Editor | Project | Rendering | Total passed | Existing ignored |
-| -------------- | -----: | ------: | --------: | -----------: | ---------------: |
-| macOS arm64    |     82 |     130 |       266 |          478 |                5 |
-| Linux x86_64   |     83 |     130 |       239 |          452 |                5 |
-| Windows x86_64 |     82 |     129 |       239 |          450 |                5 |
-
-Formatting, scoped checks and strict all-target Clippy passed. macOS scoped checks also include Tauri and GPUI. The project/rendering results carry forward from v1 because all runtime sources are byte-identical; v2 changes only the native fixture duration and reruns the complete editor suite on every system. Linux uses Mesa llvmpipe through Vulkan; a separate probe of the same production adapter factory selected Microsoft Basic Render Driver through DX12 on Windows. This is software-sandbox coverage on Windows/Linux, not proof across physical GPUs.
-
-The four-second v1 fixture failed before rendering on Linux and Windows: their libx264 encoder produced two fragments, below the test's required three. V2 generates three existing encoder GOPs (currently six seconds), preserving every assertion and requested frame time. All four composed cases then executed and passed on all three systems. The original failures remain recorded. Clippy also caught a test fixture initialization style issue; it was corrected and rerun. Controller tests caught a readiness notification race that could mask the original initialization error with cancellation; the final implementation retains the actual error.
-
-Four composed native tests compare visible RGBA pixels and frame layouts against independently finalized ordinary media. They cover PNG/SVG cursors with explicitly changing cursor samples, nonidentity zoom, independently encoded display/camera with distinct FPS and timing offsets, forward/backward/tail seeks, source relocation, three-clip eviction/reopening and a callback panic followed by native joins and source-owner release. Four additional deterministic native-thread tests cover failure while idle or after a returned frame and dropped terminal waiters, with release barriers proving both joins. These synthetic component tests do not measure Stop latency, audio synchronization, physical multi-clip capture, UI presentation or p95. Hardware decoding is requested but not asserted by the controller test API; the composed comparison uses RGBA. The original shared `.cap` fixtures remain unchanged.
-
-The frozen 15-file v2 bundle is `optimization-stage-11/preparing-controller-v2-overlay.tar.gz`, SHA-256 `78114380b3fba19df63a50e5a177f8c66fbbd1a1fc29397084e6a829e3382e60`; its manifest is `ef1b93c9a4c9c87b5b71a955a504a71d950a4c6dc8fa86bde44b931b6ab0e26b`. Evidence: `optimization-stage-11/native-combined-macos-v2/assessment.json`, `optimization-stage-11/native-linux/assessment-v2.json`, `windows-validation-20260910/stage11/`, `native-preview-macos/summary.json`, and `managed-terminal-tests/`. Exact v1/v2 editor executables are preserved. Stage11's helper compares collected visible bytes, dimensions and layout, but equal truncation of both buffers is not independently excluded by a byte-count assertion. No truncation was observed. Stage12 adds explicit complete-buffer length and stride assertions.
-
-## GPUI preparing image integration
-
-Stage12 connects the recording-owned observer to a GPUI loading window. The consumer requests the composed first image, checks the current recording attempt and window identity before delivery, and retains actual task and native decoder completion across close/reopen. Ordinary project loading waits for every retained consumer for that project. Closing a preview does not cancel recording finalization. The image remains read-only: playback, editing and export still require the ordinary ready editor. Progressive synchronized playback and a playable-range UI remain unfinished.
-
-The GPUI adapter preserves stopped configuration and uses the same initial clip-calibration calculation as ordinary loading. Current camera blur and selected animated-gradient preferences decline this early image. Keyboard presentation still declines; merely recording a keyboard sidecar does not, because GPUI ordinary rendering requires an explicitly configured keyboard overlay. The raw files remain available unchanged to ordinary loading and later user actions.
-
-The frozen Stage12 shared-core native matrix is complete, with the Linux filesystem qualification below. These scoped function and synthetic composed-frame tests exclude app consumers. Platform-specific tests account for the different totals; each platform also retains one ignored manual canonical probe.
-
-| Native platform                           | Editor | Recovery | Receipt | Passing total |
-| ----------------------------------------- | -----: | -------: | ------: | ------------: |
-| macOS                                     |     88 |      101 |       8 |           197 |
-| Linux, same recording executable on tmpfs |     87 |      101 |       8 |           196 |
-| Windows                                   |     86 |       96 |       8 |           190 |
-
-Linux recovery on the regular container filesystem passed 98 tests and failed three with `Conflicting recovery publication state; all files retained`; the same executable passed all 101 on tmpfs. Two failing test bodies and the relevant receipt guards are byte-identical to repository HEAD. The third test was introduced during Stage8 and already exposed the same rootfs limitation there; it is not an inherited HEAD test. The failure names and exact provenance are retained in `native-linux/receipt-source-provenance.json`. This remains a qualified result, not a clean Linux rootfs gate.
-
-Scoped format/check gates passed on all three platforms. Strict recording Clippy passed for lib/tests on macOS and Linux, and lib on Windows; editor all-target Clippy passed on all three. Seven new public-finalizer paired cases cover output/error parity and observed preview admission or decline, and three initial-calibration tests passed. Native composed cases include cursor assets, camera, zoom, relocation, three-clip eviction/reopen, keyboard-sidecar behavior and actual cleanup after callback panic. Full dimensions, stride and buffer length are checked before pixel comparison. macOS also asserts the ordinary AVAssetReader backend and actual BGRA surface parity. Linux ran the cases on software llvmpipe/Vulkan; there is no discrete Windows GPU claim.
-
-All 17 frozen overlay members, 26 Linux runtime members and 82 Windows evidence archive members were independently verified against their SHA256/size manifests, along with the exact retained macOS editor executable. Windows canonical fixtures matched before/after; Linux's three retained audio components matched, and its canonical media sandbox was not resumed. Linux and Windows build VMs were stopped with artifacts retained. Evidence and exact executable/source identities are aggregated in `.git/stop-editor-20260909/optimization-stage-12/native-summary.json`. The later Tauri dependency entry and renderer diagnostics are separate from this frozen source.
-
-On macOS, 39 targeted GPUI lifecycle, finalization, presentation and capture-stop tests also passed. The app compiles with the current stable toolchain. Strict GPUI all-target Clippy reports 28 unique inherited findings: each diagnostic context was checked against HEAD `0c403be8a57f7ba4de67140273708a392c8b4050`. This is a qualified gate, not a clean whole-app Clippy result. Evidence is under `native-gpui-macos/` and `gpui-consumer-review/clippy-provenance.json`.
-
-Four actual GPUI runs used matched builds in baseline/candidate/candidate/baseline order, each recording the same synthetic window scenario for about 60 seconds at 2560×1504. Each row is a distinct recording. Times below are milliseconds from the app's Stop marker; the ordinary-frame column records receipt by the ordinary editor.
-
-| Run          | Capture stopped | Loading shell | Early composed image | Next app cycle for early image | Finalization finished | Ordinary first frame |
-| ------------ | --------------: | ------------: | -------------------: | -----------------------------: | --------------------: | -------------------: |
-| 01 baseline  |            37.9 |          50.5 |                    — |                              — |                 300.3 |              1,067.7 |
-| 02 candidate |            46.1 |          60.6 |       No early image |                              — |                 298.0 |                878.1 |
-| 03 candidate |            43.7 |          58.7 |                185.3 |                          190.4 |                 287.0 |                332.2 |
-| 04 baseline  |            49.2 |          61.6 |                    — |                              — |                 291.9 |                383.2 |
-
-Only one of two candidate runs produced an early image. In run02, sources were available at 64.0 ms, but finalization ended before renderer readiness. The ordinary path proceeded after retained preview cleanup and preflight, reaching its validation marker at 814.5 ms; that interval is not an isolated cleanup measurement. Run03 produced the image at 185.3 ms and reached the next app cycle at 190.4 ms. The app-cycle marker does not independently prove external compositor presentation. Different initialization costs and only two observations per variant prevent p95, significance or universal speedup claims. No two-hour app timing or audio synchronization was measured.
-
-All four runs finalized, sought and played for about five seconds with zero dropped frames in the app logs. Complete post-run decoding compared every published frame and PTS against that run's retained original fragments: all 14,380 matched. Cursor/keyboard sidecars remained byte-identical, and all 192 retained project files were rehashed against the audit manifests. Configuration changes were limited to ordinary duration calibration (at most 4.5 microseconds in these runs); metadata changed only completion status and display path. The audit compares each recording to its own original, not media across runs. Raw results and matched-source/binary provenance are in `gpui-native-stop/summary.json`.
-
-The Tauri consumer is now applied. Its earlier complete native library run passed 446 tests; after the presentation extraction, nine new configuration/adapter-guard tests passed and the 61-test recording scope passed, including five of those nine. These counts must not be added as independent runs or described as a latest full 455-test run. Tauri Rust format/check and strict lib/tests Clippy passed, two command bindings were generated, six frontend files passed Biome, ten lease/socket tests passed, and desktop TypeScript checking passed. Tauri conservatively declines recorded camera and raw keyboard inputs: ordinary Tauri loading automatically creates keyboard presentation from raw events. The new parity tests establish configuration and guard behavior; subsequent genuine Tauri capture evidence and its remaining handoff issue are described below.
-
-A subsequent Tauri change stops an abandoned admission after its response receiver disappears, joining the owned transport without waiting for or cancelling recording finalization. Its ten-test native consumer scope passed, including the new pending-finalization case; formatting, scoped check and strict lib/tests Clippy also passed. The 15-file consumer snapshot is retained in `tauri-applied-v2/manifest.json`. These scoped runs overlap earlier tests.
-
-The opt-in native Tauri pixel/DOM capture harness subsequently passed formatting, scoped check and strict lib/tests Clippy. Its exact retained macOS library executable passed all 459 tests, including three frame-capture/fixture-selection tests. The first launch failed before tests because the nested artifact directory lacked its required Frameworks path; an owned Frameworks symlink fixed the layout, and the same executable then passed. This is harness and library validation; the two native WebView diagnostics below follow this suite. The fresh static frontend build succeeded with all 326 inspected source/config hashes unchanged, and its 693-file public output is retained. Evidence is in `tauri-native-preparation/harness/native-v2.json` and `tauri-native-preparation/frontend-result.json`.
-
-Two subsequent native Tauri diagnostic captures, 15 and 120 seconds, finalized successfully. All 895 and 7,169 decoded frames respectively matched each run's own retained source fragments in pixels and PTS; project/source trees were unchanged during verification. Both captured ordinary RGBA frame zero, but neither produced preparing RGBA or a visible preparing canvas. The parity verifier correctly reports incomplete. DOM observations therefore do not establish a seamless preview handoff. These unoptimized debug runs are functional diagnostics, not release latency benchmarks. The private log override passed its three native tests and scoped binary check/strict Clippy; normal log paths are unchanged. Evidence: `tauri-native-run/verification-matrix.json` and both `untimed*/benchmark.json` results.
-
-Source tracing explains the missing Tauri admission: `clean_capture::prepare` returns `None` on macOS/Windows, but the new preparing finalizer required the resulting Linux-only generation to be present. A correction giving actual recording finalizations their own checked, monotonically allocated identity is now applied; formatting, scoped check, strict Tauri lib/tests Clippy and all 461 native library tests passed, including allocation, duplicate, exhaustion and stale/recovery admission cases. The updated native executable then passed a genuine 120-second capture: preparing and ordinary frame zero matched all 3,358,368 visible RGBA bytes, dimensions and layout, and both canvases were observed visible. All 7,157 published frames and PTS matched the retained original fragments; all 66 original files were retained and the project tree stayed unchanged during audit. Generic crash-recovery requests, genuine clean-stop receipts, source ownership, current-attempt checks and failure handling remain guarded. Evidence is in `tauri-native-run/untimed-120s-v2/`.
-
-The corrected capture observed a live preparing canvas, then its retained image, then the ordinary canvas without a sampled blank state. A remaining size transient prevents a seamless-handoff claim: the preparing image measured 787.11 by 462 CSS pixels, the first ordinary image grew to 803 by 471.31, and it settled back after about 103 ms. The first visible preparing sample was approximately 725.7 ms after Stop; the benchmark ordinary-frame marker was 1,606.7 ms. These are unoptimized, instrumented functional observations, not release or p95 results, and DOM samples do not independently establish compositor presentation. The 569 native source, 326 frontend source/config and 693 embedded asset hashes remained unchanged through this capture.
-
-A further 120-second native layout diagnostic reproduced the transient and established its immediate cause. At first ordinary visibility, the actual container was already 835 by 494 pixels and all parent, toolbar and timeline geometry was at its final size. The accepted canvas alone retained 803 by 471.31 dimensions for 109 ms before settling to 787.11 by 462. The preparing/ordinary frame-zero pixels matched exactly again; all 7,171 published frames and PTS matched their original fragments, and all 26,930,830 original source bytes were retained. Native/frontend/asset hashes and owned-process shutdown passed. The additional bounded DOM geometry instrumentation makes this an untimed functional diagnostic. Evidence: `tauri-native-run/native-v3-assessment.json` and `untimed-120s-layout-v3/layout-analysis.json`.
-
-The first sizing fix passed 17 related frontend tests and desktop TypeScript, but its actual 120-second native v4 capture still showed the same 109 ms size transient. It therefore failed the intended visual outcome. Preparing and ordinary frame-zero pixels matched all 3,358,368 visible bytes; all 7,092 published video frames and PTS matched their retained originals, and all 26,559,920 original bytes remained unchanged. Source, binary, embedded assets and shutdown checks passed. The next refinement keeps initial geometry eager through one animation-frame remeasurement, preserving later 100 ms resize debouncing and cancelling pending work on teardown. Its subsequent native verification is recorded below. Evidence: `tauri-native-run/native-v4-assessment.json` and `tauri-initial-bounds-fix/first-paint-refinement/`.
-
-The refined v5 candidate passed 21 related frontend tests, desktop TypeScript and the private static/native builds. Its genuine 120-second capture used identical preparing and first ordinary bounds of 787.109375 by 462 pixels, with no later canvas-size transition in the following 1,001 ms of bounded DOM observations. Frame-zero RGBA parity passed. All 7,068 published video frames and PTS matched retained originals, all 26,426,138 original source bytes remained intact, the project tree stayed unchanged during verification, and owned processes shut down after finalization. This establishes the intended sizing outcome for this native case; it remains an unoptimized, instrumented functional check, with no audio/camera or external compositor/p95 claim. Evidence: `tauri-native-run/untimed-120s-bounds-v5/layout-analysis.json`, `parity.json` and `verification.json`.
-
-Fresh Windows validation of the current 1,356-file source freeze passed 770 native tests, with six existing/manual tests ignored. The native adapter was Microsoft Basic Render Driver through DX12. Scoped checks and strict encoder/audio/video/project/rendering/editor Clippy passed. Recording library Clippy passed; its test target retains an inherited `items_after_test_module` finding in a file byte-identical to HEAD. Both canonical 15-second screen and 60-second microphone probes passed: four video requests per recording matched independently finalized media, all 2,883,584 microphone PCM frames matched bit-for-bit through EOF, outputs/documents matched, originals stayed unchanged, and source release plus lock reacquisition passed. These are opt0 function diagnostics, not app latency or p95 evidence. Tauri compilation encountered compiler allocation failures with two jobs; the one-job native library check and test executable build later passed, but the storage reserve prevented that test execution. Expanded two-hour validation is paused under the narrower editor goal. Evidence: `platform-capacity/windows-fresh/current-shared-short-assessment.json`.
-
-Fresh Linux validation of the same source freeze passed 782 unique native tests, with the same six existing/manual tests ignored. The adapter was Mesa llvmpipe through Vulkan. Scoped formatting, checks and strict Clippy passed. All 101 recovery cases passed on the default root filesystem and again with the same binary on tmpfs; the duplicate run is excluded from the total. All three previously failing filesystem cases explicitly passed in both scopes. An initial receipt filter matched no tests and is retained as non-evidence; the corrected filter passed all eight intended cases. The 73-file evidence archive was downloaded and every member verified. Canonical integration runs remain separate from these unit fixtures. Evidence: `platform-capacity/linux-fresh/native-evidence/assessment.json`.
-
-The current Linux standard canonical 15-second and 60-second probes subsequently passed publication, retained-source, cancellation, actual-join and recovery-lock checks. All 2,883,584 microphone PCM frames matched bit-for-bit through EOF. Preparing video coverage was partial: finalization ended after two of four requested screen seeks and three of four microphone-recording seeks. Every returned frame matched its independently finalized software reference; the remaining requests were cancelled. No delay was added to force those requests to complete, and these results do not establish four-seek canonical parity. Evidence: `platform-capacity/linux-fresh/canonical-short-evidence/assessment.json`.
-
-A separate eight-file renderer diagnostic patch was applied after the frozen native matrix and matched GPUI runs. Its format/check, strict rendering all-target Clippy and 15 preparing-preview tests passed on macOS. A fresh 15-second native capture reached the ordinary first frame at 388.02 ms and played 300 frames at 60.0 fps with zero reported drops. All 895 decoded frames and PTS matched its retained originals; sidecars and source hashes were preserved. It produced no early image: finalization ended at 236.94 ms while preparing layer construction continued until 319.72 ms.
-
-That diagnostic measured 83.74 ms for managed decoder readiness, 4.09 ms for graphics constants and 176.42 ms for layer construction. The largest layer costs were the shared compositing pipeline (70.77 ms), first font-system scan (54.38 ms) and image layer (32.84 ms). These nested, instrumented timings identify work to investigate; they are not independent hardware timings or a matched speedup. No rendering phases occurred before Stop in this process. The exact diagnostic binary and 564 source hashes are retained under `renderer-readiness-diagnostics/summary.json`. No font warmup or initialization optimization has been accepted, and these later diagnostics do not change the provenance of the four reported timings. The instant, playable-editor goal is still in progress.
-
-An experimental follow-up omits the image/text/caption/keyboard layer bundle only for the checked preparing renderer. Existing ordinary constructors retain the complete bundle and original preparation/render order. Unsupported project or prepared-text inputs produce an explicit error before restricted rendering. On macOS, all 88 editor and 269 rendering library tests passed, with five existing rendering tests ignored; scoped checks also covered export and Tauri, and strict editor/rendering all-target Clippy passed after removing four unused imports from the extraction. Two additional native tests now pass on Apple M4 Max/Metal: ordinary image, text, caption and keyboard output remains visible, full/restricted frame-zero pixels and layout match, and late unsupported inputs leave the GPU target unchanged. Seven existing native preparing-preview tests also passed. The first boundary-test attempt used a segmented-text-only inspection method on ordinary batched text; that test assertion was corrected while retaining its rendered-pixel check, and both attempts are preserved under `preparing-overlay-boundary/native-macos/`. A fresh matched comparison is recorded below. No cross-platform or speedup acceptance is inferred from this local experiment; skipping early initialization can defer costs to ordinary loading.
-
-A fresh GPUI comparison isolates the optional overlay bundle change: both variants already include the preparing consumer and renderer diagnostics. Both opt2 executables were built from the same isolated source paths, with equal dependency profiles, features, locks, external inputs and assets. Only the documented four-path renderer/editor delta differs. Post-matrix source, binary and harness verification passed before any replay-test additions; an unused Tauri scaffold change in the original checkout is separately recorded and excluded from the GPUI dependency graph.
-
-| Fresh run                 | Early composed image | Early image app cycle | Recording finished | Ordinary first frame |
-| ------------------------- | -------------------: | --------------------: | -----------------: | -------------------: |
-| 01 full layers            |       No early image |                     — |           295.6 ms |             368.0 ms |
-| 02 omitted overlay layers |       No early image |                     — |           282.4 ms |             849.7 ms |
-| 03 omitted overlay layers |             177.3 ms |              183.3 ms |           295.5 ms |             349.8 ms |
-| 04 full layers            |             184.3 ms |              190.4 ms |           298.1 ms |             340.6 ms |
-
-The 849.7 ms observation remains part of the result. Its preparing layer construction took 523.6 ms, dominated by several GPU pipeline constructors; none of the omitted overlay constructors ran. Finalization safely relocated the source and ended the job, but ordinary loading waited for the retained preparing task to return from synchronous construction and join its native workers. Ordinary validation occurred at 681.3 ms. The logs establish this ordering, not the precise worker-exit timestamp or shader-cache causality. Both variants completed five seconds of ordinary playback with zero reported dropped frames. All 14,379 published frames and PTS matched their respective retained original fragments (3,595, 3,594, 3,595 and 3,595 frames); sidecars and all project trees remained unchanged during verification. Separate same-source composition replays subsequently passed for all four recordings on Apple M4 Max/Metal with FFmpeg hardware decoding: every visible pixel, dimensions, stride and layout matched the ordinary full renderer at frame zero. Decoder workers joined, and project/source trees stayed unchanged. These replays use a retained pinned Rust 1.88 opt0 test executable, separately qualified from the stable opt2 measured apps. The initial nonmember-workspace failure and paused rebuild are retained; neither changes the measured binaries or their completed source audit. These recordings contain no cursor motion, so motion coverage belongs to the earlier dedicated native tests. This four-observation matrix establishes neither a reliable speedup nor p95. Exact phases and provenance are under `preparing-overlay-benchmark/phase-assessment.json` and `source-after-matrix.json`.
-
-A separate optimized macOS diagnostic over the immutable two-hour canonical input completed successfully. It uses the public preparing path, a test-only clean-stop receipt on a validated writer-normalized copy, and the current restricted preparing renderer. The exact three runtime source hashes match its retained build manifest; the earlier description of this binary as using full layers was incorrect. T0 precedes finalizer worker spawn; source-copy/hash/projection preflight warms the input before T0. It does not execute native recorder shutdown, app presentation or audible playback.
-
-| Two-hour diagnostic milestone        | Time from T0 |
-| ------------------------------------ | -----------: |
-| Preparing sources observed available |   1,166.0 ms |
-| First composed RGBA frame            |   2,443.6 ms |
-| Preparing finalization returned      |  12,441.6 ms |
-| Preview native workers joined        |  12,444.2 ms |
-
-All first-frame pixels and layout matched the ordinary finalized-media compositor; published media bytes and normalized documents matched the ordinary run. Canonical originals, source/native/binary identities and lock availability after joins were verified. This diagnostic did not repeat PCM decoding; complete PCM comparison belongs to the earlier Stage10 evidence. Its subsequent ordinary finalization measured 11,560.5 ms, but this one ordered pair, with different concurrent work and warmed state, is not a matched no-regression performance result.
-
-Initial analysis consumed 397.7 ms, including 307.8 ms for 3,383 fragment-duration probes, all returning None. Initial whole-source snapshotting consumed 676.2 ms; hash groups contained 1.273 and 1.123 GB, so gross group imbalance does not explain the delay. Projection took 91.4 ms, primarily repeated completeness checks. After availability, managed decoder startup took 604.8 ms, graphics constants 91.4 ms, retained preparing layers 567.0 ms and RGBA output 12.1 ms. Nested/overlapping phases must not be summed as isolated costs. The next optimizations must preserve the existing integrity checks and duration fallback behavior. Raw common-origin phases, exact build qualification and results are in `two-hour-composed-diagnostic/isolated-preparation/evidence/two-hour-run-v2/assessment.json`. This single optimized function diagnostic establishes neither actual two-hour Stop/UI latency nor p95, and the instant/playable-editor goal remains incomplete.
-
-## Drivers
-
-From `apps/desktop-gpui`, build the drivers with `cargo build --example studio-finalization-benchmark --example media-readiness-benchmark --example audio-readiness-benchmark`. The finalization driver takes a disposable unfinished Studio project; it mutates that copy. The readiness drivers take media paths and are read-only. The audio driver's default mode reproduces the old background loader; `CAP_BENCH_PROGRESSIVE_AUDIO=1` selects the block loader. Both emit their execution and timing methodology. `CAP_BENCH_MIX_AUDIO=1` adds a current-renderer comparison with a barrier after every repetition. Hashing and mixing remain outside the decode timer.
-
-`CAP_BENCH_FULL_AUDIO_ONLY=1` skips the range experiment. Without it, the driver exits unsuccessfully if decoded ranges differ from the full-track reference. `CAP_BENCH_STREAM_AUDIO=1` also checks every sequential chunk against that reference. Preserve failed range experiments as failures; the known two-hour timestamp-seeking mismatch must not be mistaken for passing progressive-window coverage. Progressive windows use sequential sample positions instead.
-
-The debug Tauri driver requires the explicit arguments `--stop-editor-benchmark --allow-real-recording`, both `CAP_STOP_EDITOR_BENCHMARK_OUTPUT` and `CAP_STOP_BENCH_LOG_DIR`, and a private app identifier beginning `so.cap.desktop.stop-editor-benchmark.` with a nonempty suffix. Output and log paths must be fresh private siblings under an existing canonical directory. `CAP_STOP_BENCH_SECONDS` selects 1–86,400 seconds (default 15); `CAP_STOP_BENCH_MIC` and `CAP_STOP_BENCH_SYSTEM_AUDIO` opt into audio. Environment variables alone cannot activate recording. Keep the private identifier and recordings directory isolated from the user library. Its injected editor observer waits for a completed render in the existing frame statistics, a visible editor canvas, and two animation frames. A timeout is a failed benchmark, not a successful editor-open measurement. The initial observer only watched worker messages and missed Tauri's direct-canvas renderer; that incomplete run is excluded.
-
-Existing GPUI drivers include `CAP_GPUI_AUTO_RECORD=studio:60`, `CAP_GPUI_AUTO_NO_MIC=1`, `CAP_GPUI_AUTO_SYSTEM_AUDIO=1`, `CAP_GPUI_AUTO_EDITOR`, and `CAP_GPUI_AUTO_PLAYBACK`. `CAP_GPUI_AUTO_SEEK` targets a fraction of the visible timeline viewport. The new `CAP_GPUI_AUTO_SEEK_TIME=7194.5` targets seconds into the recording and logs the clamped time passed to the ordinary seek method. Use explicit time for long-recording tail checks. Keep app data and recordings isolated from the user's library.
-
-## Preparing playback integration on 2026-09-10
-
-The active goal was narrowed and resumed through the app: immediate editor presentation, equivalent early footage, actual playable-range feedback, and retained preview/playhead/playing intent through ordinary-editor handoff. Main was subsequently synchronized to `284ae24a79bd0813743f51e6219d11ae0042d38f`, preserving the local candidate and all evidence. Neither the goal nor native screenshot acceptance is complete.
-
-The shared preparing controller uses actual composed frames and completed required audio to admit a contiguous playable range. It freezes at that range, retains the latest seek response, cancels abandoned audio-start requests, and joins media workers before handing a take-once completed PCM cache to the ordinary loader. Required audio that cannot be proven equivalent keeps playback unavailable. Cached metadata permits only the finalizer's existing clearing of audio gap summaries; it cannot relabel paths, devices or start times.
-
-The Tauri backend now exposes window/epoch/job-scoped state and playback commands, publishes actual status changes, and includes the joined snapshot in the ordinary editor response. Closing a window releases preparing audio after cleanup. A replacement preparing request retires the previous session after its join. The frontend owns resuming ordinary playback after its target frame is actually drawn; that integration and user-intent supersession are still being validated.
-
-Tauri audio admission uses the ordinary timing-repair calculation against the projected final metadata and an actual bounded recording-log snapshot. Only cleanly stopped audio writers can reach this path. A missing log matches ordinary loading; unreadable, changing, nonregular, invalid UTF-8 or over-4-MiB logs conservatively defer required audio to ordinary loading. Preserved M4A requires the same AAC/no-video probe as finalization; transcodes and changed filenames are not admitted. These conservative limits must not be presented as universal early audible playback.
-
-The latest combined macOS editor library suite passed all 116 tests, including full PCM equality through gap-cleared handoff, corrupt-audio refusal, composed image equality, source release, rename and cancellation of an old playback handle without stopping its replacement. Current scoped Tauri compilation and all 16 preparing adapter/audio/lifecycle tests passed, as did 14 tests selected by the finalization filter. Shared editor strict all-target Clippy passed. The filtered runs overlap; their counts must not be summed into a unique platform total. GPUI scoped compilation and 165 focused preparing, recording, session, window and timeline tests passed. The final Tauri frontend passed 86 tests across eight files and desktop TypeScript. Scoped Biome passed for the initial 25 handwritten files and the six-file transcript/clip correction. Those panels now use the same ownership queue; recording/import waits for an acknowledged pause and declines a superseded request. Guarded handoff starts return a receipt for their exact playback handle; late cleanup cannot stop a newer ordinary playback. The queue retains ownership during failure cleanup and transfers ownership after successful handoff.
-
-Windows passed 68 native audio tests with the shared 60-second fixture: 64 shared tests plus four existing Windows latency tests. All decoded samples and EOF matched. The older wrapper expected 64 and flagged the count despite the native executable exiting successfully; that wrapper result and the corrected source-qualified count are both retained.
-
-The initial Linux/Windows screenshot builds use a retained source-v1 snapshot plus an explicitly recorded Clone correction and required real build assets. They predate the final controller integration. Linux Tauri completed an actual 15-second X11 capture and opened its ordinary editor; that short run did not present a preparing frame. The first external desktop-video recorder failed because it inherited the app's native-library search path. A corrected run produced a verified actual desktop video and editor screenshot; the user accepted Linux and requested no further Linux screenshots. Linux GPUI also built successfully from its qualified seed source. Both owned Linux VMs are stopped with source, media and build evidence retained.
-
-Windows GPUI genuinely opens the preparing editor. Its capture also exposed a persistent outline matching the hidden main window, and a white/washed-out preview. The initial rendering failures are retained; their runtime-layout cause and corrected results are recorded below. An independently invoked native export-preview reproduced washed-out output before GPUI presentation and then exited with an access violation during or after teardown; its generated JPEG does not make that diagnostic successful. The original recording media and project remain unchanged. A fresh real Stop reproduced the outline with 772 native trace records: the main window was hidden 26 seconds before editor creation, with no later show event, move/resize loop or held mouse button at editor creation. Show/hide clears the artifact diagnostically, but that does not establish a production fix.
-
-The integrated macOS GPUI build uses a separate 2,326-file hash-verified snapshot because concurrent crop changes invalidated the preceding shared-checkout build's provenance. That preceding binary was retained and never launched. A genuine 60-second capture opened the shell on the next presentation cycle at 88 ms. Finalization finished at 199 ms before preparing rendering returned a frame, so the early-play hook did not run. Ordinary playback rendered 299 frames over five seconds at 59.8 fps with zero drops. This proves immediate chrome and ordinary fallback for that run, not playback during processing. All 3,594 decoded frames and PTS matched retained fragments, cursor/keyboard sidecars matched byte-for-byte and the full project tree remained unchanged during verification.
-
-The new Tauri frontend and native builds are source- and asset-hash qualified. The initial native launch exited before recording because newly merged startup code honors the shared GPUI preference even for a private Tauri identifier. It preserved the user's running GPUI. An explicit debug Stop-benchmark-only branch now skips that startup selector and its shared marker handling; release behavior and user settings are unchanged. The failed launch remains recorded.
-
-The subsequent actual 120-second Tauri run preserved all 7,131 decoded frames and PTS, sidecars and 19,106,595 bytes of original media. Stop returned at 172 ms and the ordinary frontend frame event arrived at 1,549 ms. Actual UI Space playback and native screenshots confirmed about 60 fps, with 603 rendered frames, zero skips and no starvation in the observed interval. Preparing correctly declined because recorded clicks require late ordinary auto-zoom settings; this is ordinary fallback proof, not early-frame or playback-handoff proof.
-
-The final 86-test frontend rebuilt in 26.5 seconds with all 345 source hashes unchanged and 696 retained assets. Its native build also includes the explicit handoff start-frame selection after awaited music loading, so late old playhead events cannot change the approved target. A genuine 15-second Stop smoke on that build returned at 194 ms and delivered the ordinary frontend frame event at 577 ms. Actual full playback and replay from the end passed; the replay screenshot shows 0:03.01. All 899 published decoded frames and PTS matched retained originals, and the source/project audit passed. These single debug runs use different recordings and are not a performance comparison.
-
-A broader Tauri library run passed 468 tests and failed the existing upload scheduling assertion at 78.3 ms versus its 50 ms threshold. The entire upload file is byte-identical to current main. The isolated recheck through Cargo passed without source or threshold changes. A preceding direct-binary recheck lacked Cargo's runtime library paths and executed no tests; its loader failure is retained. The final serial run passed all 469 tests in 6.64 seconds. These concurrency and runtime-path qualifications remain with the broad gate.
-
-Windows Tauri's first launch failed the private logger's canonical-path guard; the next exposed a frontendDist Windows-drive URL interpretation issue. Both failures are retained. A relative asset path to the same 693 verified seed assets fixed the configuration; the unchanged-source native rebuild passed in 90 seconds. Its genuine 59.59-second recording opened the editor and actual Play advanced to 0:27.56 before pausing. The captured region was covered by the GPUI editor, so the recording contains that window. The native desktop video retains both the initial disabled editor chrome and the completed editor. Finalization took about 397 ms and the ordinary preview arrived about 3.8 seconds after editor document start; no preparing frame was observed. This remains a seed-build run, not validation of the latest progressive handoff. Current Windows function validation is being refreshed against the final common source manifest; the refreshed Linux results follow below. Storage, software-GPU, build-profile and source-version limitations remain explicit; no screenshot is a release performance result or a zero-regression guarantee.
-
-Evidence is under `optimization-stage-13/preparing-playback-combined`, `tauri-playback-integration`, `tauri-audio-admission`, `gpui-playback-integration`, and `linux-native-ui`, with Windows artifacts under `optimization-stage-12/windows-native-ui`.
-
-The standalone absolute-color diagnostic uses the exact existing native rendering libraries and does not edit production source. On both macOS and Windows, known RGBA bars survive GPU upload/readback, identity compositing and full composition exactly at all 24 sampled interiors. CPU YUV conversion matches the scalar reference; GPU conversion differs by at most one channel unit. The exact Windows recording software-decodes and composites in normal color on both systems, with actual native workers joined. A second diagnostic loads the original project configuration and isolates shadow and screen motion blur: all four variants render in normal color on both platforms, at the same 1168-by-702 output geometry as the failed export. Native CPU YUV and scalar RGBA outputs match byte-for-byte. These probes explicitly force software decoding and omit real cursor sidecars and recording timing metadata; the ordinary Windows decoder fallback is still under investigation. Earlier preparing-versus-ordinary equality alone cannot rule out both paths sharing a color bug. Evidence: `optimization-stage-13/windows-color-diagnostic/`, `windows-color-diagnostic-v4/`, and `optimization-stage-12/windows-native-ui/color-probe-v3/` and `color-probe-v4/`.
-
-The outline's scoped native diagnostic replays also remain separate from product verification. A temporary hook on the task-owned GPUI main thread invoked the actual content-affinity setter and nonclient activation pair; both succeeded, preserved hidden state and produced byte-identical before/after desktop images. The hook was removed after each operation. The native affinity getter succeeded and confirmed zero. Hidden-window paint and forced GPUI repaint messages also failed to reproduce the border. These negative results do not justify a visibility, theme or affinity patch; the real editor-opening transition remains under investigation.
-
-The final Linux v3 source snapshot verified all 2,331 file hashes before and after native validation. The shared library scope passed 531 unique tests: audio 63, rendering 244, editor 115, recovery 101 and receipt 8; five existing rendering tests stayed ignored. The final Tauri library executable passed 528 tests with zero failures, filtering only the binding-writing export test to preserve frozen source. This gives 1,059 unique shared and Tauri library passes, excluding overlapping filtered runs and the separate native fixture cases. The same recovery binary also passed all 101 tests on tmpfs; that duplicate run is not added to the total.
-
-Linux's matched 15- and 60-second canonical runs compared every observed video frame against independently finalized software-decoded media. Actual end-of-stream cancelled outstanding later seek requests: the observed requests were 0 and 7 seconds on the short fixture, and 0, 30 and 58 seconds on the longer fixture. All 2,883,584 audio frames matched bit-for-bit through EOF. Required worker joins, source release, output/configuration checks and all 59 original files (167,229,187 bytes) passed. The actual renderer selected llvmpipe through Vulkan. These are native function results, not additional UI screenshots or physical-GPU coverage.
-
-Scoped Linux compilation and strict shared Clippy passed. Tauri strict Clippy exposed one equivalent optional-value condition, rewritten with `is_none_or`; macOS formatting and scoped Tauri compilation then passed, as did the current Linux scoped Tauri check. The remaining three Tauri Clippy warnings are in the upload file, whose full bytes match current HEAD. Initial storage failures and reserve stops are retained separately from source/test failures. The final native Tauri build succeeded after audited removal of obsolete or duplicated owned compiler artifacts. Evidence: `optimization-stage-13/current-linux-functions/` and `native-current-v2/`.
-
-The next color probe kept managed decoders alive through rendering, then awaited their actual joins. On macOS, software YUV420p and native VideoToolbox NV12 produced identical corresponding composed output. On Windows, both requested hardware settings resolved to FFmpeg software decoding; both produced identical normal-color output and clean joins. A further ordinary-factory diagnostic creates the renderer before the decoder, matching export initialization order. On macOS, actual AVAssetReader texture-backed NV12 and the FFmpeg VideoToolbox path again produce identical corresponding PNGs. Ordinary handles expose no join API, so those runs record handle drop without claiming a worker join. Their initial standalone dynamic-library loader failures are retained; actual tests passed with the verified native-library search path. All these probes still omit actual cursor sidecars and recording timing metadata. Evidence: `windows-color-diagnostic-v5/`, `windows-color-diagnostic-v6/` and the Windows `color-probe-v5/` directory.
-
-The additional same-thread Windows affinity replay verified successful hidden-window transitions from 0 to 17 to 0, with an unconditional final restore to 0 and successful removal of every temporary hook. All desktop captures remained byte-identical. Repeated hides and genuine reopen-from-Recents also stayed clear. Those probes used an expanded main window and do not rule out the original compact recording-to-editor sequence. The visible outline was still unresolved at this probe stage; the later Main-frame module and its source-qualified native checks are recorded below.
-
-The compact visible-affinity sequence later reproduced the exact outline: enabling exclusion while visible, hiding, then repeated hidden clears changed exactly 3,048 pixels along the original client rectangle. A matched comparison with `DwmFlush` remained clear in its immediate captures, but a later capture showed the outline again. This leaves delayed composition and inspection ordering unresolved. The same run exposed a safety issue with a proposed read-before-write guard: the hidden getter returned success and zero, while the subsequently visible window still reported exclusion enabled. That guard was rejected before any production edit. Removing the new early clear also needs an early-close check because the preparing editor can re-show the protected main window before finalization reaches Idle.
-
-The actual Windows exporter was run again on two disposable copies differing only in `cursor.hide`. Both native processes exited successfully, but both images were incorrect: the original remained washed out, while the hidden-cursor variant had white and green/magenta corruption. Hiding the cursor does not establish or fix the cause. The ordinary-factory v6 diagnostic then rendered normal output on Windows with both default MediaFoundation-failure-to-FFmpeg fallback and forced FFmpeg. Both resolved to software YUV420p and produced equal decoded/output bytes. The full-library v7 probe uses actual recording metadata and sidecars through `cap_export::preview::render_preview`; both macOS and Windows processes passed and rendered normal color, with equal corresponding outputs for both decoder preferences. All input and disposable-project bytes remained unchanged. Evidence: `same-thread-window-probe-v3/`, `same-thread-window-probe-v4/`, `gpui-export-cursor-ab-v3/`, `color-probe-v6/`, `color-probe-v7/`, and `optimization-stage-13/windows-color-diagnostic-v7/`.
-
-The shader runtime layout explains the color discrepancy. The production Windows renderer selects bundled DXC only when both `dxcompiler.dll` and `dxil.dll` are beside the executable, or beside its parent when the executable directory is named `deps`. The retained test executables, standalone GPUI output and retained CLI directory lacked that pair; they loaded FXC. The successful v7 diagnostic had the pair and loaded DXC. Copying the exact verified runtime pair beside the unchanged executables corrected the isolated overlay, cursor/ripple and editor PNG tests. Full corrected rendering passed 244 tests with five existing ignored tests; full corrected editor passed all 114 tests. Together with audio 67, recovery 96 and receipts 8, this gives 529 unique current Windows shared library passes. All 2,331 frozen source hashes and retained executable hashes remained unchanged. The original two rendering and five editor failures remain recorded as runtime-layout failures, rather than being erased or weakened. The actual adapter is Microsoft Basic Render Driver through DX12, not a physical GPU. Evidence: `optimization-stage-13/native-current-windows-v3/`.
-
-The same runtime correction restored full-color native GPUI playback in the unchanged app. Module receipts and the app log confirm the exact adjacent DXC/DXIL libraries were loaded. Real Play/Pause advanced the original footage from 0:37 through 1:04. The retained core-playback snapshot reports 3,489 rendered frames, one skipped frame and zero starvation; GPUI paint submission on this software desktop was about 4.6 fps, so this is not a visible 60 fps claim. That ordinary reopen was outline-free because the main window had been shown and hidden during reopening. It does not prove the Stop outline is fixed. The unchanged CLI also returns a correctly colored JPEG after the runtime correction, but still exits with `0xc0000005`; that diagnostic remains failed. Evidence: `optimization-stage-12/windows-native-ui/runtime-module-audit-v1/`, `runtime-module-audit-v2/` and `gpui-dxc-playback-v1/`.
-
-A further real 19.017-second Stop reproduced the outline using only real input, desktop video and a subsequent desktop capture, with no window-state queries, hooks or DWM probes during the flow. Capture stop took 81.8 ms and remux 134.1 ms; the 837 captured frames and originals were retained. These are single diagnostic observations, not performance comparisons. Separate compact-window replays reproduced the outline after either one actual hidden affinity clear or two back-to-back clears. Duplicate-clear removal alone is therefore not a demonstrated fix. No production window-style, visibility or affinity change has been applied. Evidence: `gpui-native-v7/`, `same-thread-window-probe-v6a/` and `same-thread-window-probe-v7b/`.
-
-An additional native GPUI Stop with DXC loaded retained the same desktop outline over correctly colored footage. App logs confirm a manual Stop and a validated 23.767-second recording with 1,030 frames; the initially observed 0:04 was the paused playhead, not automatic recording termination. Capture shutdown took 78.94 ms and remux 152.86 ms. A subsequent hidden-Main `SetWindowPos` using `SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE` returned success, but the before, 600 ms and six-second desktop captures remained byte-identical. A frame refresh alone does not fix the outline. The outline extends outside the video preview on the actual desktop; it is not merely a border painted into the displayed footage. Evidence: `gpui-dxc-stop-v1/` and `hidden-frame-changed-v1/`.
-
-The current Windows canonical 15- and 60-second cases subsequently passed all eight requested video comparisons against independently finalized software references. All 2,883,584 microphone audio frames matched bit-for-bit through EOF; the separate managed-audio EOF test also passed. Actual worker joins, source release, lock reacquisition, outputs, configuration and original-file audits passed. The 2,331 source hashes and retained test executables remained unchanged. These are functional equivalence results; they do not turn the unresolved outline or CLI exit failure into passes. Windows Tauri scoped compilation and strict library/test Clippy subsequently passed; native library test execution remains separate under the recorded storage limit. Evidence: `optimization-stage-13/native-current-windows-v3/`.
-
-The requested upstream Zed review is pinned to `86b2cf96623a12ae3f524c3780b6bcce4443ed23`. Its complete inspected Windows backend has no capture-affinity calls or `SW_HIDE`; application hide is empty, while minimize and destruction use separate paths. Cap's local GPUI working patch adds native caption/edge styles and custom client insets absent from both current upstream and the July 19 fork ancestor. Both default to DirectComposition. These findings identify a Cap-specific frame/lifecycle difference, not an upstream workaround already proven for this recording flow. Primary sources: [Zed window styles](https://github.com/zed-industries/zed/blob/86b2cf96623a12ae3f524c3780b6bcce4443ed23/crates/gpui_windows/src/window.rs#L471-L494) and [Windows application hide](https://github.com/zed-industries/zed/blob/86b2cf96623a12ae3f524c3780b6bcce4443ed23/crates/gpui_windows/src/platform.rs#L592-L599). Exact source snapshots, hashes, comparisons and the unapplied diagnostic-only frame patch are under `optimization-stage-13/zed-upstream-comparison/`.
-
-A fresh native run with the existing `GPUI_DISABLE_DIRECT_COMPOSITION=1` setting initially appeared outline-free. The later desktop capture instead showed the entire hidden main client region as a solid white rectangle. Floating controls also lost transparency, producing black rectangles in this recording. This alternative is rejected: it changes the artifact and introduces visible capture differences. It does not fix the settled result, and no production rendering mode was changed.
-
-The matched fresh default process restored normal composition and reproduced the outline. A subsequent reversible test on only the idle hidden Main cleared `WS_CAPTION`; Windows automatically cleared only the associated `WS_EX_WINDOWEDGE` bit. With a frame refresh, the outline disappeared while visibility, bounds and composition stayed unchanged. Restoring the exact original styles restored the outline. Both cleared captures matched each other byte-for-byte; the before and both restored captures also matched byte-for-byte. The first attempt stopped on that native coupled-style change and restored the baseline before a corrected diagnostic explicitly recognized it. This proves the visible artifact depends on the native caption/edge representation, but does not yet prove its trigger or justify changing visible-window appearance. No product source was changed. Evidence: `optimization-stage-12/windows-native-ui/hidden-frame-style-v1/` and `hidden-frame-style-v2/`.
-
-Current Windows scoped compilation for the shared audio, rendering, editor and recording crates passed. Strict library/test Clippy retained a Windows-only `items_after_test_module` finding in `capture_pipeline.rs`, whose full bytes match HEAD and the frozen manifest (`fdee8cb040177fe55cda42e7b595361d6becb87b955e5c8836ba1e0e99c07e7a`). It is recorded as an inherited lint qualification; the file and lint threshold were left unchanged. Tauri scoped compilation and strict library/test Clippy also passed. The native Tauri test run remains a separate gate.
-
-A bounded native API observer then captured an actual 21.550-second GPUI recording and manual Stop that reproduced the outline. Its 11 committed records contained eight `DefWindowProcW(WM_NCACTIVATE)` calls before recording start and none at Stop. Their common caller and varying message parameters do not identify them as Cap’s synthetic theme redraw pair. All three affinity writes requested zero: one at start and two at Stop. The active Virtual Display Driver explains the existing streamed-display exclusion bypass. The two Stop clears were about 0.870 ms apart. Successful API returns with stale last-error values were not treated as failures. The observer’s warmed disposable-window test preserved return values and last-error behavior; teardown restored exact import pointers and protections with zero dropped or in-flight records. Its inert pinned module was removed by safely exiting the owned idle process. This rules out an observed direct theme redraw at Stop in this run; no speculative theme guard was applied. Evidence: `optimization-stage-12/windows-native-ui/api-observer-gpui-v2/`.
-
-A subsequent hidden-Main nonclient policy experiment returned `S_OK` for `DWMNCRP_DISABLED` and removed the outline. Immediate and six-second captures matched byte-for-byte. Restoring `DWMNCRP_USEWINDOWSTYLE` returned `S_OK`, re-enabled nonclient rendering and restored a capture byte-identical to the baseline. Process/window identity, styles, extended styles, visibility, iconic state and outer bounds remained exact across all captures; client bounds were not recorded in this probe. The later restored capture differs and is retained. The original raw policy enum was not readable: restoring `USEWINDOWSTYLE` follows the source-default inference, and the owned idle process subsequently exited to clear diagnostic state. This proves the policy can remove the observed artifact without changing window styles. It does not yet validate automatic hide/show handling, visible appearance or a production fix. Evidence: `optimization-stage-12/windows-native-ui/hidden-nc-policy-v1/`.
-
-The resulting local GPUI candidate installs a native subclass on only the Main window. It restores the normal nonclient policy before show-related default handling, reconciles actual visibility after `WM_WINDOWPOSCHANGED`, and removes its subclass before forwarding destruction. It retains styles, bounds, capture-affinity calls and default message handling. Review removed a premature post-`WM_SHOWWINDOW` reconciliation and added show restoration before forwarding `WM_WINDOWPOSCHANGED` when earlier notification is suppressed. The shared GPUI fork is unchanged. The exact Windows module is `623fd5404020c0eeb63038bf29fd2922cad205826ee39a97dda25662e3f2f082`; macOS GPUI workspace formatting and scoped compilation passed with source/lock hashes unchanged. Those checks exclude Windows code paths; exact-module native Windows validation is pending. Evidence: `optimization-stage-13/windows-hidden-frame-production/`.
-
-The current Windows Tauri test binary initially failed before the Rust harness with loader status `0xc0000139`. Its missing Common Controls activation manifest caused this fixture-runtime failure. An external manifest did not resolve it. A retained derivative with the exact `tauri-build` application manifest embedded by Microsoft’s resource tool launched successfully; all original executable code/data sections and the untouched original executable were retained and verified. That run passed 437 tests and failed eight at the same closed-listener timeout helper. Four independently bound-and-closed Windows loopback ports correctly returned `WSAECONNREFUSED` after 2.003–2.019 seconds, beyond the helper’s two-second deadline; a fresh-process targeted rerun reproduced the timeout. The resulting test-only change allows five seconds on Windows, retains two seconds elsewhere, and preserves the required connection failure and retry logic. It changes no production listener behavior. Root workspace formatting and Tauri compilation passed with the edited source hash unchanged. Native targeted and full-suite reruns are pending; original loader and timeout failures remain retained. Evidence: `optimization-stage-13/native-current-windows-v3/loopback-report.json` and `frame-ws-test-deadline/`.
-
-The Windows Tauri test-only v4 rerun subsequently passed all eight formerly failing cases individually and all 445 library tests in the full suite, with only binding generation filtered. Combined with the 529 shared tests, this is 974 unique Windows library passes; the eight canonical video comparisons and 2,883,584 PCM EOF frames are separate integration evidence. All 2,331 source hashes in the frozen shared/Tauri v4 scope remained exact. This scope does not include the later GPUI Main-frame candidate, whose native checks are separate. No production behavior or assertion condition was changed to obtain this result.
-
-The exact GPUI Main-frame module passed a disposable native Windows test covering already-hidden and visible installation, duplicate installation, wrong-thread rejection, minimize/restore/maximize/move/resize, outer/client rectangle and style parity, forwarded message results/last-error state, and subclass removal before forwarding destruction. Explicit `SW_SHOWNORMAL` and `SWP_NOSENDCHANGING` cases confirmed restoration before underlying show handling. The initial standalone executable lacked Common Controls activation and failed before `main`; the retained v2 used the exact existing GPUI application manifest, compiled without diagnostics and passed. The production GPUI dependency already enables that manifest, so no application manifest change is required. Actual GPUI recording/Stop testing of this exact module is the next gate. Evidence: `optimization-stage-13/windows-hidden-frame-harness-v2/native-smoke-v2/`.
-
-The first actual GPUI recording with the exact Main-frame module passed the previously failing Stop transition. Installation occurred while Main was visible, before recording hid it and before the existing Stop affinity calls. The temporary installation hook was removed before capture. The 28.185-second recording opened a correctly colored editor with no Main outline in either the immediate image or the pure settled desktop capture; a later observation remained clear after three minutes. Direct native measurements kept the original outer rectangle `[339,164,685,605]`, client rectangle `[347,165,677,597]`, caption and extended style bits, while the hidden noniconic Main reported nonclient rendering disabled. Closing the editor restored a normally framed visible Main. The launcher’s first attempt failed on a PowerShell argument cast before sending any installation command; that failure and successful cleanup were retained before correction. This is a native app diagnostic using the earlier GPUI executable plus the exact new module, not a rebuilt current-controller app. Further lifecycle and current-app build verification remain pending. Evidence: `optimization-stage-13/windows-hidden-frame-harness-v2/native-gpui-v1/`; screenshots: `optimization-stage-12/windows-native-ui/gpui-input-HiddenFrame07-desktop.png` and `pure-capture-HiddenFrameSettled01-desktop.png`.
-
-## Main-frame lifecycle and remaining audio readiness work
-
-The exact Windows Main-frame module passed a second genuine manual Stop after editor reopen, compact/expanded layout changes, minimize/restore, caption movement and a System-to-Dark-to-System theme round trip. Both the immediate and 55-second settled repeat screenshots remained free of the Main outline. Visible framing, styles, outer/client bounds and normal restoration were preserved. The two task-owned recordings contain 1,691 and 1,412 independently decoded video frames with no FFprobe failure. They were screen-only recordings, so this is window lifecycle and recording-integrity evidence, not A/V or progressive-playback proof. The owned GPUI process exited normally, removing the diagnostic module. Actual Main fullscreen behavior remains untested; the compact/expanded button is a layout toggle. Full receipts are under `optimization-stage-13/windows-hidden-frame-harness-v2/ASSESSMENT.txt`.
-
-The audio goal audit found two separate constraints. GPUI had required an absent timing log and zero timing repairs, whereas the ordinary editor and Tauri accept valid timing data. GPUI now uses the same bounded regular-file/UTF-8/4-MiB log snapshot reader as Tauri and derives ordinary repairs from expected finalized metadata. Existing source paths, preserved AAC/container checks, cancellation and required-track semantics are unchanged. Scoped format/check and six native focused tests pass. The copied log reader is byte-identical to Tauri. Tests cover missing/valid/invalid logs, nonzero timing repairs, immutable snapshots and symlink refusal; no actual early-play app handoff is claimed. Evidence: `optimization-stage-13/gpui-audio-timing-admission/assessment.json`.
-
-The larger remaining constraint is in the shared playback controller: each required audio track must reach successful decoded EOF before its entire clip is admitted. A two-hour clip therefore cannot currently reveal a within-clip playable prefix. The initial video image is independent of that audio gate. The ordinary loader discards an audio track on a later decode error; earlier valid samples cannot be unheard, so partial playback must retain explicit failure handling rather than pretend the tail was validated. Neither a file hash nor packet-only preflight proves complete audio decoding. The full source audit and actual-app evidence boundaries are retained under `optimization-stage-13/preparing-audio-goal-audit/`.
-
-A bounded input API is now prepared in `cap-audio`: a nonblocking window read distinguishes pending samples, known absence, successful completed audio and errors. Pending windows report only the source-frame range they actually retain; they never claim EOF or advertise additional uncopied blocks as playable. PCM blocks are shared by Arc, and the ordinary full-track decoder/result path is unchanged. A separate review caught and corrected a race between successful result publication and sender closure. Tests exercise publication, dropped/error producers, short tails, empty/large ranges, exact sample mixing across block boundaries, and retained-block ownership. The reviewed API passed macOS formatting, scoped Cargo check, strict scoped Clippy and 69 default library tests. The separately requested matched 60-second fixture EOF test also passed, giving 70 unique audio test passes; its original 1,230,339-byte input SHA remained unchanged. Those are function checks, not Stop-to-editor benchmarks or Linux/Windows validation of this latest delta. Evidence: `optimization-stage-13/preparing-audio-windows-v1/assessment.json`. This remains input-side groundwork: the production playback controller still uses its completed-audio gate. Connecting a bounded output producer, deriving readiness from genuinely rendered output, preserving the output resampler across growth and proving the actual playing handoff are outstanding.
-
-Current Windows app compilation is held until this shared audio scope settles, to avoid rebuilding stale sources. The exact validated frontend assets and both app build plans are retained. The old Windows function VM is stopped. The Tauri build plan uses an explicitly documented isolated output-type-only manifest adjustment to avoid redundant static/CDylib artifacts in the constrained VM; that debug UI proof will not establish packaged-release or performance equivalence. No recording source, accepted fixture or application manifest in the shared checkout was changed for capacity preparation.
-
-One scoped unchanged-CLI diagnostic under ProcDump produced a valid full-color 1,168-by-702 JPEG and an observed CLI process exit code of zero. No unhandled exception dump was produced. Two first-chance COM exceptions were handled during the known Media Foundation-to-FFmpeg fallback. The executable, source project and intended adjacent shader DLL hashes were unchanged. This single non-reproduction does not resolve the previously intermittent exit-time access violation, and no crash stack or live module inventory was captured. An earlier attempt failed before rendering because ProcDump changed JSON argument quoting; an argv-only probe established the corrected transport before this run. Both attempts remain retained under `optimization-stage-12/windows-native-ui/cli-unhandled-dump-v1/` and `cli-unhandled-dump-v2/`.
-
-## Partial audio playback integration
-
-The shared preparing controller now admits a growing audio prefix within a single recording clip. A separate mixer checks every required source range before producing samples, retains ordinary calibrated offsets, timing repairs, gain and stereo mixing, and permits trailing silence only after successful EOF. Its output producer preserves the resampler across pending input and uses a bounded two-second ring. Ordinary editor mixing and output-buffer selection remain unchanged. Tests compare nonzero output samples with the ordinary path at 44.1 and 48 kHz, across channel layouts, sample formats, fractional seeks, preroll and the long-recording output mode.
-
-The callback starts with a zero consumption allowance. The controller installs the combined audio/video playable prefix before playback and grows that allowance on the same source generation. Unavailable samples produce hardware silence without advancing the media clock or acknowledging playback. Timestamped consumed spans account for queued latency and starvation; video follows the reported audible position. The last displayed image and its snapshot position remain separate from that live clock. One headless test starts before synthetic EOF, consumes exactly 4,800 frames under the initial prefix, verifies silent callbacks while held, then compares all 70,000 frames after growth without replacing the source. This is real output-control and callback execution using synthetic decoded input, not a new capture benchmark.
-
-Independent review corrected failed-stream retirement, post-spawn error ownership, initial latency pre-skipping and cleanup ownership after a timeout. A failed device stream is retired on the control thread because a stopped callback cannot process a removal command. Healthy streams retain generation-specific removal so a stale preparing request cannot stop an ordinary replacement. Output cleanup observes both actual producer-thread completion and callback-source removal. The controller retains its ticket until cleanup succeeds. Test-only construction errors and a weak silence-only long-mode fixture were corrected; failed attempts remain in the evidence.
-
-The final partial-audio snapshot before the successful-publication handoff passed 143 macOS Rust 1.88.0 editor tests after the cleanup ownership correction. The audio suite passed 69 default tests, and the separately requested matched 60-second native AAC fixture passed comparison through decoded EOF. Its original 1,230,339-byte SHA remains `05f898aca4373a8678a3487766858e2a97cdcdd72d101f54a6980440460da808`. Formatting, scoped compilation and strict Clippy for both libraries and their tests passed. Repeated and overlapping focused suites are not additional unique passes. Receipts and before/after source manifests are under `optimization-stage-13/preparing-audio-output-v1/`; independent reviews are under `preparing-audio-stream-retirement/` and `preparing-partial-audio-clock/`.
-
-Those partial-audio checks did not establish native application Stop-to-first-audible timing, Windows/Linux parity or uninterrupted ordinary-editor handoff. In that snapshot, an incomplete managed source was cancelled/joined and decoded again by ordinary loading; only completed tracks crossed the cache boundary. That implementation limitation motivated the successful-publication handoff below. Device-buffered audio cannot be retracted merely by removing a callback source. Windows retains its existing zero-latency estimate; the pinned CPAL WASAPI playback timestamp is not sufficient evidence for changing that policy. Physical endpoint/loopback timing and actual application presentation remain separate from function-test coverage.
-
-## Successful-publication playback handoff
-
-The final candidate records successful publication without reopening source admission: the observer still ends and rejects new leases. A controller that was already admitted for the same successful job can keep its existing readers and audio output alive while preparation finishes and ordinary loading reuses completed PCM. Recovery-lock ownership stays with the original readers until actual cleanup joins release them. Failure, close and supersession continue to stop and join owned work.
-
-The ordinary candidate commits adoption only against a matching presented frame and current playback intent. It then follows the existing consumed-audio clock and audio generation. Pause or seek can invalidate and replace a pending candidate, and cleanup of an uncommitted or stale candidate cannot shut down its predecessor or successor. Existing explicit ordinary pause/seek/restart paths stop and join the adopted owner before installing a replacement source.
-
-The GPUI v4 callback also permits new frames from the already-admitted, same-job preparing session after successful publication; failure, cancellation and supersession still reject delivery. Its added admission regression test is included in the three GPUI v4 suites.
-
-The macOS editor suite below includes native fixture and headless output tests for PCM reuse, candidate disposal, moving-frame handoff, cancellation and panic completion. These tests do not measure a physical audio endpoint or prove that a real application window presents an uninterrupted, aligned audio/image transition.
-
-## Current automated verification before manual testing
-
-The rows below distinguish completed native test scopes from checks left unrun when the user ended automated testing and requested PR publication. GPUI uses the matching v4 source on macOS, Linux and Windows. Root Rust inputs are unchanged across the v2–v4 source manifests; subsequent deltas are the GPUI fixes and generated Tauri bindings. The older three-platform table remains historical evidence and is not added to these counts.
-
-| Scope                               | Result                                      | Qualification                                                                                                                                                          |
-| ----------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| macOS editor library                | 161 passed                                  | Native handoff and headless audio ownership tests included                                                                                                             |
-| macOS recording/recovery            | 597 passed                                  | 535 library, 12 real-subprocess muxer and 50 recovery integration tests; three library ignores and one external-fixture integration ignore                             |
-| macOS Tauri                         | 470 passed, plus one binding-generator test | Generated bindings were then used by frontend validation                                                                                                               |
-| macOS GPUI v4                       | 993 passed                                  | Two existing picker/screenshot benchmark tests ignored; all-target compilation passed                                                                                  |
-| Linux GPUI v4                       | 1,052 passed                                | Two existing ignores; native compilation and source/runtime/binary integrity checks passed                                                                             |
-| Windows GPUI v4                     | 970 passed                                  | Two existing ignores; native compilation and source/runtime/binary integrity checks passed                                                                             |
-| Desktop frontend                    | 473 passed in 47 files                      | Final generated bindings included; TypeScript passed                                                                                                                   |
-| Web frontend                        | 3,047 passed, 28 skipped                    | 219 passing test files and six skipped files; TypeScript passed                                                                                                        |
-| Tooling                             | 64 passed                                   | Separate function-test scope                                                                                                                                           |
-| Linux root libraries                | Listed suites passed                        | Audio 69, editor 160, encoder 150, video decoder eight, project 130, rendering 244, recording 690, export 32 and muxer protocol seven                                  |
-| Linux integration follow-up         | Enabled requested scopes passed             | Real muxer/OOP, recovery, sync, pipeline, rendering and synthetic scopes passed; three explicit GPU correctness tests and the separate 60-second audio EOF case passed |
-| Windows shared libraries            | 1,314 passed                                | Audio 73, editor 159, encoder 138, decoder 11, project 129, rendering 244, recording 530 and export 30; nine default-suite ignores                                     |
-| Linux Tauri library                 | 530 passed                                  | Binding writer filtered; native library/bin check and strict library/test/bin Clippy passed                                                                            |
-| Linux Tauri main binary             | Six passed                                  | Exact main source and 107 unchanged dependency artifacts; direct compiler invocation after ordinary Cargo staticlib packaging exhausted disk                           |
-| Linux canonical follow-up           | Three executions passed                     | 15-/60-second recording scenarios plus the separate audio EOF case; this repeats the earlier audio EOF test and is not three additional unique unit tests              |
-| Windows remaining root/Tauri scopes | **Not run; testing stopped**                | Protocol check passed; protocol tests, Tauri, muxer, integrations, explicit GPU, canonical, hardware and remaining strict gates are incomplete                         |
-
-The final macOS editor and Tauri all-target strict Clippy gate passed, as did recording library/test strict Clippy. Strict GPUI Clippy did **not** pass on any of the three platforms. macOS v4 emitted 28 diagnostic blocks representing 27 distinct primary spans, including one repeated crop diagnostic. All 27 match v3; three additional unchanged `editor_frame0` spans from the v3 JSON were not emitted in the v4 text and are not classified as resolved. Linux emitted 23 errors and Windows 28. Every emitted primary excerpt and surrounding context was verified unchanged against HEAD and the initial adopted source. Linux's platform-specific `explicit_auto_deref` and Windows' `infallible_destructuring_match` also match that baseline. No new diagnostic context was found, and no suppression was added. Passing tests and compilation do not convert these strict-lint failures into green gates. Linux export strict Clippy also retains two test-only baseline diagnostics: an irrefutable `let` pattern on Linux and `field_reassign_with_default`. Their entire source files match HEAD and the initial adopted snapshot. No later strict export pass supersedes that receipt.
-
-The Linux integration follow-up passed nine muxer unit tests, 12 OOP tests, 50 recovery integrations, four sync tests, 61 instant-mode scenarios, five segmented-pipeline tests, two rendering integration tests and 28 synthetic tests. Three offscreen GPU tests and one audio EOF case were executed separately from their default suites. Empty platform targets, hardware exclusions and external-fixture/benchmark ignores are not counted as executed tests. The follow-up initially stopped on an unsuccessful 15-second canonical finalization probe (`exit 101`). A fresh native lane subsequently passed both the 15- and 60-second canonical recording scenarios and repeated the audio EOF test using the exact retained current binaries. Original fixtures, sources, native inputs and executable hashes remained unchanged. The first failed attempt remains evidence; the later result establishes successful retry execution, without claiming a cause for the first failure or complete seek-frame coverage from the summary alone. The repeated audio case is not an additional unique pass. Windows remaining root and Tauri scopes were not run before the user stopped further testing.
-
-Linux Tauri has 536 executed function-test passes in total: 530 from its normal Cargo-built library harness and six from the main-binary harness. The ordinary Cargo binary-test build failed while creating `libcap_desktop_lib.a` with `ENOSPC`. The six main tests then passed using a reconstructed direct Rust 1.88 invocation, the exact main source and 107 unchanged dependency artifacts, including the completed ordinary Rust library output. No source or Cargo manifest was changed. Strict Tauri Clippy subsequently passed for library, tests and binary targets. This closes those function-test scopes; the ordinary static-library packaging build remains failed and is not relabelled as a successful standard Cargo or release-package build.
-
-The Windows shared-library total comprises 232 unchanged-source passes inherited from the preceding VM (audio 73 and editor 159) and 1,082 newly executed passes (encoder 138, video decoder 11, project 129, rendering 244, recording 530 and export 30). The inherited suites were not rerun or counted twice. Their strict audio/editor gates had passed. The new protocol compilation check passed, but its test build did not start: the lane stopped at the 1.6-GB preflight with 1,581,453,312 bytes free. The remaining protocol tests, Tauri, muxer, integrations, explicit GPU correctness cases, canonical fixtures, hardware cases and remaining strict Clippy gates were not run for this final scope. The user then explicitly ended testing; older successful runs do not fill these gaps. Source, runtime, fixture and executable integrity checks passed at the stop. All eight executables and 50 logs are retained; the overall shared-phase receipt remains `passed: false` because the requested phase was incomplete.
-
-The earlier empty-output muxer crashes and filesystem-sensitive recovery failure retain their original failure evidence. Their focused Linux/Windows fix snapshots are historical; the current Linux recording library and real OOP/recovery results above provide later matching-root-source coverage. Current Windows recording-library passes do not complete the unrun real-helper/integration scopes; the older 27-test fix snapshot remains qualified historical evidence.
-
-Counts describe their listed scopes, not globally unique tests across operating systems. Earlier editor runs, focused frontend subsets, previous platform totals and repeated retries are not additional passes. Canonical 15-/60-second fixture scenarios remain separate from unit counts. No final-candidate physical audio-endpoint timing, continuous handoff UI test, new capture benchmark or expanded two-hour run is claimed. Manual testing should use a fresh candidate and include playback while preparation finishes, seeking, pausing and closing/reopening the editor.
-
-Current receipts are under `.git/codex-build/session-support/01a08773-a817-7de1-988e-ab9078f33c12/`: `macos-final-editor-v2-tests`, `macos-final-recording-tests`, `macos-final-tauri-tests`, `macos-final-tauri-bindings`, `macos-final-gpui-tests-v4`, their check/Clippy results, `gpui-clippy-v4-baseline-comparison.json`, `gpui-v4-platform-clippy-triage-v1/assessment.json`, `final-source-v2/linux-root-progress.json`, the captured `resume-progress-v1/{linux-root,linux-root-latest,windows-root-latest,linux-tauri-latest}.json` receipts, `linux-tauri-replacement-v1/linux-tauri-canonical-v1-latest.json`, `linux-tauri-main-test-v1/{progress,clippy-progress}.json`, `linux-tauri-bin-triage-v1/assessment.json`, `linux-export-clippy-triage-v1/assessment.json`, `windows-root-replacement-v1/shared-progress.json`, `windows-shared-retained-v1/receipt.json`, `windows-inherited-root-retained-v1/receipt.json`, `desktop-tests-final-bindings-v1.log`, `web-tests-postreview.log`, `tooling-tests.log`, and the final frontend typecheck logs. Interpret mutable polling filenames using the hashes retained with this report candidate. Further platform testing was stopped at the user’s request. Active test sandboxes were stopped and task resource removal was requested after local evidence retention. Three older restore requests rejected stop/delete because the provider reports a state change in progress; cleanup remains qualified until those requests are cleared. Receipts are in `final-sandbox-shutdown-v2/` and `final-sandbox-cleanup-v1/`; the earlier stopped-sandbox statement applies only to its historical run.
-
-### Earlier partial-audio verification snapshot, September 10
-
-In the earlier frozen partial-audio snapshot, the enabled app suites passed on all three operating systems. That broader run was not all green; its failures motivated the later fixes:
-
-| Platform |                     Tauri app suite | GPUI app suite | Broader native results                                               |
-| -------- | ----------------------------------: | -------------: | -------------------------------------------------------------------- |
-| macOS    | 468 passed, plus binding generation |     992 passed | 2,854 passed, zero failures                                          |
-| Windows  |                          445 passed |     969 passed | 2,880 reported passes, three empty-output muxer failures             |
-| Linux    |                          528 passed |   1,051 passed | 3,099 passed on the normal filesystem, four failures described below |
-
-Counts are per platform and selected native suites, not globally unique tests across operating systems. Repeated checks are not additional passes. Windows' reported passes include one loopback test that returned early because no output device existed; this is not physical loopback proof. Two canonical recording scenarios per operating system are recorded separately from the table. The explicitly requested ignored audio-EOF test is included once in each native total. Manual device, benchmark and external-fixture ignores remain documented in the individual receipts.
-
-That verification-only phase changed no production source or test assertions. The common 2,337-input source archive has SHA-256 `7f82cd885259e9100e1fa32e5a54ab2597015e21029bf99d6c78029c97d1794d`; its manifest has SHA-256 `4a5b7ebb67f54e6e22678488a9592d6fc51f6953cdd42f36de199baf31021e4a`. macOS additionally requires 24 platform-specific source files, and both app test setups require two exact committed MP4 fixtures. The Windows root-workspace source manifest differs only in GPUI-specific files, which the separate Windows GPUI gate for that snapshot covered. Later unrelated sharing/reupload and development-launcher edits in the shared checkout are excluded. These results apply only to that frozen candidate. The subsequent combined build adopts additional sharing/reupload and launcher changes as well as the recovery, muxer and handoff fixes.
-
-The desktop frontend passed all 460 tests across 47 files, TypeScript compilation and Biome on all 40 frozen touched frontend files. Another 28 packaging-function tests passed on macOS; they are not native Linux execution. Scoped native compilation and formatting passed. Strict editor/audio library-and-test Clippy passed on all three platforms. Linux Tauri strict Clippy in that earlier snapshot reported three existing findings in unchanged `upload.rs`: unused `SegmentUploader::spawn`, unused `Control::complete`, and a redundant `Bytes` conversion. The binding-writing test was filtered on Windows/Linux and explicitly executed on the isolated macOS copy; generated bindings were byte-identical.
-
-Both preserved 15- and 60-second `.cap` fixtures were replayed on every platform. macOS and Windows observed all eight requested video frames and matched their independent ordinary-finalized references exactly. Linux observed five of eight requests before finalization ended the preparing session; every observed frame matched, but three cancelled requests are not frame-equivalence coverage. All three platforms matched every one of the 2,883,584 decoded audio frames through EOF on the 60-second fixture. Source, published-media, normalized-document, lock and worker-join checks passed. Original fixture hashes stayed unchanged. These are functional comparisons, not display-latency or audible-endpoint measurements.
-
-The three OOP muxer failures in that earlier Windows/Linux run were `respawning_subprocess_reports_clean_exit_when_no_crash`, `subprocess_spawns_and_finishes_cleanly_without_packets`, and `subprocess_survives_finish_after_init_only`. Actual packet-producing muxer tests passed in that run. The helper, protocol, calling OOP module, integration tests and all 89 transitive helper lock entries matched HEAD `284ae24a79bd0813743f51e6219d11ae0042d38f` before the later fix. The standalone Windows initialize/start/finish replay with no packets also crashed before the fix. Its minidump places exception `0xC0000094` inside the loaded `avformat-61.dll` at offset `0x4c8f2`. Native disassembly divides the byte-position-derived numerator by zero immediately before formatting representation bandwidth, matching FFmpeg 7.1's unguarded final DASH bitrate calculation ([pinned source](https://github.com/FFmpeg/FFmpeg/blob/n7.1/libavformat/dashenc.c#L834)). The small dump omits the divisor's heap page and has no complete symbolized internal stack; zero numerator registers rule out quotient overflow. Linux's Rust wrapper reports a non-successful child exit without retaining the signal. macOS passes all nine OOP tests with an ARM64 helper loading FFmpeg 7.1 / libavformat 61.7.100; that does not clear the failing x86 platform cases. No trailer, bitrate, library or assertion was changed during that diagnostic phase. The subsequent reviewed helper fix and added regression tests have their own passing native receipts above.
-
-The fourth failure in that earlier normal-filesystem Linux run was `recovery::transactional_recovery_tests::interrupted_rollback_reconciles_and_relaunches_at_every_rename_boundary`. That library suite reported 685 passes and one failure on the sandbox root filesystem. On tmpfs, that rollback test passes, while two executable-launch tests fail because the mount is noexec. Across these controls, 686 distinct recording-library tests pass, making 3,100 distinct passing names across all documented Linux filesystem scopes; this union is not a green normal-filesystem run. The retained older executable and current executable each reproduce the root-filesystem failure in all three alternating runs. Before the fix, the stamp, reconciliation function and failing test matched HEAD. A separate four-rename diagnostic reproduced directory size changing from 42 to 54 bytes without content or timestamp changes on rootfs; tmpfs remained stable. This supports an existing filesystem-sensitive guard but does not capture the exact differing field in the failed journal. No receipt format or recovery validation was changed during that diagnostic phase. The later guarded stamp fix was independently tested on the normal filesystem, as recorded above.
-
-Setup corrections are retained separately from product failures. Windows hardware compatibility passed its complete 13-test enabled suite in the existing Administrator desktop session, with seven hardware-specific ignores; the original SYSTEM/session-0 capture-support failure remains in the logs. Linux GPUI passed one complete 1,051-test invocation using the original Cargo test executable name and a stable, task-owned Xvfb display. Initial display/reset and renamed-executable failures are retained. The virtual display was stopped afterward; no application UI or screenshots were produced. Three explicitly executed offscreen GPU correctness tests passed per platform; Windows used WARP and Linux used llvmpipe, so this does not establish physical-GPU coverage. Linux's final 32-test export suite passed with the frozen root Rust 1.88 graph and unchanged source/lock, after downloading one already-pinned development dependency.
-
-That snapshot supplied source-bound regression evidence and concrete failures, not packaged-release clearance or a no-regressions guarantee. The final candidate now implements the successful-publication handoff, but its physical audible transition and application presentation remain unverified.
-
-Receipts live under `optimization-stage-13/current-macos-automated-v2/`, `native-partial-audio-windows-v1/extended-assessment.json`, `windows-gpui-automated-v1/`, `partial-audio-linux-v1/`, `partial-audio-gpui-linux-v1/`, `windows-integration-failure-audit/`, and `current-frontend-tests/`. Sources, original media, exact native executables, failed attempts and diagnostic evidence are retained.
-
-All four sandboxes used for that historical test run were stopped normally after evidence verification. The older sandbox with a pending restore remains archived with a requested start; its 60-minute idle-stop and auto-archive policies were verified, and auto-delete is disabled. That receipt recorded no remaining test process at the time; it is not a current operational-state claim. The consolidated historical receipt is `optimization-stage-13/automated-test-handoff-final.json`.
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/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
deleted file mode 100644
index 5aff2e93396..00000000000
--- a/apps/desktop/src-tauri/Cargo.toml
+++ /dev/null
@@ -1,196 +0,0 @@
-[package]
-name = "cap-desktop"
-version = "0.6.2"
-description = "Beautiful screen recordings, owned by you."
-authors = ["you"]
-edition = "2024"
-
-[lints]
-workspace = true
-
-[lib]
-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"
-
-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 = { version = "0.25.2", default-features = false, features = ["rayon", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp"] }
-futures-intrusive = "0.5.0"
-anyhow.workspace = true
-futures = { workspace = true }
-axum = { version = "0.8.9", features = ["ws", "macros"] }
-tracing.workspace = true
-tempfile = "3.9.0"
-ffmpeg.workspace = true
-chrono = { version = "0.4.31", features = ["serde"] }
-regex = "1.10.4"
-rodio = "0.19.0"
-png = "0.17.13"
-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"
-sentry.workspace = true
-clipboard-rs = "0.2.2"
-whisper-rs = "0.11.0"
-lazy_static = "1.4.0"
-log = "0.4.20"
-semver = "1"
-
-cap-audio = { path = "../../../crates/audio" }
-cap-camera = { path = "../../../crates/camera", features = ["serde", "specta"] }
-cap-camera-effects = { path = "../../../crates/camera-effects" }
-cap-utils = { path = "../../../crates/utils" }
-cap-project = { path = "../../../crates/project" }
-cap-rendering = { path = "../../../crates/rendering" }
-cap-editor = { path = "../../../crates/editor" }
-cap-media = { path = "../../../crates/media" }
-cap-flags = { path = "../../../crates/flags" }
-cap-recording = { path = "../../../crates/recording" }
-cap-timestamp = { path = "../../../crates/timestamp" }
-cap-export = { path = "../../../crates/export" }
-cap-automation = { path = "../../../crates/automation" }
-cap-cli-install = { path = "../../../crates/cli-install", features = ["specta"] }
-cap-enc-ffmpeg = { path = "../../../crates/enc-ffmpeg" }
-cap-media-info = { path = "../../../crates/media-info" }
-scap-targets = { path = "../../../crates/scap-targets" }
-scap-screencapturekit = { path = "../../../crates/scap-screencapturekit" }
-scap-direct3d = { path = "../../../crates/scap-direct3d" }
-
-flume.workspace = true
-tracing-subscriber = "0.3.19"
-tracing-appender = "0.2.3"
-dirs = "6.0.0"
-relative-path = "1.9.3"
-cap-fail = { version = "0.1.0", path = "../../../crates/fail" }
-tokio-stream = { version = "0.1.17", features = ["sync"] }
-md5 = "0.7.0"
-tokio-util = "0.7.15"
-tokio-tungstenite = "=0.24.0"
-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"
-lz4_flex = "0.11"
-sanitize-filename = "0.6.0"
-tracing-futures = { version = "0.2.5", features = ["futures-03"] }
-tracing-opentelemetry = "0.32.0"
-opentelemetry = "0.31.0"
-opentelemetry-otlp = "0.31.0"                                                 #{ version = , features = ["http-proto", "reqwest-client"] }
-opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio", "trace"] }
-sysinfo = "0.35"
-workspace-hack = { version = "0.1", path = "../../../crates/workspace-hack" }
-aho-corasick.workspace = true
-
-
-[target.'cfg(target_os = "macos")'.dependencies]
-block2 = "0.6.1"
-core-graphics = "0.24.0"
-core-foundation = "0.10.0"
-objc2-app-kit = { version = "0.3.0", features = [
-    "NSWindow",
-    "NSResponder",
-    "NSHapticFeedback",
-    "NSWorkspace",
-    "NSRunningApplication",
-] }
-objc2 = "0.6.0"
-objc2-foundation = { version = "0.3.0", features = [
-    "NSNotification",
-    "NSString",
-    "NSThread",
-] }
-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"
-gtk = "0.18"
-gdkwayland-sys = "0.18.2"
-wayland-sys = { version = "0.31.7", features = ["client", "dlopen"] }
-ashpd = { version = "0.11.0", default-features = false, features = ["tokio"] }
-notify-rust = "4.11"
-
-[target.'cfg(target_os= "windows")'.dependencies]
-windows = { workspace = true, features = [
-    "Foundation",
-    "Foundation_Collections",
-    "Globalization",
-    "Graphics_Imaging",
-    "Media_Ocr",
-    "Storage_Streams",
-    "Win32_Foundation",
-    "Win32_System",
-    "Win32_Storage_FileSystem",
-    "Win32_System_Power",
-    "Win32_System_Threading",
-    "Win32_System_WinRT",
-    "Win32_UI_WindowsAndMessaging",
-    "Win32_Graphics_Gdi",
-] }
-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"] }
-wgpu = { workspace = true, features = ["noop"] }
-
-[[test]]
-name = "gpu_device_health"
-path = "src/gpu_device_health.rs"
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/assets/wix-banner.bmp b/apps/desktop/src-tauri/assets/wix-banner.bmp
deleted file mode 100644
index 3fcc74678614c03416c4a52f5d2a1b9bf6f033fc..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 405270
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

8LoEQFay6!E#an8nQ`15#G2MuE~p&Kdc8JGzgY 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 z zDNjGW)8yV&Uv#Pag$&%WOFD}<8U>cXPvuFQu(w&25li>yB;F)jk8q0$s)$Q?N}p6U z@0Zu@@`?<$j1WAf!d6SxgW20Bfd-eG>UR(J?HXM3_OX4QZYzrIiPCou$|r?c8+f1c ztck`)K2cBt@G-TD4WS$|U?fA3m&a1hRhihN8R}j=D|01pvKA8{N-GW#8X4zGIJGp6 z&4^0HmoLc&q=vLifZ^6*d7?J4c$_Cn=2_02RlND7sWc0Mqork$IqgmBf%sZVF$$CVw?D{wKpsG7s|cxw`r$u`lvnQlow-6E&Zp z8trn(sH-wmVKO*9-FWMMF&O%ZN+MZ?jbQ1+=PNBQD}CXUURRfKc`))N)+Q?JUR`6H zIOwOVySJ~KC?^fP)?pz~8UzOsZ9tefLEux-vc(s-VeDz6k(dpMt&XWIk3{OtzXV31K~g?Y)-= z?$zCQO>;D0%Vcp44ix);YQ^3-CLheYgqQZ-dwJL0h8Y*jOr0F1Eylq%o-zFK^)a1# zYGYvk^C-q+%)qcMdtzbMXl$SGl$fZUv{v9wu-qDg;U?$yPYbAZ?lJ37a~^@=6VD3B zFEAehB2d-E<7kuv1SeAHbNgfK_00Bm`k*G5e*EX?UN1VFAj3MB_2Yq`$X~=@6eD(M zbT_;2NcdtQSohx>xNfav5=*NYp4pd_Pj8cLTvW=CCQG3Kjjv}3`)Ee2L;zJ5AGz|& z=edvsoUrXDGOc4YrXt6D{Z-cGEx(lz=^EQL~$=ueVMkC!=&@Z=fT3A?n@oST2C=^2>AcK>8Ynqa*qH^T}i(-gy zs7F33mv*a)(A#>)=}K78ksUFbm8RHYNrw`3nHepT5^F9xly_d%%d_0#UOd8L)7OuX zXZFZiA)tMHkXBz#L7o$V?Z}Exci+d<3Y$n+a#P99A7l}zJ@*I;oNw3u``LO)kVsiO z?}Daigs<$mn*cNJastbU-$*rV@x)D8;t+k9*>m?bK@Ru-gyNf}mSdure9z@(wRdg~ zoZrC)Sv`!%+(OktnwKf8Y<0|o=HH$)&4VDZDcHOT{FuZ|i=dA4k$J}MI_4{Ih`AeP zvxf{#%7L{7drX}sU$+Ej7M2Lu5N<0b3MTYO#brzMcNtu)TYAEEOoj}DHLvj#tuSS~ z-La>{D67PeK{X7?%*AHj_$GWmU~j`M;FI(Ukb*HpFVqK7uvLSZiCf*6JWHb3```hv zsv`YrkmPC2f^zK-SmM-QOh=jqjmpMKEh)>X0e3+6G(yDbWqFmxp%4K-ATjqsBUutU zFc_DSPujtP%Z1SFf!@k_usc!!!D;ERIQs}<5a(`7c(gG*Y_GN54&DSQ6|>XSlo}Rx zS-wQoR5qBZTeYm4Jh#z{X3vZn_AwBO7&t9YVmNlT8uls+Kq#mzuavprD75A(P(A6* z&L1AjL18(dUFUk1!njM6uI1MGYact;HSB+jh ztD%A(Yp^+XyMnb~e9R$@(8ozuft0&J(ms3#su;w`d5XYL^;14w;J#|)8xMvP9c2=< zE3^U^;&rBngrCaLIY{qb$Ne)eH97zKZ)8*y6&-RedAmCLX>@5)@YS(VVXs@&F)x%y z`x&5~CerLGC2=xgg@q!Oa?R9?AV}GnIFLA`p!(fF%6U9%ELC3fdYAJlsEVhps{7z- zZ6#ZYrBOng4l)u{HYI5SrBvlx)VD8WQe6fyhJP(zs;P~%TwPP2kt#qGPxVkQayNLF z(|}RV>^I!vP0{tKQI#_dUbV}3sOn4EIrmVURJ?QUC%Rn$t{H?y^L2XO7m|>wngOYT z8u{{?uS;fgsa0=uwf$+5()rNgD!H-Y1yp|B6pBiRRSrAhEY%~~>t%gch0Ir|#zSgU zY^$@=78IEh+>)B36HnRK4s!jL#(($`Kw-C#+a5#D-%IK8dJV;TPYk0J;itSrhA zmBsfo(qil+(nwC&tF6Hda_bzA21RF<~8$&q`TX`%GawZ=c_Bn~Cz;V?U3+ zgV~i}#izGL*}+i){!etE7-&U5^5S9Yk1W_>b(NDFuZX^DC2_79Gy)8w$h0f07RTfn z0lsi0D_jHcOs`3d(G$^WUU?!0Eu7yel9+o5h-$GstJw2-!GmlfsS4q>_eUH>&8pls zO|r}(uI;&d`ia$Me2SY-gei`7#9b2jn9m5w{^$dgKFqY@XpG=IACsHemyWToNCAY> z;;{lcUOdr}eibYRW53E;B(cudM7A1-Q$m=Ho(NK?(kIcSyGPJtc z2Tw5~h<7d^BU#nL8(IPJCQ-9mDAC`6iD)9oncdL@R3i(K`A@8mCPA?l2m+cAd|n)3 z!WtHSj%#*|XTm9(vrO*lpZ3u{(22CI6y)D5qje=C2gzM!{WZxjcTM=qBD#T7^!Wwt z$!LX@OCxz%#6+5RpdmAJGub01%}oaW(&`%}nbfyPM;OQCVB+dtq$B53;NNGZlJdUE z*EPnmgFZT+R-_|~%)XnzS>A3!2L2<$3(!Q5p+%yxVozJ&&$78BL6={Rf}NTi!MAk@iI5=8(p05zOU_s~ zjc!G2TS`E{F6Ktui#8-gu(h_E&S-qao@_*~xWsvOHkV zRSlRn53h_iM6wh(-Z7yhe$LMm88IQe_x<&|GhLJg?(TIZ5` zLCWPRl4GssmW;6#H-$PeTp3GlU^wY#4K!`(4 z#j$IsSGB(z)l{0${A!=55)7)b8KqAS-o7zsy`OF>w&lE{x>ugjzf`xn>RBmIzu?^( zQ7|pNI@Bds6)%~1e<_$mOw|G?gY`zG-}FrtRBhV;zzF^%)Nj&xCer`lQo5Z z*(L`8*cQk@7U%^8#^=dy{b(x~9$C#yD~gg`cboEPK&*`w*Y#a@WF7epA|Gb8dTI!& zfAd;A9Zqj0_MbiX7{Bj8L1tHAJnq27gCgVn!FY&>A^cQLyVj>Z@{5jYWi5s**Y47p z{lw`pmtxjJrSj^NNJm(J_*zPM{veu=r9EDZS_F)z&LEWL-V%+>v)kF5C1}<0EwMM- zbARISi}++Q5t2aDH_z@}x5vWaksq4JqcP!zUuN40rB$YAv5Bg+%>=#|+gCR=F5 zMA?x8UL^a&J`!}xK^-qvh=jRtJgMbN?5E%PA7J(QefKUX9ok+C;I~Bk$U!m~93vwy z?2FX;GyeXkY`1REw)jaJFP70_O47&HWNaET*lU%#MC8>)f7el)I^TRGDw5(OsB|I; z6ea0cTWz(K<>u?PLxRyoJwFEp$Dj>q z8e%`$7V?~lLDaSB%M*SPORZ9l#8zrTLna4JQh33Sv0x zX&SP4-<+j$$g_%)q$0NV*&o@*K;Md<HKm8l=PKE#dKlW9Y=gb&J*vFKP2RGxL?0|)nDsZsw_p3;;#9Kb)^Gt!knFoh>=Y}~S{F55Dtqq6Vy1USQIc>UVviht z$dV}0%k21090M!5mjzHPXi^g9OwbO3NExeBi>$u|rkW{qYI@Dw2lfc}TGMopSa4c| zvDwOU9suTyY=(8ICfrQE?sX>UZo^=V4R=W(v8!MMu#q`BBxqUU)vQipX96}xIMnm6 zSvyuJN;p8oB(nh_4Wi8KvMofKP);skNr9I@(P+L7fjAi?P*6cRQ>mgbQpy<&Yvd^< zJP#iRR+Udjh(jQ8Rz_>tcuEptGWe&!8;~~IA`@lSq!H1VLw97t+K1WOD8NtQLCX4x zgXFn{&*e5ASAE!W!`JuqFtEW> zEF;J3zM`Q5nO5Bt*jy+s09aOzBe74O096=AlaXRBatJQfm5FkYOL7Z-7DZ~)*L9@? zS^zspsS4GneJWc}5NiWOjhrqX5bk!FU+K6<5{*cOf}cUSjZ!St|c%wRxL4--QIB8fi1vT<120H zO}a3B#z_gZZPpd4-f<>Hr8Zje1u1o(*Vdpl)KleQbmc+m+@gnWAw|!-S&#O!WfAJ4 zR8ne<&;i`J>Xk=mNs@=E^_8hFrX#9-y6+r2Xf;sETyp+smW&qXKVI?0@QyP4(-C@~{Zf8bN@eyeLowOrRf-~SL)G>(8T;oV!NDLjV-(%~wL>;}M4)VHH72L(mFn85Jve%~?+VnxXk1UHE|t~Y1I;HvXIAqLgZjEFf|@OT#fvAq?WB2R>fVtQnW1QPqR2jkLC zNr4wo+??E;7{82Cj!eP!q-wMbiMV3z@?yR^2sa^8yR_$ihFh5#pIi+&XQ2en9TwW) zwaq$9Hc?bfg*gU4s?e}!jY3?Lq%V1K}ALNQnNwWUPqd%M9 z6}h64QvjuEhku@UzmNSiUKmep7E-KA@@UN1tc$^GUJT^dPsY?ruf6<4);&=v+v}&x zz~&6KNW70>OHs>oUr`d=B%a9pSjpZ?OHw9Vad=NCGkoNgV^*n91r81{2^CMo>@-Jh z`9s*=Ueb}eZwohx5Wf9KCc%D3fA%9&SD61TxR9VEgVk1hkwndLAdc-eG$d=0jOJL2 z573a9coh>tXS9`kX5M{u5ma}0J@mi)$NBsJKFX1bsZ|~s|J&SM|NEdEX&TZ*z+Q;j zzx+#n*XQ|{Y5uk|7S3MeNHTB}Dw2EuYf2ldE#bDR35q<7r;k7T6J*f-x90DtK<7N|ty|GGB_|Awv4Hy;_5BLgdO(wV+g3Y5-s@tHia zbS2T4AR|{sg~&uRG9E~Ee_=MKiBkg(;YBcwMG;5@yBR@d4?K2U;nQnfXr>k%8Ah(5UsEO$E**v;VTwf7n_T}Nj`=L9<;`5%)^Lc_T4wX_b&1!=c9ii zcakB|4H?eExArQ%@q*v}L(#pWYB4SHg8)%_k~IYf&zSNG)zA@0VEGSTs1yW+pv;CQ zV)(?*OpYtR5a3-J5Sf4knf(j4HEs-0FE7!asa?u>1P8b(z%avkA0#6o+)ztYyJW&G zDQ+H7Xey5`)E9snL3&`)fN)IHe6D-?_S`H^U82as04WKG#r9qfC0BQ*H<-jFnYijw z06bx}2(cyPL_jpSy$zP=H1Lv+fCLy*CH?ZmERq}K9Mo5`dO%m20GN7^J$&UP@~9nD zIl8AU-{}NF9XwEw5nVtt!lg(=XznW8Zo9hat1z4M$cP3B?SfK5374RX%@!3^yM6Ky zAU(n)g&|i5x3GpHDTrQLpg^zkE3J}4eHf@9U}%C%@&}DV&EQpdn^jx&Nar6gKf)?e z5i0Q#^7bAoaICUfb4||ff+4$q{nS2kQ?^`E&gG>snOX{;cVa_D+g@7n4}SI*L|1MX zvT`QiHhP&b7l5;Cq~vbI2n;+40t@*!#SJ{9BjWuCz{3YU)T%uaD|1Fp3C2IkqI&`G zVZ_BXjP`=Q6<#%#U|%$hu_||Yx|Z})RuW4V@>S($+UIc@8-9-CSzqX7BhV-XS=FLp zyaUC{-q;=xZy>Sqdz|wg(Ov8qp3h5E@0Ul^x+3~99fS!k|6-V6U5(S0 z76Vqde#edJ8sWt2mGtQ3R;9|g%8QdN52};S7%C|_wz9Ic0M!c(y!A`SV`U{x+NADM zS&gq*TuPgFtxJ8GvKjN)=h3}sqHZy@=)^rN^9pn^fc7e^9YE|51e6>=fhi(e7EekF;dW0Oh&HH_nRbD90R#O8z$k@;h{Qa3k z7K6|oZd6TsC%c?Ji5ZhqH>tY}z3B?o+SO`SAmy6@75PA3IJeS2t%V|-ex3l_TAbn8)lOsQCL?FJF&+pEnU3@@JY!GgXhq%ehgcZXx zAB~+cg)-iNdCC-Pc+DJL2`wiARTgnOWQIi|c<3i6F1Ha%4MJmz3@eheOn7cLO$y^E zlWgM5SlQ&lQ6aWicG01rY|4Ofa6M>?FZ<|C*tVV9k7$^^*1WJ|cMQjzc=h$Bq3dEf^lpJ(Dn6Wx%-U-%&KjDBX1pU#J`mA8AVNLaXm z!P%Znyk7&X^doxFh~*EQ**V%B<754OpccPjQ;f{Fbq`Vqq*Vh02yF?-(= zZX&jn|5A39j`@jre_8u+bcOr=Nb&{pRV_Yn>;GsF_WoCMG%2Xkttp zqQGVb8B|0N0Yz~_98qz=8Jwp?GoK_1?1|lCnP`Ysiwt_-W|2|^zN${c3lCH&fgjm z0i(u~UBslV7tU?|o^LgG5&{`Z|IDqIetO%_PT%oshP4Y4+Hzpre*+qlkNK@vNUSD6 zx<#3EA~A$FeuqPfs-knbMN;^m0%pDL11)XSZWTUF#z>005e$yf#2V@VexNtc+Uo98 zUxh_2@zzS0Lkp4fvm97^K3o(ruk&CefXaE<^35lrV(bJn5V?yRvnGpsfPLfM0wK=W ztkgul4zvUmaggT185_pQ6pn`P;&)~WWxdV~Vqi5XIo5h$#`ZWKh<;&l6U7Z~RVllga7 zHw{z>l&YkJE8#E!uX0aKWixASS7`D&$&h^Z58|znDFqGeRHywFT6=!$rD8 z?hTS1$MhvAH@b%2nC>_*+R3Vki1D9=NY|DX)`;{{vH%#wZrn6Ug%QANwiswsIi+$6 zz9MrDn*BlGP{HGd<K8g zAXL|W>Z+m%KpnW{MgfPVVCwGVDv(ke@?4!iyXL$o1=ucD8>+vh;42Uw+^d)ei8)qj zQe;=bq1-oD6=8uD&4OxOtDElV*iZyj#+vSHpHAfowB>Qa9b%$(Nk3F(&9mV`dI7DH z@52yzB)Y-M>#{m^3W2gI(}mR*Tw3tbv*Dx-M$G_fIVbcr=eZV4O|NU96n3ChsjW`> zliE(d+#-W!G(76Xzw%tQ4O@YERRuOz+tlMx(Zbo)nF2p~*0T#qs`^#-|V z)lj^{*U1BbMit{wk6CbuLas+L-+1ImCIFX9_CNN6(!b8PfDRF~?#D zYxDY~O#9P0SaWX1zxrazROLp-)+L5TJ({6>?Dw>%CRAa*b%xpuhT8ic`X>8u-F%5$ zS4naqsRa8Xa`hJv^N9vWXDnnarb?Dxp5`AU`U-`J{J(N^btyBga;p>###YkT4)7b-OGfN(K10;c)=2o=_%b{b zM-TG(m{+}e1nQ=e2(x%%Hy001;)#>L@A^1m-28(i_1Jb%WN3F@Ye7777c-}grw(q6 z!PQfHK=cYLbTARajy=c>%mHA}y*Deb1aOb+L}kNJHYIvjB-eD0oPb${ld#>ZKh;|as6XZ9z^ zuwXT}|3VI8eA9}Ypu`NfZvSOG-w!0wq0A$553H9n#h>hOID6taQI7BsdF`=I%My9! z@yE}+@&pp$yaXu3o1rQ^lwW^x{=HX2Dy3r&@rm9As1e z)gyFLlBaNrX)Q@CG~}!=9Gh&Ijzxl%r{D(|-tydm0<-Y;PyiPj~L%)2jt0Z^Z) z5BB|P$Tl(}{HRFoUppuxCYlik(Aq!mpY}|ewB#s&->uuLy_7X!N%!1K^0_JnTq^i#R<@7y zF~fojk=gO1m~Y*D34Du&$Rx|0De%BTCOf>~U0W|EhJ~gqe-=bbXUiwE$QJxi*um-5 z=fWXi2>?9f7-C^RfVLEvw3x!-Sv{?xR&(HFP236~y7+*U_Qxt&zf@NDyElfm92G^q*i1 z*ok_SldnF;-~kFMsWMb%iI2pxevK6ztM!fFLI;>*H8;sXm6bRE6U7HfGHb3v8i6$B zeaylHP}IQ`C^ba8LPS(Tc8VC5ftKEB$=r?KiW5*pT1)Wfn}3WH1PyNgfiZ~}Cmy=_ zg6_M1nXJH=5Rjl{W2O6s@eDA-{)c~!c+QO^3nCf^Vt>-wCSn@aW9Y)nEX1`b(p~3W zOGV|{Ai%PuYA$K&70X1BOV{mmjXKP6PhXJLnmitVF97e)p;#eyptnjYTo(1!W`hYy z3VEG{8w}nSFNsNQS_qeRQ`3?Wb}9oLYLAft1+_Cb(%|CG2F+Y3Qwbz6EPCta9o zZ~1W=Q{+gUVz^~O%Lr2$_tIbWn+xUDfVvuCw>Dz-YRG--xuuuU(^Nu5RnMgJ9!Ldr zr0<>~t+7%pnuQHt#YT;1A^G6}$hB32pSnAK)eI+HZfx`CnsW_cp5*!j8J4tdoa=5w z-*htLe=qKak+~@~rN;L|7#(6hXy2OKQAEZIQi`prGo$BC*98w)*q_0ywz-z#BM(N+ zyDwp0Sk`g42Jbvtr{Q(3OFRr4zB;!q&%CNKn5)lsoDp#MZQqNKir#$BFTq+S<iRS-*lF=kQ9FLVrqSOB5UZ|5(+=ZsX-9kM0z>?WO56_?Ghbw+3+VX z@?!@XHsd36kD?4WbF4T?o+je@mLxDGD~nW(kI8*&;&bsx#fGw;vdEHu-0I01jj5GN zxUoxp`0N4l|5HcqNCp!TckQJ(VN%7@q@tVB(QxC9HPnj_&WLy{X*(|P+(B0*-0mYc zao>)bMk>GuHj(Mgi|)1mT0t_cs%*&;AKk1Yq{DRcnul6QnGewL6tRUsp;5}}Khq!O9{ z9^|47$RSt^E=F8O?;;~LR#7dXBpjtjMM<FlQMQ1Y_$AT zn_DcoOWztFX_bm*cl&D}OS7C=RO=Mqp-EZ7K^&tJT~D>7D9@z*#j2LxF@;dpzeZDo<+U>Yk&p;hQVA5R%PPMqtuRj+a zn-nPA7`t>8Hm-o!t?a5oNrGMqSgO+jV3XyWsF|ZKX`wTPD^i}3QVT`tDY(68YRR_& zp@0GU8U{ApI=05}TO|sp92yUtgO^yyGl(j;mq2k;V#Bu-X4nI7q`^e2g)!Jdq$Ome z0%w(DyJO@c;JFNY6xLd2`Yu`vp|PKFqtp}W@en~&8&sQnp>jeG6?;kt)gvvnY-Q44 z-tr%(q$8KMtW4=R_{58W-|^P(VW0vv^B97_T?NPLk$NC24G1b531Ia!7~4TwRkCYi z-l)=)X-U##BYpRN^U`Kta7exn?;U z;==T_u%*4>H6yud+lBcF`l_2|*6Nt>S(B7WJLSi$Rwb};jH4DnSwM*PL zElf8vR;M%a8AfeTe^=|OjO5gE=Mkk=49(thiQDNKvgW)8DfJ+sreo^7?75zKf2j_` ztul8#T<&66To|E;p`hL^Wkd$KJ~eql;-P=8mneJRC`r-_iragsUDcg+VNFnak;(O# zc`~sQUyOS2JdMtKJlwte=vt$Z5f<|eg3n85V`Z;Hr#4P=*6w=@av+W-yB- znWpMmy4){IKOre&#DrF(dErqgukOC9sSHluy@tY zuzI|M^vr^lFp_`zO@iD1Iea86<8MhZ5PX7(#L+Ta+Yj&-i4YYrDV<1G%s!(wg1E}S zo~q~`9j@E{xgMWS*Ywc>}-+%FdHx)?~a`s4TKlg{XR|8;`QSeIo;N$NxT zokJCGlllg$V#R&z-rQQclM_!QkWRHWu~?-+dHo34ruaw+`OB9?3X=CII+f)9UVYhijt^K zp4gS7B<0wim`)XCt5hVL1{DdbxeEN-lq7@#-jFG#GLj%8gHy@q7}96DfRa(H)CRrS zu$ws4+_&~0`N$$_2d%IA9|Iqm#L?aSjrLxjuz=7LVC-M@?}L|{jHEpiMF}*5K#lfV z#i#yTUYYUVVjt_w=lDo_k+75ty8Emb2_05|<)|VtiQyxE{6K!vh#CM|J_s?*iV`zk z()BJE1`Zeo;~T>d21Ok{>*U`;L%{4-Jv7D!@>`uTSX+g0d@kep1oj8#PY6p9ikxZO zzT`Yg(zmERl+HTRp+9|0rJ`^?^}fCNAiSIq zBJ|@i+B`z=63;lwP;dH?z0%X1e(d)6j@6&Bo2Br5F6v_+L!Y(Svhr|0yq$WN$|Et% zcU&f22u#jci&P<>xV=|fdXkj(&Ku~CNo;&G=k_EpJMVQL%o5`-!oLB27T?BQB@_0i!its2dhlm(HKHN@()w@XN-$5zh0FzI zNMj-Hn!%5*nbAUIxwVc%VjAPZ0IF4KmGh1Zx&#zCC>=pKj@6v{qd-~=9#o4g1f`J0 z(dLvrQoLySr{DN3ZZ&8i6-jWN49gHgdEf^8AW$xBtj>^|_d4P5jo)DphL`yXERW0ZzqAaxMEY-XifR=Ysk}&YN!CahP!van+JJYsaNH>HN*{=rm;q@2ir<+Pbm;gPRtIkwtgWK z4U`6I!boOW`4k3{W7E|-h_9+jF^J2stJAQ+|E5MeT?J6Z>Q8JaXl-3^a&CeAf`3qA zx#Buj-mzN)b(tWITpg6F|QJD-~$cr;oIKbFMJI+<) z+pUb?+Mt_N20BG8yTVlE38}m2o%(Hr@CrK_8=-1az_2la#zl25Vy>G`s?c4A<5E|l zCwUk$aRb1fH}5^abr{4A|J2jDwM^zW6NU zxoSH)!UO^L($qBIheHXWZ_eUeW6n-_NDx6-G#;8Xx zGNG$9yGJG`GgFKMOBUo32N7ZjqwQH@q>2GmbnA$l#QzlM_TgKjHIVY}g?MBH(2gFwc}@&P63Wg1+6?jU^)ksuaBFTiZ<)V}zhXGz_Cg}_-_GQacESWOG3 zFKdQqR7@SJ%{bJ&H0W{Cd4?qwcM@*zs8duSxBpD|Vg?L}v*MSQTZq=R|kfhd)Vk3>uI)P80^ zHj0voFG(T*7aw6JR4Nj&o{Eyljv-h3=-b|`q?H}Ix%5CxU^K+OPog11;V_YP4YpO4 z;9d7`oPPI9sYDDBD-B{o=8pDS3;VAQ@}C$-2{PbMi1c6d52Ihy*Gw9=|LW!=11rHz zbb%6Q(dg?l&7JU(yyhWmYnM=xO-)HU&{iT3v=@n|x16EEN8nhZat;U#wCF3n{w+B# zVM4v^e-2bGJl{+t`*@URYcL;nFrfP42stiwMrTVe^d68u!uUq-ll=ter%Y{=GyaST z*081W+{;$ljivXEBfMb0T5PR}cF@ z`ObL3V>EkYPu$|@rk~kQwnHwF$cLSDseIy`XSV9y$2FLnF6_Uq!Zs#61vRk*yQC{# z*!jC}@OCfU8|g%R;&xx{T|UKIM55TcZa8xwNn`O5Ay#tW6T0IkKBuw8-F`8FZPJd= zm?TXGK%Q*KtdimeZi5~R8Cso76H(!_3QJfb1G8IH*qAWrBCBn~vpNO^m%|AP7dkqx z#jSpVz^$#8)ZWKjszO|{f(D8r?MFa(YODz!Rz6^WG<#~fla}lN`_jlnLdbBUfCrEn z=u*o)*yb3bC=3wbI?vvzXjCw2thnQz57?kWDsj-TXTmHr!v;&ZRw5690unlaEjn>7 z-2~r_ic+hL0)`^s!~nKAmedJyLx?U#He2MVYqJL@KreTJiX~OFw-LAr;Y;*K7gkH& zug>iH30u1b2`+EFR46TcgqTtRdv6)^q;+KNFgBrZAdrw8;=Co8Q7+Ut$=G5K)tDX>O&FraqHJucZ8q7k8bGoz(YCO^M23 za*afg0bNg>iB9wYih}~5`sJo5Sb{$k20pWHx$=4B3NS;P4r*AUU+9y0o~Z(hcWbI+ z_w`fb|Jr{|Z6+}t_bzCRyJ$ewY2V`se+K;yNPd`Tch97cCDq^-BgYxepWYav@yV)l zwLucq?vAS5=;W$%{AAOS9voeEyKZ4#y-=#sbT9}sT&0j|xrC~mX*k`Z=cA!_J#_d3|CxlejcQl>a&KSlj)EGRQ|o^RbJQ|Bn`svhk$NxBg8<<`|gH_|(m=!LDT zbqlXgJ$|uK?9Yz9M)vbRJ+)k6m2`P~m-G}kjxwbPQQ1ajO!~3$+IGnvD{t0x1^v@y zwYg(YapG=?N`2o3uG!HkbViThYR15O;;;R}p=8AS*b-JskW%hP!C@mE?d<9~umV;r zm%sj*BV-83#J{o*nsMD_j$PSHhl1*bAb$D5_-+&9#|z^?qCY*f?exL*i35D+^(>tj3!QpoM-Z~4 ze$P~J7?HKmkZ13XV&&+8BqDi2Gyg9gS7?1lS7sUF@=JCIOddww3fh26=7h+f4f^p2}_4VPpXmJrAHOf5($2-q`` z;@~d_BJe=9VSDS@g}0B&YmbAl!4t2H>IB`G|1hx#(w&d84;f*_5n{r2>$Ui{lZ(C) z&k=*5!g-12h1e>`c7|L2blzw;C0XPnxozU)4S)pVGnO>7ZS}LT8@wKP8$DxF68f0K9cb)uxGT5`TPU;$l%yc zZ^cRzK~8jnCt{k&4hJ8Zm7laXl;lqz2GCNJczH{h5GJxHu^)o#PPjn8a}$JO9|Ni% z&Ww<|2}QyCMF9U<2aO5)7;yfmh6pKGKWYkIL!$it~CB>>?eURhPeWLe8hSSp zw{Cofx0<&&&OPAiT7}!v+KKk0BtIdEf9=Rx6~b4Lka(tRG3v^A99yy7aG43JG$%cy1muR;E6Z`Ct+d-M86GU0=o3iS^+-5XnOf zHJR5YGRcARh!^jXoZ+sONXnX!#CU3hcM$0@Vd+BGay7Z3PiUNh3^nqir@oaWw};5Us~Ux%@O~A9&Lf zAC=h{;eRjR&;mHEusUG_c)OP7V6#+G+Lpd`V6$uyUARn%8c_uKPn`!kYA1N`cV_MK zWs^qgCP^tFHk+$*Z*j(-=`6?L%1N=vFEA0ta>AEYw5upPq;5iPsi!n| zP6n}Sz{YQDl37qD_bNM?6l1`TXO$W#1hX2J*+ksxq&*kBX<~o%~cqOUdPf#J3(B{^^NTN#mTi*bKb$42>Xz6>dtPe=~$gQaWu^~o950u=FAB1QwhaT zIkWfeJU=~CO7?UVQ9AeUA*WG+*(2!+Yn8_L!_#+6%|!|vq#5g@z{*w4VQ-Fkm@~TX zq|I@9w@IXesMz|XLCYnBYNH|)c-YDV?38{f}u z_ecZ$ly4E~!APL0r-P-LFLSD`%MJ2S#w~n2w_YepNd)yz?oZq#wn>G|MTG+93EQnf z&}nQo{!(Hz7FPfHF^Cc}(ji%xW{JvT1uV@-EV35K$X{a4(ScuDbwam-M3B*u%drr! zEIR)E`w1Ufs5rI+SWl}zSgOb7`Hev3@RwLP+ee>*m;gvhg3#~31T&|dCnEWe-$cTb z9!^Kd<(|A90g22_#=VEOlH<`kz*gg<$bkn;opSgP_ zCc-$OleuvHVCO&`6MHk)%AANSyd58z&;(?G%Zwh$M)&-^Ov~mU+`!d>P%)u^_avuS zs0|?@hV{T^<_(-vP90c5+)cp9jw`{0H6B4b{&<{g-i=Q>PY|KWs63+h5SrlKSMZUz z1yS^TL^HxRgCPfQ68CmSfi%Rppskb#CIY*#cd z?C}n5`2s#NQ6f-=kNo#|XO!CrG9Jx{Uk5# z*L+09@z-emNC13q4Xq2_J6Rf&;im%mQIP=c4aMisgy}`s?>;#}>7PUR4c+ZaIa1W7 zNklt+G(>O2{<+4aNyQyn>oNJY4?z9aK`cE#AqbknAuPk+6O;^09jBFEl%Xa5}Fh^DR`F4 zW1-Qa>@vxQtD_~MuVE8K2F~zCjHwvCM5h3sKw=RcuZj_f3u}vcLxWFcz{t8?D3YsO zn6f>00{z z8bI6i!$P8lZoNdcYSX-?FhM?ChTX*w!JHz#x(hxcectBei<#C9z)`JU33^`>Je&t6gI_jO+-(TwSD&Nu>Q2vZMA>=9y0$`M z?xIJYrL?(FA4Y(Qu-9^Tk6!>=x{|rYnq2; zl;WchW?Et>8ppzu2ElRvX)qlqrp+ob*N*EdKsUNMzt+2x7Y8WS_`FT4&P~ThYM^}2 zj=L-GaZOcZ_4RULE=&h?DnDTaF}M4AUmn{euV;CncVT60nG~@jw*Sx_X#||neX14* zW&RWyzAZDd>ANPq-yrRAY*<{9F{EViNS6wLCy{H7b|#``A&QTU-*KTDytAI7TvB&; zNuGa5UJQrr(PQR?Edrvao~3>y@~#!>aACTjBNVCiXqm6!sjdq95$(#odeFYkrkkq* z@0~YdXPIMzECqxMHO$B;)^=satt+WH%EURak6BmU z2=~wE*{@#iu^C$E0jBPpI?u>6#_ORIK7C{io3)S^#`9asD=NoC7a?2npZi?-#XHG;e;H^;B!(OX1@NtSa) zO_t*z_T#0jDJP2xO=HOpfal5b_H)t>#+#l`_mlgqr_7{9syunqkLpt#B{jEn3urQi z?7uc%?FVj1h7@*j<-;DYlQ+W@0{%tZedk#GKC&h2N&;kY=XvZl`V_`X480PpScb~G zR&sAoX2!c#&?zL|uLb??1e=OP@v&X8qkt16u;b_;?=OZ;Ax=ly6v(5rOe zw{On82Ar8UA6GcgxZw)M?J+H%4V$Bu3Pi4?p{I!g-+7Ch{ zO+ykeD#^#U%{;tq`h(+3QF;^y6F$PI10R`qvNPGb{(lZ+D9^LnXl7OlwAs_Xw}n21fYd(VP*v2UMg%%C4RIE9bf@R82dZW!rDFhNr=4B8m@ zw_Qjm5M!^!Il9E=3&J!}l;qE)8AcLjb|SjRGQ96aJqFE)WBw!P$>d~Hm6WVh>CG$x z+8dTb>`Sx>jnyanp>UPy;|U*WT}Bh4)+v8wE-PM8A8> zN0po>wyE->3c8-J>(ioSB)s7(k4EukyyDSYMEp2)=%(mO?%T+o)-pt*LQBF6J}FiuHmX!6 zojrE`-qnfeamO{jfpfS11e|040}^wxo8{@SDITT)c!iO&6fz;;^j)~rpUpe6dbTRT z8IVR!y#fsIKrh))S>qxlt-=YerDPU;Yki@Dwv0`oMcyU^SuZ7cgItpc$C9*uVrPH=!hgh~jU)hsLs zHt}lEqEr=T^zDGIt(Sbd`C85Qkg|xkdp&j>|;c!#N(cd%vzU3 zG59zuur6R;4fY6ymT0^!+@YIP@@l^ZU28)%1|IG<=UxTOJRT;C5+`Sf$8~nqxq8tS zJJFJ?iuNJT1~GdXWu2Z{rrH3-sdF;vj;mTH8@GF4lyn}Vh85kY+v#5OYmuhhE0^#h zUvn;?`{~MM*cM!ENd081af@HS?x;4mE0a0b>P{(o0)RDGVRhR3(^J~4&7U#Bs*~C< zsWz3+XRSInL77&a+l|y^Eip2hN`KVN3%OYLHDx^ir&pchJxcv-#@3q6OkB@$Vyrd0 z)LAU7n_8xK&bwW5lw`7^XfvwD$NR2#nrgDT{ay-lT7E!zv}UUrp&q^%5}4L=(%b>z zZBHL{s=+w8g#v!nTIF+4XWUV1%~2}x%mC_?DWHB)%e22{C+{^5C|_zlihAer3|G-B z`#hz?@Madfwu6tR?^jZP3pU+7!92v-F!jMh|#ETLNs(9 zuoUAwh4`$DhAY!yev5a6OR^ZO*Ui29;dEdC(~P>2!fw%ThfaB~jh>&3XA00uqHI#w zjFqTa7(hC{#Mctz_x7u}D$E?55(SqDhs=7s3*%?O(Obe17!@X=87V%JV+s+}pq$nb zjbb8Yxlxt_%}4s=B0?CAxFVFJ_23U;>vixC?_wOG7)T16F6@Iv76QUD1neiQ`@{QK zN!HN57gAF;(z)c1@6oNqPZMS6^cn9y-z(BTzh z>HV?JdUzX1W)WmRkCj*fCCCoOe)|P=vR|s$VzFf!s1yCj@822H$G`VtmS7$$;%+^A zkmU@fFpDhgyqvr);Vb1d-1XB1S-?23-@PLC&kx=xXn++t&hJTda&N-$79UA`U5P*5 zeZfJ@B;4JfxYf}E0%|d$CgR3u!bJ98ho10=A&EM5&sz2j0rq#_pnfYmUjPPp3k)i_ zkcjjvY*$C_l1>82;-g)D>xjwh`^RG?UU?;SDxN!hcG{)AT}Xshd+RwdmSeSrfV3k? z1=_+&5^<~eNMZ;X)qwIqYR))Gn*iy+=Etb@vt1aX)z+pQ(UBsvZ_IhI!g z<7gj~xP;Ue6zwUU5RgXH?AF3@n738N6Z@GDPy8cDFIIA9fb@K{`1UF8Z&9znzIKc)vQErL~%c`tuOkAk8rmU{n#eD0zT5(r8e2Bk&HTt$zPjn zc?H5pcuo}`DUIas$kHQ)D3z!T;K9H}L*n7mCR_dt=@_iA8s`Jg!OsP-$AfTz0aUk6 zA-H{T%82^li?*dSWKy62f3cXZr}YI*D<)0qG$;+=3*--I-zEL$NR*bs{-w?CP7P*V zvB)a+;c}{(j^$N6qK^ljGFWYu`Bo`PnwWGimMBE3<*{YxF z;DnOoOPeiWwVH+uavl`JlY^MV{wje-@@Muj)AFa2O;Q}kWJ`H=v%)wF7~&(qViOam zA$=>dk(iI-BX80{KU(3w%d)h-1ME~|!9_X|bJJ}3CJ2Q7!XetysA#htBvE0n73F({ zE{v$BQYg|yNBF~Ok%_6G(+_SCon!U^nL4-0T_RxDEG6}kq*YD6r$=v&MdiH-Z!Qd1 z%z+p$iS)~*7n=QsIE@Fr>UcqMR7*M^&Zf-F=b z_z;!8FF_C#mayS=}h-}7^Pgsn1@W53!G(Oo!~ zJAUOPEeZO=52&)V2WDr)8f=#tJm}(2=Yt#La2?#$IG@hTzy_vri!Q5=;M>CLGHF6k z1hxQ=L2KCK0f?;I@W$gf&`~{*l8JCRJU>BhdhIz;z_m)ASsn4aN;JTqHY1FYInXLe zKpZFuYXPWnPQgP9qjt*!Rtv4!I|Bh~iQ7ej-L;jlIuDov!aA*r7Oy+91h6fSIZ&1N zko1A4F2ki$1^Bh!;Dj|7B84O!7@SnQw(ZBxBd>0_+ieT4gjzG;1CoSGyK*N3{!5*q zF$P5CeoA*l+$ybvk!h@MBm-GJYz4N5V(_OxUK#@u7i&lh{Ylf|#VplZ?u9=h+VvBd z>U_^{O0vtUE_g?}9)D`8XVrZ}MhfH)tg5=8%}Uy80AnPkX2X~U)U^}LoCcKfTh|;k zUU+_fD&vNcnb3R(v~XP>kG!D;zw;o`vbjZlIy6K5_Ay<9)oa8&6^6pC1$F@7F?V-K z)Z%`kiY6Gw7)Ivd$*|~=sn1hHi%J2?QmgIgYDNr&Pgys<*;YSt9)QV1;wkNSL=B^2 zV!7w7$J4F7yl&L5lKYxU1KT6-ydmP77EMs;KKi-M`MRW^kW@nXSgMfji$7Ze}oDG3$xWI~5^MA}TnelIXZ8RqtOLHwp$zJiZ^=#0i43fFp^; z$O>gS3k4v8D@xMVSy=>mQ2uR9f@YZ>mdlbc7Ge?QW^qa7-fmU>>+d3k1Ay_4h~qv8 z7{)##$78_Ex*&^VJTCt*(33C}i>4*D?q~^apG_-lmRc5E*b29IC>lF>>sfLtOkpAO7n+dpya!if5b@a*9jlzswIsxZ zb(v_!8c!7Y{;SNx!h`6H8W`{>dFl>yiQ+>JM5#tX9#zX#ze1uRDTyBZz~+U!u8$W9 zp*M|>D8wA_PdqX6T2cl?M9Fr7M+q}93a^CmJkDNA7t$A$P;}bi1)pfl7EeAGWyVtn^ys5JL>HC){mYJ2CcAkc*l6`OuhsDLAD6&Pg#fFWI`OAt1#&28GB-sao0Lq;0}47B5FVihqrEUkNpwQK}K>V%@w9 z)RLid+$|r+t@J*Iv4~+LKQ`#)MA{Sf@#kbDi6p0$CcdApV#>uc zsH|j(BW8c|y4OGEn(sVM=^Wcp3S3dvPgLaa|U}Ru6gv)Bo$QEbnZ0QtYo9Vg@E<2@Ya=JV|2{7yYpv z5*YC`iPhHLtL29!=MgKIXdN(ykOKzrfS4N^#Ml8YAUuRszS}EaeH(rn& z(*Id-NE)Wznq1Y;;L8&!y3E!jv-P zPAi+gIaqO&u~1B*)>n{E1|)b*Q3h2(J^=<8Qvw0Co2MwOgUz<}+D`!&ab6fwJiv38=N@+xGsnx1l7)s;`P678Blk zMRI1}ubj6jU}dD{R>mZ9fsA6y?Y4Z^bm(CETiq-HZ?p_h3j7S%z=UPb-yY}z-jM)H zxKN%Wfmpy5MrJesJP9&_OIy(pyOAP{%jInhN{r`nuASTdeM1_kS-{?009yVm)D$(! z+?r)DQh!4@rr@Y&?9)|eyM=0l(-4ZbpP}c%B9Y>{u))8nQ3-(R)_r&NfR4t^UL^)3 zm{9x6{?M`VZkLxOangd8qduEj<~WZT$PK8k${VBxKRS%1ws#O^xb7X2#X5})Y zk#VSz>8VkbOPo=IhRv^8W_ZyDH_{|Gg-CISQ}k}*G-;I~Qh&zWi__=Br_M2Mh6Sqk zlk<&-n|b*)SY7JCYPT^& z9yH@mM4s+^6L_nzcUGT(SDFP$DB0LpW3-=2&Gm;C#vP zLNu(NG$xkLKUC0B3$8YlH%gY^!Hi_EJ&2VNw}^M2pMO2^sm{E?m*mM<4sa(aK62*e z2czy0H0g<*;%;FFg1tnrLslc}*8Xe7jieGv`a&UU$@c`IdjF-^Eih*igo+81b-B0+ zFkjvl$xv^67T$T0S=a2_&r2Mcc>9*ogDVMzS}sI$o%k^KtYxDmj7;o^?w* z+HPd!#rR?V&8ITdZ6$>Sm23IJM@a}0tBDsMlm-R-o<6*lWwmt~hf*g+31ULqLhfBX zS{usq;1-!#bb*%#e}t2J(Gw$&pmWwhMf@|j%;4Iv_HPve6iOm!TW@gB#t+Q$UK33in(Y>w`$T(SLIAKyTN~cnly4^PxaY@Gy|K+zPJYc&S14we576G(U7Xj zzDpJ#8EB4^6IK#-Q|hD?sL`8@;qa#x7MokNMr;3(`Z(uPiF6RoAS1r6#Wix=AJzJz z=A&Awli1(pxB?VlA|+;txhC^hZ#^@uH;#(+$4twMA1(RY&(EBAip^)#!6%;L^>!K= z;Y#-MV{wHkjTG2tAp@{0!#L@8FK56H ztO)s#Qd(P;!2)$2wjY@7V$9gwIfG-Z+iRixl;s}8|>HNk-vQ0fB6RA4Gdg3!)alJ?mn_PgA zLC#=#R_7R&^tBdieSvT+^r%qQZgyFfNkpgUGFS*peeUdu@0g|p%u-K z{JG`36db3v{2-)BPLuWDNf^<%O%l8p8R5v2kZ86w)c2?PGw!{P?gQh91=k+hlj~V2&+faHw{U5YjM$a+GdRo$=XraS&0# z9yK`VV?WKTTGlndW+hrbCvdLn40dhGsp?xr$hQ)Bj`5;&@)yz6MY+4Js$rXrPtbz$se>Meb zpXsQ=dY#*WzVP14FdFAmAtqjhA=2mwFp-tPhst+r+12#8Q671Kodh^NZ~6tD+CXA* zY`v?i(hT*a{lVMAub^@_BO}id$%U(nIw|1gYK-mOsn*)-RLF<{F%jK7JWf=Bl_3J% z$k32-g~*@llG_E0cSm=3c}8s01x8*CTAuURQ(dcmu)ozzIF8E=0+6hXcRY-y*+-3= zV=ZZVn`f*v1uDe0@>&_sn9{jjW?!``cNZnqQ|-(rC1Z;WYM}H~=%0xUF#s&2o%`Sk## zlA>neN3{*gj;h(i+$WXHSGROYX@i!_pHltmGNFp~Mpye6A8(iE`Jo=#cCq#+IL3Wz z5>lR358q?~Z=c*9&HMWh_=zS3G$XVB8LO>qfBkG+-xAYVF?&JJeq8;t+SV$cwYXQ< zA{1dX#Y$o!kPwtvMh6ZO(}INIYEP5=3Y^Ej>qEC@E`k9eM+vQ83vP_HWb_MimPGiO zKxNBlR?=r9(w9u!@<%(sGRm6x=q>D!X%k|EeETn`n;E#u*+kSsdc#M9n|UPC(T_Y4 z+i9>IL`Rp(tU9eN7~k&gv>0I!co3_qx1VzhL1sX1pXL7QvZ}I}x+!>^AV?2wiRe!@ zMug(QP3X^=$8Q1r$P5V>`GLk>i*BNFrc=sAVpmq?Tjfk0m+HnLO*fLMq&!V}51gdfxHWhVwP_}zW`Ut(t`x48 zF6PLd)3i%p^e-k`SN+=%=C^=7NN*{Rr$z(*qqjRoNrE01wGFOY{Nsx%{wJLC?xaa$ z$ggT%QXv-&8OVq(torx=nvYDJN7`NsC4mjH#XhrCQAyPP#e9SU6f4QA*Vd9T%^DMq z)IqRu+%86vBE&b441wpxk4m%|frI`6IOyXxa|)7=sZvF+nkX7Z!!2>BqBLnHvV@OJ zXiKhb44)h2j=6hNq;AznQu|sLFdl7H>4Rq|anO`D{8)?-uvziw7cLz=G#gU{EGcASv-vK0r3 zSmU*Lk>q>w@Rgs|YK{@BhLujF%bdhYcd@L{Ak;XlG^OM;SoirEDE z*gm}7_mB+_3*mS#g0=T;^jRZg@saecrs|DP-y=6UdHU{^fx+9p&ki;)n_os*E|qL8 z7Fm{Huz}#zy2f~>mqLO#WF#Q`=F4ydKGXp7%43u0#U@{`szuE%fU#Z&duLdfbX`FK}w~00cIloE;AkrJ!aug_=LDJ zFp|OlmTzYexTxH68I+scD_f-hNIE%?gXB48S5zK}Ja(Zz+Kfp;>~6b=-)Nj1#oCIp z39CsPa*;YU-;KSP_Iwgl<3@ z2fC^D=w06>c5YNHRf9GJdv8n#g}!e_zF@h%=gP?EJjPT2cwtSD?I$aX@r?}8Pr86I z`$=K5X76^Ix>#q^v;gUV|47ISEoT6=mV^BIs6vKKk20b8)7=fW&QY}}Z;mx%ASk)lg z_oLD^bh9<1z5}Vj?V@%mhN8|CtLNEI{>)&G^*GQ|xA0YLMy$3bDSnExiQ3pehMXd{ zxlr$VG<@qk7%o>~jLoY1_7TNYwfW3?wu@ew3vF;Co1S{K{uaNUj0T3APYU&AjhIpL zOe*F;YeQ<*#Lq?URF!82`1n<~`@xOTb z4nRG+1JLXCpNT-o7mrF>W-e^nigSANx4GB(idsfv+C<+WU@TIg`&!5iU-Rr4m?bPf zwaD|c?Qth~&z{h3&8eRv{+&q?1P2!8ec@eso|o|<{_FknCX)SZb2c6fU%xTvw7+(*o) zkGw@<2mu>#E$1mi6dB)AF}t3pDvC2Z@Suf~2src3vH5os%rj&A@te&@Vi$*r{Pvyj z5g8I&OM;P!Dpsr{M5f$H5;UMItaR;QM!-pVso+*olpG5y(J#cL`ABpnZM9Xt?U8v# z4!;AN$kw!CqA?kLKw5_~*&EoQd_{r^yW#AhLL7vNQCS&b)4fd#IPhivuKvHGRv1xz zxCxnL>qqSIkv{gsE-d`(0{$*UygyNnIA}h?C3b1cHGcJ3|Ajw$lM%|1FV~OMOHs*dS|`t1Jo~T%u<(3=t+?jpMuCDD(=(ZT~a({DUcp9cB5{nKKQ(C znS39B`BDAR+K}FNfZSG*49CS;nh^x*yM>hH%|!a6q|IqVB`z;+k`GjhjoWb50Q>us zrB&Uq`dF0~qg*^fF~Ty2GyUg;k1YPFOle7qm4ujtkF=u1wFsBYEM=e8q^9pRxoQ3s zmw3q#4L?oVJYgkGzWQ70IYArgug#S2YI2V75Ru$4e1vIMC`oyau@MCQ z?rraW-uismUi0M)AK|fH{^?@t<X6$SmaD`t0HnOqe_=m#q3 z2R-tArAJQU*DXSJ@EIZJhjwN*e61qrhJe_t<`%&P$0b*^($#u>4&f(2A~hk<&eJ6R zCIUL`_$m26%D}5y@Iz-H^O>Dj0Ou+X@o?wWbV4+%(m}EFC(^KPoj>RQnAll-&u_odnT}o0TjbQ<>wH@Xlv=5s>{lYI+`EPqF_%UTYiACwB>EF^ z#T1KjPn(})mv>$pVW2?G^hYra%Z57PVv!^>6N+!ea;75${S=Q)fNn2wp)Fd90#l}=Vh<{0cQqHQH4rL{RQ1ureR|)!-8&%;zHS}}!vX=AfV5mnSa8Zm3e%BqVkD!`1 zt5aMf?NqGm0ppuSX_DJH=|F>1t(LI20eV-S4FtNxL3cr?8gTYW)oRI^sk7}3mp0ox z`GB+`nwdJQd!b2%FoVg7wcRL#yyhGT-x~+GI@9g`+tX`IPfA}++xS5q_8(5d~!%}lDUv)N$l#%g_ z)d+O8JvhqDPMwvGvq|$gD*H68RdaVi4HfzXUCuVXaN1`A<;XxGjz6GUaT-x75MMF*qgO}=KU1f=pmu>^Z+L~+lAVI#32!` z(vq;zqGSG?+j3EEl>Da}aIW@io5@O@tU3u>YCytCP3}~9hl6sQ$wT6eR$I!~!;BGk zPyLiOY`W06)$<&J$==eZW`XM5g7WrJ3lo6KZl{?-RR08MqVl$*ft3qBM@d*rmB=(E zTbBFakP=Z!%3RsdvicJejw|WFRzR96>Du6&ia817M@(l~^_7BK{J9-G}<&?OA!5ezkYG+88ylXmWQ^zu0E;?2seuxZH(_=#^yg zG%3PQ9la}wEzppIt3hZuoHarmGat5XC+=gimFVDx*qclEFI$l23-jSZ#IqGE&p%fReHO#morXE#@3rjpLby}dl#qbPZLFPhNdwh)JbM{rdbG$v7+L~qjE#NhPSY~{1IST1QM zGNB>t!BY9tdpW=&K5l0_{n6WBNJpaQxZ(c`A8F5#A|^hbj7)}LR!v5zOSl6sQILFL zZU5Yp-i}c@5)-oaZSmPGJ~FgF&iLnineK-FNbI$Q)vA5zM}~AQ9b&8PwdNlDjJ;M& z7DqlZC`Up`Vyu-{KKoKbN0yyFD;IKtq^u}BBTB4{Oa%I;-5Jm<}wl!A5X`V zPiS>5YN<>|Q$?bo(qv&JyzplK@`q3lx)Mr|a%@i^kqX+3kK`j(TgtTC2%}jaUSHi0 zU-5z`F{`f%zc$-zk9D~3k~ut_3?ovYauTVC9Gu0x^1fiC5YEK?kfi0%R!)fYw)yjs zsDQnjj7S%r-sf{iF61?*>P=k7+kXip(=1R=b7Leag6erX#}i@ybwF$+@b^u{X>RY; z6xm@GB1Z{lN(fvF9~%Y-kQI;s>EZ~8vdCTfcSD(l_N zKb9%}_|ix8WRWcF%dtO<&`uIEb6&h@GOhD*EgmmQ=>Ow8P2F_*Oh=h(2m!L z6E(m4N@+EsFOcYJ{|y1&d#^ir`!53cE7?aZrZ!!K^>D$c^o!?H$VRtv)Azx02Yk9y z;ihlU8nDQUZ8jiU<}PAeL0M7FY)F>9unY?pIWy&maQ!#^NQ0vfy7f|g${97poCfvS z{vYUuD4I6KEO{I{u#C8iYO^RSMKrKHc?{7sl!}EFt1VM7v+HL(&?%D6+;L^5qr&!{ z-r=gt`GepW^LO8nAnga&Syd_hF;FV4$oihx6|-lPd-#?dJg|vts&MyMNrvUX3N9qG zyMNAt3$4@Uxh*j#i^@qZ_)QlAOVMZmIh!u{`-{QeE^DAbw;SA{|HYrh8a1r`Etx^}}kFr+R=l`sO@0QJ%$=}W+2+1q$i)I-V* zwYskYf!2lR4I_r4fDSs7cM4z7)a`Q0=bb{MT;kW5X+37R(Tq>M2!X{BwSa~jAvkZMp&4tE$lqd_FG0rYgty>ITUKz~q0WkRZf|H7= zxeKTPm$%P6(#0g|Y~=76SY77264#6MWhoydL%pf_8dD z>p8LS)_Nq=oL;M6CL=6Eb{vn_aw2 z3stM0be0-dSS@*@ihi2qWK9CmkKC59jpXy_1{s`+!MM&2OMF;Kwjv{0RqJ5*(@M&0pccfa12|a&DaGlya% z_EeC$ty1`DPc$juI*x$Lux=7L>Z5mJiIhxUFTlw$zG5@*#O)eIS;Xw2b73FFkx8xpzqS%&)P_XiLK`Bo@yJ0km;!t~dw^bM`r~&#PZrwV3U+5-F_4k}2HR)5;s0%& zNO_Kw|2X`w@Bi9=8i0>XTw0os2u)mV9w-%VLdN(dwqz4Nf{FAd-tS`M(qS2E8K)--1~EY$BD-kE;D=|iWf9H2uaUY1wYBt$8aORtT0P?5CZR`eq!S7~`p_7ndRYG_(%lAO1mC2#Rr z^F3jmg_0m6KBQzj2?-)FU~U$_+>m@(} zq%phloJe=AXf4!T5rFC&ZjG}nTFw1w{SBA_<53t!N(_)F3s^c;pxUT5ETFgi2c7uz zo~t@xUrd#x?UPLWmc*ISwg5=saW;HAp=nrNp(lK$w_Z|ef-9K&CRWL=EBMzh?7zCb z`4$eW)HM;nqAuC>QWD6apgMo~-Bsk=^!F${>bh zHm%ZHC+V9$-Svy`!QEHJ_guL6JAX|^VIN5<+G@~&Uq?$&00;_%p z{ipM3Hkh{pa$6RaM9KF1UJyPZSRPT_N+R_JjZAB*N!_D`DW*zMsiaQM@&)<0{M2Zq z<&93Yu(7BxoFT`7_kW@keC%7){3469xUm?lYR6=YJNHI6fGggQ?m19QL zm+qH8Gb8CHN?4;*1L)6|-e-`LedFr^y)~qT8LofIxXry0P^ zj$;Se7wVp=QPNI>F%X zvTuk4{a$f)#;sVJ18kzJRXEMt9HQh*-q1RrFG!vEX6Xg$UYU{HV#x!havKUcX`b=# z8met)V;0q;JWi=!n6D-;9&FETJwNV(kDprgmAYNHPadsmn7ztt5YkQs>iJn-&Bz&# z*|d-3ns3yT7J}@l?vg4olAROQ-m`?-oL(3H?!E>s2c;-+ppiWtJ>VwP;H%BlSFv!D zd1Y3eZLDhga=DXQ>)Ix>D2z@esZgqok}JM%yRP&V@->6W^SS0XxAA*tc3&QcYZ7nq zlpy@L5`>Rr<@$3xAG2naPl5F-D*oSBmXOX)2%k(~27r+je3UVc=jT7Mj1JL9h(kq8R-;O#P9f7=l1of;7G4#D{-?iJ*Ui7>2%>Zd7qr|8nl5BeGlM`;O8i1pil7IE}}H3USF5DHv*-v%G~ zg?+z@*L;@g4Pwh6BWIr33qo@OMM>i1LO&&ZjcP+l%CjS8Z0|=MXIvn6?=FkCjyAkLIC$V3m4lA}46gI0xvgGj>oOfiwN z5JT47bmrstzLZRuqn}lLEx;Zz$^PF4=aI(zy?^`Ym)7vl{-LN-><i6&Zk5suIVVe-*%&>$ZLC=N1!|0R3`H<_>!ADqcI zXMpSDXQ#hQp+c1lY2ZY4yOgXuI;gE80fsmfcTgwFP&KFW+WKJWUc=_6>+VHQ($4y` zFLYHDJxjialbbHR^(ey`oJx|hg`&i*tA*XXoaqMJk9;Jfie8x%q|e~veWDi@Hyl_= z<%;rk^#KVnVFxQh2oLwl>*SNtW?DH}_F5!4`N(|ua=t%#$Dhl$Lbh2^L6~FVAkRek zn3tNR*3Dhw5PL_O#VMF?X{xYYjh1&`S=lizr!|O* z;g%nA?uI}75+|i9m3>OcSjo;vXABp1D z3pMHF)*po*2`_)kW!mZd^i4kw`?&22ijUJ6(4Jpv=iJ?E>6_BLz1Nzfa0;2Qn8xkn zcuuqP>nJ+}RZj56#BZeEk9Gsy;2y%=Oj4%@Z?tkTHWpSz0qTBBD#nqLkMUdFwNU2A z(kLfBvxHe2Wl`)(V?uM3^%rq7bUQDN6A<#quNGyic{fv{1VGDyVdV28gMbfB zsEM9HonfMjhXU;}n;9{AW7>xC(A|J|g$0SBiyGhQR z6x$hu+x#8mHv;f#gT2bZQ3sxI$AzndT@!JdHeprBl$NWxVRK*Gu+vXA=$b)}YY~t- zvm6=0K@j2Q?|6js8H(|yIS{UP#o0<4VSj2BZH_Ler{H?`BF)ogy-1hW1m9vz8rjXz zNLWjGEmTrHs<4G-bWHHs6i9wF}WJ#r$d3CO`EQ=oI~0KN1*I z-#Vitmj~NCW6pbP#aRYW`HW2I-2nauUO^YAS0fC7WO$1T-?}P2*_AfD4%RQik7$r(j@*Q?vuE_cu<^25Q^iouRhYcerOZijYSYB&QTrq z{^-p(w*3BsBpv47{+Ey9mC3NGnpdmptI-+=uJvqU&n6S%DY6X2wFsB202+KMa1$e_sp{6vd2)8-;l~i{zS2-p1+p?w!O$R%}Q8pkx!ZhgAf;z z(V;%Wf^+@mwU7(Ck9U$90BnfmRVzEh>SE( zA0-IIZm6Ohg#9rRtBmc@G?k)i?l-TFWTc|*R20xbOGUR5N|K-fvF=Fho!FUoKww;{ z-AUriQdV2#PBK_h1=2``Hgc0NizVD-Bq>x{gC|HhNGUUn;2^y-mIz3N3?(7 zBV^8B@^9oL>*B*+$w%hPlfC2pc6_aok9>hYd!TH`wQH2y`!J4pD?qp_v zO_YI9uC2>+pnTDpGA@c+M}TyH1SWQ#tt)BZU7Cf7(|TZkTTInK&QJJA6PCjKLG1kr z@;PeCQ{SaU8sbkJq>J^99EX*ZYNjhG>QMwFRAOKjMK>l)q)+yr&6Sq!bACckiq{m! zDVj6(G5liUiQjEl+Nx^XphKzi>6$Sas8fAQYXB!CF&|+unGJGOBngQYFOr03ZxzW9 zM?K$IUoa+H@{HI>oJ#GBn+z6QgLUW$l81a%d}_#oPCPDvo*e7m+{jFxy_?$_=>ZjR zw7`$w))qSnta0~>NZNN@!TxIc)+>^Af-}WkD+N>XdE~bqSAsoPo!N0IzX_h#fOo*g z`mgyQbGo#cTGrtK@vQs=x2hs~g zl}J+&!EL#SU6#PS0!GE1gcfG!PuV7&*?(PBKKdGfCD+74XKg)Z)Ye8CyoTU-S)#z zZvG*cjbLvK1@po$II}aAS${q&Vnz!Rvd6M(!`FLDmXJ-~1h5e|03P+QWUJ7H39GCA z-Nl~v{QT7J@FNUs=B{5`>^S4h-u3H+dsa&9f=5`A#3u~7h;zpZ`g(hGBO0s6Zb2(> zOhO@hY{F-mP)JU$2-c*9di=JEow9ZI@D^kwqBaJ}^xDtd&3>44D2r^qS9^VjbL?6x z9R&*WfP2@(k>sxH6IpB5&*tyATK8ipzU4ct#kj7}L&dX(DOy>8z(|b%7iY6DVMO91 zc7myb?h4f_2HeW_NQJQkE$0bo>bey2DLhs?KurirJsv_>%jM9WKO5>VdENs1`@(+A z6g$20f5m-oM2{Na0S;1au2kpK!T@9Yx}C62h*BX&=ap0s44LB**WqI7aj*PTm8jEz z6d(*InhZk8tE*w%3T5nU$rv)U?MnA_#uuNDq;k`jUbWkA%+X18C|WKH=rub9__n>gcgl{ZQ! zwdhAkUz`d4r#mPxZ%F;BB+8&F?bQ9cc6qb5kSq@pL#a~bs3Cn9g?sFao|ft%<7~r{ z@=_O*o%GS|x;@StSnA*|oKao!Tn#*HEj7yU{Pe>xQl899kCl&Sm~uUMNT?fm&ig0z z_oLW%7ga!pBVS8(dW5`9x=;ja(3f>f^xCl4Pukqw*CcOfmoZlTT=`QsyQD`eVaKA? zDt<@nxA{9HRMe)7SA7{~SdAWtc!UuVPeZFuaI7dRd$4?_`Dp`a{;b`Lw&k@RQ`ug}(KD33uNqu1Vgrgl#3UW9ouc^rzCq%{|K*~gXZWDI^4r4<7z6GUAVDv_`QF)E`sdHtD$DQ2mKwY4`5-Sr*?Z+vYXQ<(WmwVi@|IHgwk(62_v(P>!4tTYfW4o$s%}1h(ks&3N zBpKTigr|ak#&Amr(24^bve1t0t;;C?j$j*!t9B8Bjh)exPv|J>FFZmaHtuT>wvGl%oeU9%>zWe4QOZ6iOV)hU5enr)bZq$oO z+2K`9#P7<|9<18|?uJx)Bnc6-*FF|O5bxrjO=_x!(z7%->5oPuYV;)5R*Tr1OGs%J zM*YZAZqj6=&9?^RUowy;8mXlCNbN^&`l`9?xLO;MeMmv=`+L={AA$W#G$TvN#nNN< zo&4Dci~Hgu{!BfSt*A|iNmMYC`PvypFviM&t694V7O|MVeZ(p#Ja1=jOzku}u^pZTbda0RbO zqNP_KiR46nL%Lleaq;mUPCVXZDm-;8VbOHi>D^a}RZ9yHvH$%WPv5^m z=Foi4S(M$g>eSw=B^)PZq6(Q2IE(8&25Y>17|=$`WqT_0WS< z?o{8*1XZ(mJ(JMx_Q^QIpUD2Hz0*(bnLfNDHhZ|+eG%DZ=_NrA9Cn7i@2KwWAcC`llaA2pNlZa)fN{

Uz=pkAW;Ig_|8-Hh6(CN4LvfB@-o3DWg3@3^U{Gb&NirXx|lkg>1(y8C+T-A-%M z$a2GRQsMN-`%>fFol;c<9mE~mFG>sjv?49{tM=Hp-H}@icdXG!dy0CvPzzIeJwMgE zbhF3JNi%3P6fjfy)$DBi4i?aZ>;cQ584zj|q+rEa!r3WKI;1cox2sqRT?Rd>j}ul~-nLnG%op?I=e+cqNYTZ$3M7 z^lrN2NO+#UGap#W5n=5QZJ}+CC!^Q}EAoD23d%{1tSMGodw$+u^=OvqIEcRlt~|oi zd0(XE#~)`<&98(NnNTxvdw=OcKb?MLCv%r5K@bxP5#k`L^TRhWukG)ACaZR1fgeN3avqsR}#RF8|>pd}mMw3Yzy5`N{+# z6tNsbF5Zax17!?da?2a5!Rp;vA33VH7Cc%zg#Ryae- z_s93zObaXNEq@LvNXFp0z&|>Ge*UZenb~eb_{iWzB5he&myMbc&HlxIkHq^+{!PF4 z3Jx;a2qHStZ}`Z4GTm=xKQs5xjRd4aIOPf*K;{XAB>zZ}ky$yu{}!)=WNGggRljIaJ$l*lC-n8)R>Mn@y8EmfB#-siLDeRiTKnppem(!1Li1M+PDkfX^lzwl{80a zkl&=^GnAxMBz-1jp#2F}ua8etkHN#ebt3kyANgddBY9bt7Dt$ng@KYJPH*|7b4#BX zWjUVcN=8c3jkM3fSaSGN%_G5SCQ_Q0!J>8G8k4(^WMm0p84O>Q><$k7@|hiUOci0E zbx%IFrm3vv7PTZL?LiMs%^Wwh!MgQbVxO4ZR_KyG89lh!ERVcsNPT|GL^GP!ckZ~$*ekD|FDF`l`LsEA9gy9p7fRIy` zDl1x+NK5&~?_`;)b-Y(P7}9}t(6UvF;Mt&*C78!()^sAc{2DtMCmXBeOJ0DgeRbPW zl!}DsW~^`pq>}irqiiwR-EnDrMp$YQPtj`}TmxpOBm5xbUKAsd%ftN}LpRtG#P{KP zBBoP!udzBn`_Y5nc@1~_Av^6M;np{S_h?8=o(^xbWI*!N3a9aud)G5vqhxVAjwfg2 zG`9xrx--lN}SXAm9So5M9nDc#<;75g;k_1aDqh>XOrkb_FY2@V|dRR(EGz;icd$K>m321i|B24w74cQypgQI(=D~7X6eBphc1!v=G zq!>QJlG*y0C*<(<*}JD=)pqA~G*Aro!lwW;)_RpW1rdurD>EL?3y%k^2<4h(um^^B zU}FrwAb>4P7|G%bnS(%9^FdVsS=~?p*6J!xMHm4cTv3VCDT{hRP(TZoBf^a+e(**) z%hOfpF(7=vmW*V8u~vC_#%gnws)DlIZpGQ!sbT%o?9^8rhsD$xxK~r1$rDaCUPQ*9 zadsY`a9vZIKLJHQIkxww-UUXEAQ^b`v`1^!Kdk`g(#}wi@kQ{b_rl&)K)YtZpX$Mh zAj8=k($sFP5%Ug4T~dXW4{xgbdJ_XL$6j<}{Mrpj3yTN!d2Ik?gXi74uEJ>i)UWgZ z&)a=?YjTz6g8%F0&hnkGCa;T9OD5W66XgMnZIX>TE2|};mIMOj01^mggpfo~mPi5w z28<2Y7=`Y0`gG6ldEWZIb6QxwFmvbjs#RU5zN%e&@7lHZ^Sm(^(sCXPyb)^F}BpmR(n2-%BR;;JH(yzXB?=&x0;jZNL zp*ek!0}Lf?T+K6s*u7#8Qb_iCTQ(M!p9k~G6gA!JP0iaeGQ&oJ##Nc>lbW;-hajY! zl9A{{dfpjT(gX+7E*GSnw#(i&E!sS=OAXT%h5gQlee@-&07{a2>P9S}O*0bok7Avi z-d1yWseGz2Jhua59+#6jpNqBBaXPkoAH6zbZ|PO92AdW=W3+f3sBWK&?Y%*_F9ohX zG-o_$N}iv5U+M5N9X~lv4}a!8IT`p}=Y2Dt&);6SauNV?iEI8kPHn=2)73tG4>?%G z=0AHX1f-bu*pCB@W9sw7R{_EVZUS@#q?Mn7-4wTLHpv#gajPsViGfzdcL5;;bOSd0 zG-0*)q(n24pR#qi96mB&Ca7CtKJZwqT1kGb6pT^`a7>$UZ?mF|S|m_M?63HAeD4G- zjYGl4a)Q&C6DEK0pcO>ALz8xlX8A}mR3?M!8`o*+B@5h5iPK1q7w?ao|2|0)YeIaC z$Kq}KSj;Ah2x1~LOQKnR_Q-voWj;+v$S2*b>pbh(I3PTFo9OzR-@M%{9TB2rcHc?7 zDf4{dX9~becL1?-ifNi-tAH(95}F0uGY?qqLq@_1ADhmPHiXa;;er5d|M>jkz@oiU zkP~ByE%xK2Ux`r}^%h5a5j)`r>a7>z=n*{#`tYMr4{q;UKjd%nNILfW@yaRE$lkB% zz1oSzV6V1Ywb!aT#=9u@X`p~rl&zqY_@U@?1&)e@$yQV(wu!hE6-f-zvQAY|5@ON> zB{AZv`}wjbXFGXwi*4j@4u*pm4+>1Di7kxpjIRoon(f{UVB1soZ9rc;{+(oZwRLQZ zm}tzjYPA)0$?pI4I0CXW%9cL`LpZ+mTHfM2ivO>D=#h`Hc>ox$UtH{`Zs+{(!$%fl zGnGGh`iZS$>&K^=3+u|coqUA6>p8T9g% z{W^Rk>XPIEU*bq?{rq_!Blzm;n|(e*YD#by*3Gyb2dKmeAKM|khEoZ^tTg+`B{~T# z0*Xlm0i{v#t)dMUx4RxyjJ6uFXYdXn$dbJ zPaNK{)|H?qwH8|#$FL9ADB)y=vR$jy`O%UrF}R{4sfYW@{%Ua;NwtS4ReLfd#eSH` zy28`CD1fT^;eFFdHOeMJo*^cV&8}u+{_MKMwh|T3udgLavBIN?KOuvAG zaC6{T%>e}LWcU%ir&sS?jH{*8Qw>7sWn=gK&sR(pA5SQB%D(-pf`>WsY6{32F|(qoOywjcvdo zk%y1)xMy)hyq`E=H)bAq;A%)7XGiCe5m}{JJT^VbC5~;cK>K%JE}vu8ru(lVx#CqJ z#A1BezI}&uQAAR?;EsdV*7$K_XE%N-?(;W&nn~5xH6O*!xPj;*jvkRTvc=-C-;DE# zzIEk$g%w(?vr1>gwqg~A;9y4a!e9<}<2ms?|I&H9Lb!XxtwY9G7IeHk?*FXt%dD{C z$}xmOj^l6Lk>K#WBOkp@9A-9nF$$GdCJWGmA{b1H+oYJW5_!?&W{=-VtQ37mH6S4) z-@M(7t&q&j#7$fR&F~cc-@eUaw+M)O7pmgvxE`C^XitQ85%Kls6m3vRJUc2z+)JzcN$@`^_p6th%vCIurT1Z116}}0=n!*jR>NOl^lwK5p zd}P{v#EegM3-2HYF5T!|DPiE8%0x4=0(;{?LSAAvbKaRcP|ZiT=%l-*8Z|&lS701IKwwqpxN;zFno|(# z%gRIh*@_>Pwpah6Vjdk02xZ6N_u8vu>mQ=wn)Ip~PAfc@;^X`<3*{qg>P!^xDeem8 z*`MoG*lggkF;7w%*QdMS^Gn{Q$*I-(XGQHbtyB2C)+!4;-SsN$KwG@ZRJ#>Wb0-xT zuF<w)-8Hwk_~^l1I#atGJE@}#*B1}OF3wL&B}+mLk912+{?5{? zQ|)%3J`BfkZz^nxYwt}3GF*oc&2Vj14Tk7_n8`6gtO1ypo(kw{T)^Ehi}PID3>YV8gyb6C zHNM!3z9uQ`HPGjovAX}PBm2|vW1*Z1Nv$-*86D!iJFum|21(8Urgf5x=9<3^6u;+E z%GShn91x3)fS|%ZKHo$hjES5_HgS1`i5 z5D?q=j0AyXq2z%Qr~~2DWjk3&YEBTp6^28Q_KzM*{@-s_eGJfk_o29HQ=t53iAZ9; z9xhXq9%kegkejcx>5kN;$*OSke4C;^Uj3IDnPQdb6P((Lrjy}P0q72YQ1eCnKuLs} zOo9?_AVgu;evIGW=4}Up;s}(OW|8yUcbMWMMnuF)xCSi%jRr3j0r}x!@_AsGtZ4%B zb7Xir={dqOo+}d(LQ_cIXCF!|tf%j1R2Iki@T?riA)Voqc*4@D#GXowtm>zB2sV+0 zXo9je*0IcnvCc~5VciunQm2wwe0e)wA-u+gB;kLbZr0|iNOV+5M6Ig6ok*e9 z>-9FIW4&LWr&Zbf&K*%IvAE;%FWF)pX|p))pdqmet2#z!4iEBloapF3{lgli{~HH4 zeqtNgueBs!y=bS&R)_#n399`!{}*?vQI3E{JqTC2eg9Ht>}l;ha`UHl)((#eC0YGj z^N}!-JMoc~r563jlAJxx?8K^TK4P3F?pgVWkI~1K#CiEt8`pwHk`z6;GDC!;Dy<N8Xh$$R-&kWx&QKuOd1oDHZwpX(`K%SmIT<2`Bta_ z+-|>8My!d$N!-B&a6YU)aD5I|`yn7HZ6~&0k%HkeRbG@Slte5P0|$w6WXTJk%plob z!Y`cP$&BXusnYD%tYn(7C`ZDZ7LCa^Co0znXG!(erd-RxR6e$3nlpJ!kdf6^H=JaE zT7*CAgO!#feB{@0A(3+gH^d70*&Y9<(P#a1bRrS{bk_A|oyl8kguN)i6I(Chh|EYY zJhAyhbN-|G&`fBf-j!(x5T!m2fd8!ET1m;B)Egg2((Oan?r>LMTMEx>?RFa|xZNx|zy77x+)`E`IbxRk!YunD5vFJhHDySTKqMt$6G!oB>Va5I9-K#*=r! z+kOJ?t2&Xq_46)HPWi9Lksk-iy)b|N{DO3}@tAnz#+W@LHQ%^4O8KT4r6plWB{Lvn zDm((GV7ivLyL?ha*8SW^#rpy+1jvg1hzGrV=%N$LY4Z6*=z5tk!)*tdex3MI$cR-q zwqakrEGyKP4u&gzH7ZWDh#q7tva0*IUYvm>f3a?26TE&Z#vvTbnnhFpaT<;JK{g1GD}8z0ILE-<4J@H0e}hMw4mBNQ3taT(+>mC0x?y32^ojR9=) zEjZ5geU_0Qu3F@#6+y6!^FQ;Rd>uV$pO%&c^-OHrawU|WZ|BTBwEf_gS1ye|`#jOb z9+z$8{JD6kQn-AN72;QW^~qf*Ra2a|Q25D93Q@bcLVLt0_O7Q1F&Y;+TwoE_|yAWZ<#R=eCbr zv{&cVw-6UI>xvpHyuiiE4~N=c%@F~0*-ai1=l$gxg}B}bNi}M8Wbc+tO?_pq_sxbo zA9{P8Yr>f3jrenUk19zw)Nzr~y}WrZ);Qb; zgu}%MK$pasL=%2R{<$bbNFzsrM_9KKhmO@p5YDWUteWK+T;d5giWJQYe}}O<`qASI zfuQKjhr~Dlwy?Jr&D=Y=y^^MoxQ}%cJWL3)aowjfp+)CAq19qbDO5||&X8RFaLJh4 z?MeV`X@Go&SA8JyRX+bQw{sz(i;rf6E$&wiCB}$YA}nT2><`~`MD*&GwJc;LH6Q(m z*AyX}bnv4hVS#2He0-ad=&0F-YuMu;)uB+MEL(07QUH;$J^ocIp#5yJJ;q-i^&6|O z(ZvV?5E}A8p4&|H{{=qceL?l3B3Zd(B=tGiZ;Of4vL(0UYedssq5l8M{~zBv8bdyr z8oCW1QLAKRt~O_zjHtg+^xFUMkx`4pjeQ3nVOvr@a{2KF*BU-ToV(;hl*F_7M`DXb zF_-YLlRhs3_Q=nTJzwH@Dn0*bD6KzD7v_y0hLX%UiNPU1ufpAcVRl^0HyiJgNFn^W ziai_N$IZbk!Be~QM#x35dJ~$Ll?*XiV1G$Uh(J{p`DsN!b{-VQ*Oz=$48v{~GEzgY z+GXW-iIdh^55?9@E9G}{L9Vf{q9n&F+pV3~IG&&8diy0rFgvb3X!umFS*bkMHOvJo z*OHYoWt*PR>@3DwWmVgM;wCFclF^Th%x6(TWlsLbNN+Gfyi%Wsm6U)a&=wQBY~ImY zIg+>;-r9&eI0;gMn}4WC=vbfsj>#?a7<+;t_T zc3k;t_NgJ}E7P(M%B1qPp^t z+hWBe1MkB(uo=c;#IEF=B1~=rzh4mUz-CFT-Pnx9j4h$Dc-hk;iHmjZCj`h!vJzVR zn~-v}1RSK^MxlkQA~b zDwT-ivcQP|siQp9DmgPer;>CLqrST?j=`G@@A35iXwp+&aW=Xdso;>2%ibXdW>Bti z65JAq4%fvfsG|asabOdF)dkT4WMBk4S?giQ{KR?T++DD4Xv>IpjT!bi2hR+$>g|Bg z9L((oVI;r@&jjp^&m0#P4h(ip&h$Zm#0jn$G#UqzDlV8=%&yqOzY&mD^9)_+w-g2% ze|cYyJKE3OI(PQ(ED)kXGpy)#HIajq>yZXE>u9Z9X4u^wVJAg?hBpT%(_L zU0%?DXXgRhs4SKL`aB|62F&ZaND_KuYQF!>`au-*mK_wwo~a2M5@Nzh0ovb}mbxqZ z0$MZ4k-p^+rLkh#AG0OBox%};>VWjHS|#!rX3_4|EgE9**r(e2!X`X){u~%QC75B9 zW)4wEI;vw#1wts!J!|w_xw0+kOZ#FFm0Mbf9?r|U zG-@E7Gee~poXMdn6%&lBN*~12IGKCZWQIgw{QlFObdWREuX&u(;>F(%_m<<$aqcdm z6ZfQ(*Tf`i4igDNXtHJ&~?4gU6=j{2AM)Z$+k5cM`-+N{&?3lFhJz^9$H7o|T@o z*x_#DUh&-rEd-$k&pl|yHpxd|MPm-p{p7LDpP&_wv$l$sO2A$;0X(1+Nf*WNd-Nl* z?uHYZEdF>B*{_^C;@*yu#8*8g-^5%6ib+c1wSG62TI!Zs@iAe8;j;MVr*BETLv4sd z!$F9?hlwNrR{&>Ls2FIKiPV!w;n+=`*+WL2{RYFcdbNM8ijtV0WgVF-C z|I-p>YehyDZo&ZRzvVxSduY*$8AVe=*2+lhBat1t6k%Wc>vdYdG-z6h~cQa7x+c%`Y{|z5PhV}LP83$=06XsxHB}3UpQIZHDtB_?UbCX!a=~DA; zm|#Nw=Ah!ErQ6&2Ats!F=KVIrR^}XXp`RwOUcAQWLrEb_AZFud9*AYB{nPhGO!UNE z)R!?Vt`3-Uleo6?bm;?)KCG{{9i-vL~1!9bp41>beU|2>`oTcA3K&OPI`KUb6oU!5AxJm@HvpCyK|#BjfP<;wWu@_-x+d@@{NW ze>cSj(9W;n#J9gfC9OppjFe`qCQ3WkoYxURifrvXTg48K89pBGQOg8I3YX>dSYha(DAh|BoRyr(TQ(;IldcD zKM?muqCCoX4NPh(X25A_1dKoYPHnLJW(f^``Xt{#s|mGcR#@~dOsrwU=Qnnhj?im|1fCn*@ECH5FOs$zQL&|R3bL_fL zZpmVQ!>7cw#KnRq>97vJhqZ?MgUlPeM_FJP;Y<3Q6}+}d@T6graT37u`oi@whL0E) z_%SdHtr<~S{VR0`)do7gxG!*M#IS&Y1IW$h#`Q^e4NY(oNY3aQU<@nn+Z7~Pq7m!; zsm2O6yMADB>$dCI{xhJ;Ak1hYjPRJp1fPqv(2<`i8OFi1+4?!;&j9PxkH7 z7Nt#J1)b^*fr1YqDS>c#dN^j9?73c|3moe!TW+teXFAzW1Ni5K`qTT0w@4WZ0vn*N zul6nP-GM*R1Wib>C2^jj)7+Aig~u5r!-Wee%|g05=}5}ymK)7K9Z|!o3{GI%?YbhJ zEO19jXx@b|R)9C7!K&fho~>`N4?0r`X0|!0gn)J((%#MmM)76=`eVOmT%PB0}>VginR z{o14*%g5Nv z{N$M!Q!$%St1=NB1nC%|Ujp-vI^zcq4M*Sl9U|LS*4>& zu5x4{BYGq3rpl4@iPZ->)y#5)B9{9@BC6fML?U79wv%*ZwR-gy!I~}pAaz2~OCfYm z-KWfmF$;(0DISV|L}p8irw-jz?C7t5@!B{&zii#`x8Wm$@Eu?!AODl%*Zk$eK~`KK zD?V!nVEDhyKiHMA$C?m1n3`Pu!PGXDY`ot&24?=llodnS&X6)^ z#*XV~iN&8!~hFd>pfS3NS321~!r6&(X_7qFgI=4G)hU5<4;-*i>8^j3JWPV63rgQ$dc6c-9ECo zi<9Co$-g9wAm@|~uvxQ7lWMi~`dP>_nFDO&XIi)t+`@!LDrDI@F8hn)hvEqi-zF;u z%gj3t@=Q-=4UQflJ*L2i0=LqxtSM<`WG@cHJlAkstN}xWMlk_q5^kWvWiXqW`TNI` zS<2l^kD1l!8c>z*ef?;sdc8!G60sGs#;5z?8@QABSZI-iF{d<96U0AuzhAy^>r3Yf ziOipi2YrI;HhNXOQ65ZKiFj>%^NTz;Q&bJ5)IWx-%*o0Kq6)!!1fP^t7d?FXZtq3f zTnlI_7D^Q>tcac{(5+UJ+}2w*)yM5CroWMK{UQ#bIzc|%4F?fM#fLPDni68ZMVwVb z1(ZboUuLwmNzQm+;YKsU4B&fT-+1Qh($*|X-m~{@Jo|vvInxD$E)H=ITx~8&>XV!P z4(;L*lW<$tGhO@Wk|kn= zTLM0F7mDP$ISy)#boyli?c&Vr`?1Vj!N~_u7=d}u&;x0HNt{1jsj;bENkRIvFbUHM zmj~U?)k|?YScLyN1cDM2q9!^@$RnJ=%hL1dx;(Bg1^o7Bx}qANsZ5kIm%THe_T1-^)i~2^^^;D}os61j_ht=(8wdAw(w%dd43pJPKRLg+5 zq?cwmbwBN$Gkr(fBUAb5YL7!4dNHo;5pMCPi#0Ik+oi)1Jap>jR~OIiv|yv>@)lel zf)ntcd(i<02+@@3Sj}fVxHoUy`#rcz|C^|NDQ+=T)FCe=tvi!S&PjI}IlU#SM%`%S zcxBG}Gq;Q69C{MnhsrRnb3AX#qm}6v<2g@^SSfEmsy8F^z!^zde#>s+7bd2e-%Q<) z-UeeFBl9O1VLr-qHd+4;B)Iy`=fvNH=tkT>-fgBMwpE0vJVAc53YEhMd}Md%$#{;) zL{P6-S0=fJ<S^x5El(96Wm8JrgeNN+y3Y*`WHD6~>o zs&J5or5boYKV{YU@sn~mx^XGcS}=A~_S&0{ihd-DA&3q>3cVN2NaTd$ z!*1P)@Qr39j9`bJBFDNbhRS!Z*~hSq5{+>`U-mAW_`o?>8De1{gpCTBy@R14N?!QR ziiVVAeIsvbY@xO5`;WdRA6dJ5W6&S}?_&G3GdJR=|CbEu9b_cB3EQ*x?Q|RecS*qi zhaZn^#&~aSrnP1z4_-}o69coljU4|(s(s>9|1pT--~G4dBg0BUOlqk$`jM5f7H9S= z7h|o)@PvXEd##GfeP|gHG$V#Lct#g!t^ZB`2L3CK7`# zlxG;wtJN_%KmH)YNil{`}+l_MeUL0oJu;MSx3rwaatL zIogDI{U=Xc|8e;8+`xCU@_ozS~O%f0DM8 zvY#+WISG;HrF;IdNW4++d6#}9~x;>w8l|*S2@R^QX`qp_RIH^7ZEOSto`Atq_ z2Y0FD(WoTO&@OL>`4dnY>!b7E5=BN{8HLL((ATfh4~2Zij5B=7nFt(GpIxe*k{N4} z$;%q!4SFc`M`ie0mzR=vUGn8!_pY$5F`7ze7L)5ScA;UN+eDysj&^3b25wgkSUS)# z?_EEAjeZN9W}k`vYgR}3eUX9UK}Qp%i8T} zyYEHJ_P$)h<4E*kc_YT5E}C1ySu)zIK)>|uN5yIU;<=zpf~enld8WY%Ml#hreP3Mv z9=h)6?H8jYNuu->sSPe5(#gC82LHpeK^nYBe)sER6OqzVI3@% znSVw{M#?c!2T-5#5En#k zW#Q{VGs66p=Ez!O-LZ=OB>ozqNL{!C*t8wo67tku^2D5JW^;?w55A@M@Qx^bn!Yeh zifC?ZrIhAMEX+!w;S<>g2#h}|ZG{vhKL}JH#w0+FN~oFW{S;G*VA0 zNQl)A8WNbHp5K4;2ub_D^KV!Qhmk?}v?QZ(u$MqNT8YNkX}GP0lCZe`|M?&8KiZJx ziNi;bqbuIEUA@*b+~lhFo!H#?H)^ydToYfb`jIss8Rf`M{YZ?@SHZPLKXOb$wDBLA z0W8Q;v)IHC3w!-8+3$VyhMaw1CuZb1X0JGS3GaR;&IMqJ_Y(D4heU)GW4mSa2KfCr z;hvIjC@|P3A9ZJyCXwMcR8$PN*m;iRKdht?rOG&>D;aJw4jM3g;r$BXk2Yb&N8D0U zFa%`1506+Ahj53JQi_CVyzqy@k7Y>p46Q=a2eOPsjuT}h}>KR!zp;ZKtq1RGdMYl9JTVPW!Xf^JcWT=1qW zQWw2Z^mPoyij0`Vo_1231?F`{%TgHWZq~R+mR=vSbLl%1&Wk4TsowCp|Eg8fW07A27m5LIlVk^3jtStZd zgGH{@)H7++I@Ux#!!F8UTN)L5f_aTHrNm^!Q_<{OJE8!J9}Ptb-Hqe8y=R0}NnIni zgm7t#sEXovLTG71`}*fL@4rTR7X%g=*|_=B;)8L_kFDCZ@tu(xOq{WeL+`*xX5$kS zYdxN}m%QW1Kf6+06m;1R{!4vMAEUY91$LVVZV&)C9Rc5K+mDZQ2k}CrS`M1<8!< zS%XA513W9%y(KKUM#%xRs6>Q-N^srEk|? zhtA1JW(|wHm|72{hUtGjZ;*J!v7^gfp;{SHZ;WieZ*BdQl-&MNRl`z&DEYKK^rT!P z4Q${zl|dU7;z}o7W~x8dL6|Yn&z6r#>bx3nxK$V#yWhj_OE8cdKb@o`79GGlgL%jjC($_b=w}P$r*7GnFgx>!*L{eAx6t52YU&+Sj*^u>VohQJ>_~JBvE&= zN;;r1$(p#H|1Antn!v`^M>8-pT0uo#`Lghm`=aEP@oHF@p3Jn9k4w`uu zX7Xo5I#>~_;kYM1|0um!2#z?14SYgE?eaA)s7|--kg{)~vv@nZBR^TqHIW^hd!k7dx&Rcs;{m4!PZ(x7y zKfv~Z_ticu)=+m{oTYR06zITg&QPi3-wwpTv$Q|{cxNmuWMl^oX&RJyu1w0GT;)KV zdd{AJ`oHvVr+y@EBRiZ&C`aNva`)k#!L?Y4O!AT8BSs(@X~tNFN6ZW3GbPM2B@P=B zMkA71e*4Q2kk*kTeA8PNSLeyQI2S~aOF`zCT-SiXbA1t2jMi`~vgqQv(37OsX)LeJ z-z*sz7dUMiDbmJA27YHhdJrrF@?Ra7q%3w#LHIS|iYe6CN3C4T5roBWeN9G6D8fy4 zT4c@1)tR9c<4Cf$-9kxbbK~%l<)HVvWp!5$w*T}iKGGU4bq+mAg%_T+(u_jEh(|$*=MBJ()LFooe+E z;VwYGNVoh;A|z)e(k)Tg^?*`I)VuOH2U)OezfKh8$cSo)81JgeloH7SCa;6lF`>Fd zV*K`(#{t|#R{)>rC7CcSE;EBA8p-340@bxVVI*IO>`PG=n)tI=ENVdOrGy^RRI(!D zZy0&ew@gh5OF~2@GJjk`vV6p@ESbW>EUX<%WPUV?Y7+nY=b!r4(hz0|G-b4tRg-B; zlqS`o^mf){PZjMXF@#7yq%9)?oeUE^fgEY>JVyxQ-H(vD?Ppz)@&{km~jt{+)#RB9UJDkhky&-TkR(na()+aUs-pWfz zR9lJUc*xcs$#TDOAn+3SA&3?QJr-OC$kKz~0@}y8tAi&F2!(~gNJPPf(A@`*Wmukj zMljNO^=n}E!{*10eyT`(ZYj_T$cJw|&Y!lx<~|OdSN{mTxzB^&!zS|dP!C|wr!h}Y zIps|+?>j9|NySe0Mc8}=enpO;xR9m;XP^KH@LsoQdl!%TBj3WgZ(tdPf5EN;1p}LX zJ}nbu)d?@{P4$XA1D-)vzqUE(PZ&fm);R%7x(=dl&6hLzHJxyppMX+5j5ek%zEC)p zrIs2NTe=|XkAhW|8A_t6O7QelrnQ>=)Td8B6LV3Zep4SsF$#*lqaayVX%$_(>_bGFJ%@dIk)@!}dPjGbr|A7n-114qGH^)!uG_7MJ)MnZ*3}W@(y%#OA z>9MFk`qqx|jO(k(n@VUY?|Qm1yd%t%j2ii{Hn zHicx&)P`-&WDvGRO4Dj5)1Q5)uG@L9pTZy894tWnOJO?5L&|dRY>r2-q(D9IeI4H) z3T!y_g0M&>WK80Bc^#uFN>x=u+chfVxnsaBE>NJ?r;}44yS^&ywB_Z!(d)Tp^YSRp zrrXn^XwznTKiwCHjQG>K;5*v9@J;b1GY@b*6QX?VF)T^k+pC8alO(!V4vaj$dEVV~ z81P8g*vL_zOv1k5BQLG8A2#S#_z3A7`4aI<5l(Q9`PQN^BB{vk8b#khR)MYy=E-P~ zpEKWXKL1$6N01@Q`RhNnDGTl4_xFR!I2pxRCyn%^=IGwx*Yo1ilf#;kcX3+Idjl1} z_C^kK_1T+^@xCR$u*^*sFY852XR~-^Ew+kD;Q{Ac3)K0S-$f#8TeBwLvW%}Nop z5c^(gntESmNvijHtC|X<8=$+zPQ0mS9tvT6YFNOyI&yD!LGrl=!j|BM?frPZG;nPen;>0@12w0v_m5K~(e%O2x zEQ!l%j7(q;88GD`{r2sf$y0DEAu3WIFhdFvlXbZ*XcB+w8>yLAH6vAy1pbLkOG#=5 zn0!XRYlw^q(-&g0Bjx(|hLn$l4lH_*=odmps;bEOns|j8%(fyL;W9%>SXGsb*zcqm zr6;d!GmdIW!Z_CIlAT4h5Eti(J)b95MY2419U^S|{Meq=po`U3ls-dA%Av|ZUPVS0 zVlo76=RQd!C&7LDXJsh6=%%YESt72F4mwWmaVjaU4(g7yyh8Fq>rfJ&1^nYO*O_O` z{Lg>Or4vhF)C6fgmvlyyB3QFv`!c{Xkm59KS~6Pv5c-y_ymY-q3j z8|inU%C)GCX|csLXhD1)LGsUg10N9baG=yJ79dw{K|6=u<`}|(F05<87}-Fqvq00M z^5|XqcG4vNjIkxYhQZ@;X^&v~;%NSwpYFtuhAikt|yk17<~Asq8x#8vGg4`N=a@;fS!z z31Txg3})Jrb>!j5!G4fDxoJMpR^?zPp1AkK6G^aWe&CWRS+IAMyqUM|VNFN!$%X&8 zFk)AIfT7We&+un|e_+q0?_{`@+%cEGo5n4E?w7m^GmUGB?3S0ln^O`2FQpqVq(tmt zWfXseBB+|O1oIH}7smn#4u()TpRF(wLuzE>hj!ZF;;*R#*0l(p8{q}!7#@dlM*tFVX(cRU5UWzE~rmAE*P}t zz-22~xTrIx{witY7e9x;KziMu4k88yU+|`2%e|?9N``vm5!h3?)wyTDifX0pe$_LB z|J|aVFk~K47NA2?B~?98kLj`lR!pjtdx^ve>hYsGO;n;DL4kf#Y_V~WD#gT+xTS>9 zsWy-qWL|;4F4a+*>8IRy`Cn{Z@fUqfp$vLkQK<}u*q#ZZ9u((wK)?EY?-{(d2a{*7 z*cyGsTf9^vGYrT70^?M5Z#rC~g8%z=){uCZf|EKh?dmuQbe#9N{xdZ{Q_pK)s_H59 z0_xC_GiOrBzSDGzroA|0(p~NIkjJ^QU6(qPV@@-N5V>H%|M=0cWADx*Xq5UokTR5^ z_b!}5$HwHTmCu0b>$VT?XfyLuNnFOmrSG8B&=naf@pDiAEBVEmJ5IyxH9>t!@KeZ3 z{%l+FvD7<$edVzOr@eg9Tj<4HFswwwy}qSs9?b}CZfCRCuLhYNy^>dhp%`Fu-h-x% zR7;CriTcwRqJFpSlZQ7tWd^s9Y7s@Gg1V=Vg)Dcz@erSZB)9Ww1-t++8aW##}n@Uiuc7$^4ui)WN&PW z%X)hA-wW_={R(CV%Yp3?VX3O^hu_+u1F}G?j0%tSb%JO9H1j#NPWV(-`L6Fj%X(LJ zM${!JiIm4~L^oD8$uUdowC`7(%e1d&(oAloRgOf)^$!Utrhh_J#(sb>87m?9s<)|b zk67%7@iZTOeF()EYYkuEYp)?)8Dr2(6Tur|qM)pNkOMe>ki_mb_m0|K+$1Oz2$6w3 zPVx9iT{Z$slw3a!60j`s*}_CDwc#l%O-ID(^+1mpjqaYxBYKePK5E7@yo0i4O+ZrY z$}wc8HY7CU`_+k*hN!S+YVWWQ`xw9N*#00s0>m?Hz(GFw9~Q%{@)25+1lJ7*StUk) zw{c#(jP(5UWaFugS`4cFd_z8V@oz%X=(_K`~m-7 z#Yd<}q9sY@k;`qvM>vn*BXMRI!F!c_Wb`8$RQX7gsWZdoi92I+!jdg%n#nn@#oTQP z{=;B{6f|b!cd_9~Hi`$Yi81^`*T=$>ES-PZ_Y%@A1jo2VNmk~XQIXV_BzwRq4roi7 zl9sFUk7+>R_nHy1@1AtGgCr;2j(v@$cyN3$UL;p1*&mQ8gw*lN-@_1Vl60U*5o8@Y(8$C#bKh-ajnOH>X zBOjuX=mFoiQe=il0V;G-PIyEK#jFt#%Jobt?_iF-Dawnud(g<&rDe|N;wHN+(~R!J zH>I{zJgb+UrS`Y6-^&&`~*=9T1Fh44~W`xYUm6tUiPO$aRD1e(JrtJ4T1TU zNiwf+2qfTYg?=5oF<7M!QZhux-9wq+w388)+k>gC-ce@fJwMbu`_MSRtloLQ6C51tf&T*sX4ns+c z_J~wN@(=--KnqhCydVLwU(Ii$=(--Ye zWu`!mOIW8N-CyU8OeO7IU;xKY$D8%y`ev6$=o40&{f;tJkiPTZB7P$`z|E)PZU7{6 z-vBDa`a8m+Nlf*-E+d0F`rr-b`~YL}Pr`fxmPXlPHel_`G~TH%leQ_*jA!CaP}%3) z64iDkKg9l-+r`!ClDf?KRtm7636n=tXG2E>uwM@!i2>^MpFAofr_dAqh*qyDI=J-1 z_E?QlX*wqq6(wZpCrUF5ljNQc__)l`K6%E-&{?2^< zuS!YEO4hbpAt^Tma(h9?Hebap{;&rwd_-E47(#qo=7Jq$WR6E8^Mwz`b)D`;Wo8?1 zmuo+A;txxS^80*b{72S&glRo85+~F9uac6-|rQvHHKd3l(T$8q(}xVJYv7h_Jp3JQUbBp9j47*F2=j~Su_r@?HDTYU00 z+43nF;ee2@CLfNCJ&B3JcSIxw&}_Iy2EyB7=&x07TJ04j^k}Uvg1Zf$N8mXRA@zuroa%8+Gw?g(dkT`OG#So z6(3n>*ASC6-6%a-ag*O9Y0FMNGEugiT!j-!*G&gj$UlEpMY7~@#{@}!Y~dp_9hqTF zbU*uU1SvmD6hRSgGq)HC(%U_98`9bgxrb$pmDfWOcFxF5ZcbeDSIKsF{m0DD_Ht_E zq#S=&#J4?C?(-8VD_Z^wVrDF&NYs><{MkzvybUmmvGk>Hr%VAsxf1Xk*|VA}Xg0xkR5{de}h+;hNbkw!g$m9uWFMao5MMHEL%>ay` zxPpJhLYv7F9!&N6U_yEQ0^niPg8YKa05JAr`%g3#r{-BYs-{qylY4_> zm!>&T`FSIL3b^%_m9SzK195X%0kfZU*K|VmWs$?w950<8z#kaz%Bhc%RmM}&A#_;# z^J_ZR2;|nZs$?oqHtL$$ywGW~i7wUNpZEUKp7i{3!Fg15r!63scBzUUKcyivqVmKj zo{S=IVb1$W1ro(qo%>GnllOjU9Clpezy(7|(%x$QR!`ZLHJF#;g%ZT0|TQ&=!`%y*c=HQfxye$p$OA^EYo{D;}}{NdKJD`3IQ{D`i?1!7`DM zk;-8*d6Em6vVQhm^8XxH5JYs7Rkibfm)GB9!lXfttHqP7RoRJ&FDgeNB`?Vdo6_{y zfj`Y8)@B;X#t$+zo#1s3`Nd($2fTN0A@hKAKMYV!EGD!|-fBC3`8&vGj(zq+$8Y#M z>i!Mz?9p4<8(Ygjbn10F0y>@gRy>gv&0M`mal4?e*0Rp+;ariwkvF0nj8dxCh}>zS zveXSklLbMI6;^|pgq~D^vKX}JlNfI4rRF1Z4gBVzwf+hjp)_FwCJ}br{GX&Hr@l!n zVpL*n)PzIv6Tvmcqzci9#wP&^BCn%ikBUK<%IHCIlAeOjy&8?JXOhq=CSFh7Q}svO zEwKr%m;LkJC^BjT7K#C~VkmSj?j;gLQmQ4%PvZk3Ak0`^)ym7`EY@rF2adC2i=Rta zRVh+x*v;rt)zntH`Pu_SA5=H|olIo-h%`7RmbD#;~kuSu+Za7VmW=NOC@5HqQ+Pn9*z96NR^Bh!-B>+3>9bF7PLQSeh@$J{KR}7>d_H8O)qlkc0vR6fD`N(l?s2oRWHE&1xRldjO zIa_jg<=*;(Li^kV6-Hw*PRzrt0hl8=MlX1 znh$YfPrAb^-en=iM?zXZ^Ileeu>idCgU7G>zZvW$MuD95IPcXT-1yvI6CoxdXp>5h za2^IAvxWDLSk?=cg29U7oDcHnD!bsKBL9N40NMjZ(Z!99gVscK<8(Zzvgj*Jr`QZ^ z8$hdMx;c=8{lCWrTKAyKiDuq79QQY5{3SimvhiZ8>SXjRrl>7|`qBhP9D0$la>-gYvt>OJ8~3w}FzI{u}q z)58&o=g);_IqA82HTsAc{DgyOj;#+3mi7n~G7Vg{OSR{{3}_Gh9wC2A7@d(Wn)+mH z91N8++vKDy@w+efA+rE?L7Y|W38AoG19pE za&t?hpyB{SBWz;XbWztb9E}Fyg($(zffdWlR2e@(lda`|_W#F^g}juEEHiU03bGJ% z0nIWhxiCBrlk**bdE}+Nd9QgX5;l56K^{7LXqQ%K&U9nPLteC#gahfZSizm{LvY-t z)%^IP1nA(xZ)S__C4KPXTYW z?8=S3GOL^AK$NZLpPa~AaC^80Yk^7yp?a_%7eEaXSVj{lL4H`%thH;5#9rJ~pNj&e z{)yoli*Is0ltWCoe%s?0Pu(3SWp-H{allg5)rNyiAg?rM+*?XUc35XcEi&71kXP;h2tESa!!31eAMjsKjk17G z{tw4*`1r7f@`Y==kBVfQN<4X|*L33L|BPgu#1c;aD?hmajP&A|k?bs?$6IULFn`M0 z{E7eAYOE5DU?s8I`jfob6!mJeublk7f3M~vcV4#AkDSCuk_oZN5XGrD-eA95kK82x zh%XNPPr@}eT{NLzIkfeaOSbO3Y~#Mq**5PveEh4Ib0Wh~l9uScD;aTdC^&+rd~ttb z=6vN)P~taki~}#O%1nikOr@{Nr2l~&Y%Cmi-c(K#-_DUrhm zD+Um*vX&1SSz!Fi3!?~sqF1emKv=^L(m^vqqfw;>%5ak5*6L(A$Vs;>bR!pRpI;b< zlNg7mtTiL$Dfum(+!gN$f(5OOJr<2l&gHv+&ih3cQGMf~X? z+UL=dj1^oa=WUa%boD5zBKKH=asbbeXAq`&3E(6H_dPy!7X#h+4+?2=*X4*Og|&sK@5;t_TrWWo*ZWs#bFhn@bF>lFQO*eW@rf z?+&LBAF`|E3x;=W{r1;t=$2f$ggA&zS_)Z~+7OfIN~TP_ zri4fhwFuLx`$Y6c}Fqj4S2h4mnFz+H2)%RxArjJlXNYdk;ek215vJ z-A<|1j^FqxFVU+!dfmtI5l%n}Z+PW=RTTT_G}mqe~^iF z!aJY;`mGDgs!_})OBv}J`(0mzqkpF&fULLfKZ%6m`oT${TBmGihL z6jWdgcoc=B1t1Mfy5yY>)JHwxVHHqF9#+UpZg}iDnWgsOyLbJu0@~Xx#{!PBmnP{cLK&?BE-u& zSxYHyMtHveRJY7K&C?qp%9#PiuB1_N-husy+>xCi!*sp|hL-wO4`4kG#1rrqj#r(O zpS%konO#k*8m6{>U#)=p3?)(2a50r}#+&k!wi{z!WnP4VbS~akL$7(K?d_c3d+O%J z?+AkBA%4l*U)p<$n#^mO#|=tn`8My-EiTYT*XSP=4&1G#X8PX+u2g?77X{FJWfmDE zuk1a=U1^t;^ypr(ab9jurYF51}iJA<~*N|~xxVyGN{=0%4Q!`SYMzX^*< z95a7*qz#$!u{hbD$tzRm)@09t3^J;{B zxcptVeoARhMz8MmrqbI`^@_Z+_p@B%)oe&-1C_*ATE$n+!o1a zA*OjBZFS_gF(!%9XJ|+drWILYBJxUOBXT6Ust`C$!%BWxS(DDOCYs3=xFQ`{#jN`c z&zNNt48T+D>zo*;l|^Ho6eoU)mc)wi)29=UrGZvb!*M((jxPQuCye2Az<(m2AR{s3 z|KTIDR!s~~+<}c&5Tmu@ndJKj8KKdTIG!z5qxB|A5=z1`nfFAwN$hjDjZll!eFyR9?O1Au#5q`!)RG6K~6gOUV~*(h%7GZi>pXD$dCRFG7=WN+H}IkZ+`WA z@cgw-lsQ->>#4DAyG1wQjM!GP7*LUnf5%#j!HoiOSpVr*QZTjkUc7yvrJyuoReCh9 z5|5xy=`D*Z3IgKKZd5Q!(h~)K_5+@~vl@=eP5vZH`KwsZYqhE8U(H7r=aI{f-+lSY znO*XctD+yd|C*i2*@urH8ceMcbnB~^`Xb~TJubDJXxMih@)O;tl+*d}3CV9h$0WeF zT_KxyUj>UMVEETQOHx5J8nZo7caT05nAF-El&`1y!LfTUzfN2T^dx5UB(aGLl#tgE zBQFy|qhXBTI+UcLfQOI3jXJ#BHZfT=4&@`GC1Ji*C-+zDFIJQx_FY)Rk!VA5>Sqqk3EKVo{d}KhB-R zOJt!Z(E8WDKr3PJAg4wYl^Op#IsPIEzxS%JRGj6m%TUx~SnAE^h0JJe&X5m~dL74;6I zOvUP-;#bO=^^02x?!r@coXiDYdW)KU8nu*@Po{sa9AP9W)~H^PYv!_yhmfH4c*rW& zO>Off)(kB2LyWbNxUi3qzAj{0%d$e%`JGvs37Oj<>(=|PGCW3|Izs|6Asay_U;gZ0 z)18HqTzPhkF|Im0;rHzwU;USyk6!uw$A$VldhI8JLq7Kr(GqH9ofH+IBQK=D*N9aI zCha2rwZuCKH^%E0GzEAp`uk6h(3SrYGID8RLk-S}5ep1f#2c&#z&8YzyGDZ${8@}N zfHsJtMmY8X9N{VUJ<{V?bo42y3>*k; z_9;P<1$BchbG~Olz9|{cdZk<=h4ZG-%<6hlTAS=o9RlgO3@Jzrdp=RMd$GNrT%TTo zsVcApaJrw16l2X+R3s%`2yN{|#A61RR<`#hIH=Ia0A?$ZY4 zhJ!FDB7WCDo~1(u)^fUU`S}xz*>lQf5p}NIordSed9*iG^SLz*@!l6aJgtN3x$OXi z?GSC8>yHvz)?Vz4-W~*wQ0V7jG0IPcsUD2pUTL^aZ@YNjvPUR0?`tmZ z8yqE+9?nztpQXE$sih8_IeKoMJH%W=@)TOU%ip!of$GI#YPD+A#`$k@&F21773jca znv;jNN-3(Z+kUMdnV;6zC@QZT!HlDQM!x5!nPtlJTy=1lUh*O6dCi$tsPmLl8Nz7> z)8_+f612RA0*aqj+g|TAHD&1NBBN4Ab_EWQ;orTX{;xT~0I@1pl&kF-|^u&EIZN z3Tql^@WZ#j-XSAGQL%14hO&jkKUOVY?2hm~Y~Cu0xU!2_s7KAWGQC?v;aE;3#8821MQfExW&URvFyuui|Ku5Jsn{|F@`3T+ z3f&u%Ckl~TaHakZN>!L<#mkaD3xjY{umW~g!sbwFpc^zPO~n-t8nN+N+8~3E-x=$* zC+>2rPohS_PpcWB9054TQY-HNv>}PybrJztt3oC`S2&0S@#`2_+ZI`gazIhB<-@5mHaV$q}W|957s72Zp3X9scI$u`(&Dq{$e;t zT}Z}=YwhQbR;)S89I$3YF4iaKf*~Ne_r?_~S!r9GeDHIzoyLRWGolWyM${tJlE@wZ zNB(;iA6Y9$b_CZ7A4zid@{x70UmrXDNK&`o{h8xmxgZ&NZrvNh7$I@wR+c{C{;Or3 z2nv1R^FDtOav0~JORT?QB7XnjWS4&QixR+PHo$O=it)fQN`diD?(G-h z8;SQWE60xfjPQJJKk@|y4pq7v>@;8-Cj8e$O%P-?pdAfaB27inu$VtJ%2w$>XvJcD zE-tg^K_d4KTUqZN%(l&3N>yS!wqiP^A*o&} z*=<-!_Gv25{07_eC(i^`-qsZrh(|AvgZrvCsV#p*PVtS@(try;psJWsdME@gu(c z-2f*^EZz|^a&dx}5-uf?qILH2cZ(++{rL4CgHfsBL|n0FVT<+PBOXnG8=p1q8zZra zvtE;oQ1q1HhGEtFPtTaW1L?zUOW*3Gvy;-UnaKJzwq8QZPpT?FLkymF8Nu}$bW zta?NfV|nF(v@IH$tZR8L*pGVf=D=dQcQD5(WO&3j`!226@#j+EdC~Q18M@(osd`+8 zLSdG6Ee#BBT75n)n?G}7W5}#2PsWTzZHQ8dYVF4|LS~$FkknZwYuWQzyX}X)`hI=X z6!$;8GxnFC025H(O_>Bm7}rx>os zZlBmW8UzQRql+WP+!Imr0uqY5fQtMX030Md=!gV67|e~kf!NA^S&Romrocc@=vpMt z{kwqS0i1*3b8!_4E&yYe%2$Lwb%x;~&j_(AKn`+)xBG~}u6bf>?xYelB(Mi)EMl}* zF@+#&?xh}@1_ti#4gyy!n7xv62NbENTRc>;vkmN2VWp*p6~9ZJkS4Sy?TzXbD#Ld{ z%;ESN54LZ2jT=h~TmYD3czbjAJSDPppc*$(=2ZS=$GSAM#aqi8L25KD9oQzXo_o4> zdHeoU|FnpJy3%Q2Z@S{>1xdiFXEZ+wsg4AJ}M(FL0Og4Y_nt&!qV*oi2XcLUnUeGt)pDD(7+Gn-{%h$~4|`i%u3@>fq~- z?Td7M3ZGWY_3G?;G{*>u2)(7vdzt6j^ZglK>=rK+{$E%R3H+YiPS?X{+_Q9hm`+EL zE0J|+Ro1z?b>^aA>qKu+|tYD z%I?o_UTGQ9z03GaXLhjB#ng)=V<4Zcey*vQtV2+BT=G_VMoxU~vq!1v0o1Qw zAN@EB-b>y=Xs~(PUTXp13X}%BK5-ZH7?TVeLvc)G0=Ac8n9wW`04hKmuE93dJVySO z98cfadi*OB1N7T~ygx^CmBMH6E7*SAB50pDcNSPr&EAD#AM+9- zbq`?@rmuoLLq<}87-fwHq#h#P?{E-R^3xng>$HP|sBjE>qQP0Vh|`CL4Exxz=O}86 zfAnfg)SCZyd<4`7FfM*mbQ34LkS|Ugd`#}VaIMk6HI~4`OAl+G`CTA<2aLSW29@?h{942aqEGAGzUEQYwd`5JY@&FLlYr zmk!2r`G!wnC7BjPH{!TTwdmM_45F?kwb3Eo#9F}jafQiT-MAEf!yLqI|$ zs`ywAR)iuqGRkVi;MtdhY}1om5N=ZKN%twCrL8H+9N35EjEZFCCo+6w8|x0x-8RRH z1UtV@OLxHF1^r@P5!6@E?aJmHDI#6jWmQxj;JwlU1b(yHiZHx>6P1V043=qaYXKd$^#^Qx4N`Aoaj|?QTjwR>aJ+bHX?VJ2ra}iiQb_YWpu0tCSet|S8 z8s{&cONoX)Y}|U_rCawwn47ns+rV5OVilZ3Bv-umrAyySKtvKAKY~Ml-YAG~W=u*0DCQQdk_?bXmm{a>nB@>^BZ%R`4t6! zp*(#N^TeF*8$k{7YWmg|n$X}~B@~V2m5S}yGTIllV4h{mtIwI`(Q25{QmxJMYtC%V zjjPeM8dOtqILkae3}vcqy!pyjVD|5Nis;RO{FLKuB~D( zJeL&7mWjHW(x@JP@H!HY6Zc(BQG%Jur2O)Ye=lGTGwE1LA3mGs(b20uVEsi#XwY7{ z>LUVh#bW%*58|PG+cz$K+wlwEypcT736n*LJ-`*}8xR=$v+q<;Ct~X2D`7|pSA%H* zr~PNWxFCengw+bXJQzj;s^wzB+l#p6)-|=qGI?BE{#YZf@Oddv;Ux3%))-I2MAr0V@P%PDawCwPpA#JyI8Fh!lhP4t*&$o zKn6D!QUZa=TR~_KQcAN?460xk%wOJrU1y8{RafX z#wxRU=&jHoaOkJNS4h=CAMM^e1@!ItBYR#KEIlV%&-6xHm)FsTtPiGj9yI4uW}ln7 z^!Ssv}hA#uuAHzy+uFDzG?RG``-rMYs7!VbI|J>W7I7%=Wa zk-RK;H6>j9wkT*?D;Kx(Pc;-3l~LC>=mPc0b4L@=(W@06YX36h)#b9~wa%aFpb)&w zyVDP&z{+6re#_2Wrjo(K`%V=}%ulJ$p4YicyHcf&VGngn%G8+|1Fv(9UuTeOtYe$z z=yPvy27qHH9pj0DQ9D*b3NMdfqK2$F8I{3UBvwc1bvtS3s;vW~Q9mchtcE8Sd z)E-V-Qs8_S6b;w@jp**jZ$B@l!nf{ukURZ!fW2+suOWAG$i{fLGX^AT--xcF9c*9)q79^f`hzz3w(VI zCKCDQii4~VGTxFVEQVWuNW9;j=?E97Z#ia=yn_+cP->Mh@#B9Y;LnMNu0cB1+KnDG zEHGMe5(l|qxdu2tiH0nikvVCzmquOK5g?}Z$W-0=7DKfSgo=Lo28_|(>QpAh#3 zet--C3~A1Mxg4*2%Yp#=Y>z&CbK*h>4~v*QeRsT#)_lYuFV-EB9$?dyZB9n|%5dGU zH4EuMe02kxi>r$v10Zd|>e(Q}f#oZ3zJr^Dk_cdh4ulcVXe7bMLPJ)5BGHogX>6`e zrWiq?HYaC42PtjrdDiDwYLVW)@R1d@8O}0vW+~9^t>oBTr8)it8Z~G3LpK5=Bf)lA zQRvDXYkg%f>guEeTqAU20ljNf3=9Vwo1JQ=m7a_{b4<3v@&;{IMN%=i)G!gVEU&|h zV1UF1m4x3U_Rj^Mk(_{?PbCVv*J|1!LqG(cdyt)HrWBF59=SPT5KN@^Ua=w2>sKy1 z{^bi#+BjY+ zpjAPQH)&OF*}Zk^-eb2U@8RbCVpd;CIkNG1r?18gH+4j9D%JrS>n= z{3;6;Fu%<~s#fct3q#qV#`4POu(n=Yah4eat9$d}x-Dg9WM*3d{zbL7W88KOd(j=q ztMk=%O*tJQOR*;(FEqA{HT6V}c8(vTqSYR5<$^f8xUAYk^Wo{6c3pO&fLz6Lff5eA3oTNnKV3Gz zQU`$gvBn=QfGp$%z$)yyj$IibI^da&iYBnON~?n~Jy*4(zkqd9@+pqty*z?NR+R;C_w;tc^J%Qd1s^K{v|2Sg#MJsOwH}54_)cNtG9P@`Z))AT*_%DWT$zpyw4Q^!=nj=U*ZJLNP&~PeomlITcDYhL^9{Do zoi~1*H!8vt_q`rA3A=?pRp4@R)>_$hp#_!cjC8*Bzf%MpT>H#ZP%ZgLm;rZJ0m$g)0XN? zyiD8GJfo2TXPImUZTnL{`-G!OWZ!u1nQ|#^`snbH!+*&w=P6Q8=ApoXsT=k>$ z0o0Ku;9pd$nR}NRI1_Rtia|}~n1CMy4VzHLfja}Wp4%%5|R3*tZW2gmz;70xH;y!5S$;nvEJW861}o1FO6|9FCn2Sbzzy)`uC zWF`_yJ~V`hSA9&vK6Vh6c#GKhfV0B-`2P^K$S0Rf@p(#Pe2uOonvo$RzwaRbs+#}a zpX{{+)!bH*RiT+^>lzfogfKdh z$SWd{k84%%%=+W%!QkOi^4f}k&9&MZ!7@BKILdypx+!!Yp>g`H5N;872Rc^V9llT< zN5f>{2x}FFb(Q-)_VsFNm(D1iR7+r8d4jrn0T!$yHZqgrmXQ~(PnAX z8W}D)mIQm1o_=KDAZ32aK2r1-f1}muft7M3Fmj~sdZyeh^HR2uj?7NwbRi(Qa`7LD zan86)gPmqG(;6nSaFh7KMFLa9R1+QMlApwS`n4DL;~L$4H#2+;CP@jNh|q(2)Wq`4 zy&I2y5g|<-EiR45T3MG+*KX=MOoGwMvL ziPos{v=ZCOkzoXV9YOUoNLFvBf9B+Hfz@!D%a-$Vjh}|LtXP0s<`r)%FO=Um6;gnJ z^iTV+iTPE30k^-l~n7HHam&;ZqEKm2up}iSY)w8UK zNj@&U6xJ4clHp_^)fgb7#iz;wtznbroAs&Y6Sg+tBMcsX^mx=~k}D-t_?HCXlzhZs zi0u|XBNNLPKE8R)-&k<7?laKh9&+`E5;pUS_kj>eeA#la)j7gh^mF zFp{X&S`WQMw7chZ14YmQ%Y7I6rrrQe$;>0TeqHd)`EP`Z2ks_LFER4&dO+_zMF{{{ z@lxcsMazM;L2h-kRfs=r$_MZYjZS^#oIDqHEyfEHhJGVXkFUWg@NHmTKvd1H0;Fs6 zlSjvMT;WRdtbr&2GrcDJC};u^Q_kL0e!abP&g;M%w>UGx{~)5;FL~A!<#xx=9qbnX zxwnO&Q6)RaE`FP032)Ul57qpN(6y`L_aZ2|r-Nsq1D=w*%4O!_si(aS;B>(tSoJ(c zfd!Oukq8D$IX2hm-kd3UaY0^?^Gu|61Eq_mU|x+^FppD1FKB28LJ?X~nijIz3J+EE z_BGJOUNjlIYush89KEL~kIQQ*RIw!#|MLOiMo}Md zKY6om#L+ZI=Y)h-hM$M-4Oz%g>`7;~_S8C#t*468zNY35oE)% z?0D(4zM6n}Z}#Bpm6;|ud11zt`Z)Ibz*hrUHFA*SF;E&^`h`SCS?FgGK!`c4diEJin6GN(w$1_B3$?6T9OhB2sQJN)f*DhI2N)a!FzuGk^#3 zSxv`!DuGF}M;2`6WFp6`&XwFoniL`A5~!ql?Ff+)C_b@khM2@)0Nse07F9?*(-R7^ zMg_5Xj_1g8VH~&{p|QsW7$m4;EZ-YtIue#z&)gf&ktgqEXc1Ef`XDmKC+-=;Ejp3p5#v#UtpJm@ zr(@X_ORb?Z$jEci#cXqp#o%fOAHhU+C`9m)P?8lNiA;5+8Bx7RHM{NPAfV1{zfVSf zpO_3E*#unwsDC@r{>CfT$9j%5s?`p5FiatoLHsM&DHdF-oQb0Zsp_PLSanxllM&=) zrl357!jgNPnM|C(~mO&mcJRwYp=%r?} zH596^pm4g$RfD>-0oA`9+4AT|YS0pWL5=6f=YsLfz%%a21!D_x8>_A1CN<)U_GH1I z$p%taj@6mDaWG!@Yh<{(B{+5|dC168ev~FL>l!Mwx@L_71EcPTIfnyJIi^HY%|7TV5TcWP@-`6Iy;w4riGs_Rrkwz@t~}CfvE# zJEMI2RO6*p!kYQD*YCVC<-GDQ*`^Mw5>oh@+AQBiRV3xPGu|?~%4%!cmzTcq5o2cf zh;Aef#|9z@EzjgbK+(9k)1!+n4hz)C7TK z^rp;$`69r<>TXxypeYv+nGOOE?u9C2#`8w^D%sPWfr|U*nE~Fx)oip#j~T|~PZb*q z=6F;L>5AR)+$Wz`NGgx44a{rv0m9hl6vKm{@vRul+{!0M;rYT_m8ukorVa&5gIfO| z>fZEUtLjYG{p@yO>5ZwBuoj?54~l}Kh}fWlqM|5@qS#{> z5n|s|l1h%kTI*fizOMTjbFLR9RjFM$`<%mX{$A#qbIdVEKkxhL1~1|j{Ccd^u&00` zCBcS@I)26;l+`}*r>I(sQ4y^}od`%3iy&jn@<=rWICI6B7E)Eng?W6juEBWIFJEj9 zfbHeNc%J1Ve*(qD07+SQnv1M#A1acZeX1C7qeb4e7P$5- z)9Q)zMJ{Tn6eA!v2^X|6O+9&a25JcMV8vpJ;Y;IkPUhsp2rnAtm<+w8oYE87diD@Z z*{Zb}S3EMa@=WFe6%k-xvsgtQ!zt8;6JAXjNKbjVTZ5Tj3Jmt;BR89}@ zWyaY4iKUq+?gsB=^Lmsf*+tO}8cGENLhD&c72s10w{+E%!oX{Y1CO%+l4pvF;Z|(7 zlvvg|+6ul7Y*6y{VJM-LgqT`wi$k_MJm7ALp&L5y9MJ+CEP=WN6$zcCw+L(n11-XB zY3TJh;zx|WFc;@@ukJ(EwcjPhskBw8CzLPLs-_=}SW?4#!+6*#NsBRvfn zKH)Pr%t88dKYu8R$SveWO86iB`(lJ%(hmcEm~ziVBp~rCuV0BnjJWu7^dFS#?plIH zimDJ4BIWkphs-L?$f2FaN8L4oRs8IVGs+~4-%y+Ez%BvlUQs9L5b?6Ff5Qd)kLuOm z*O2eQO`#!rkw{5OInwbF?6pvi80l_KgAIH zVAHT~hH)1}6hoj`1Vvp^j#uCxT!8;rfg_sThXr5+0&1%qC-ef+?Kecm1_`?Mq!(qg5Jg=p{MM#4u?<;>SWNuW#&qH0 zSCGk{InQ+$F(SuS_@>W1X{}vt&TpB1*)?WcYnlrY=A#Hh2&Tc}fQ8&t9tNT=} z5J&;bK&cJE7TDb;8$~JgiYckf7ZM~k&A~*FE!vMfM)(Vd8IePP&{b2`Y8t9W%a9tP z^-dxDemn^E!T%0}939;#3ebAozJ?~iJ?BjRAP(W7-Gg(C)eIX24p9w-=@U01OjbF} zQ>N@Iax}9*uT#{1n0Znu$uouQO4#Z}QR_B&0z{I{AG5JP(p}8G`4#FDj z_yu-%P!fX4V7P@sL<{rlE}h!&Eo6qo{2{0&0oJgQz$8OEZK7p@pE0RY#h`kk0HFv$ ze0u@-1PGyQAGHVvxHPy6Bv1eYOfHHDv=(9&GRGRHs0Uye-(iqOv?@S2C7dvV>#>81 zjm=7-1=xd&$x2K`Za^~LZpy3AoL~$SC3vMGUkd=tUMHa9oC9ow5V#Q&0P6AvX?Uqw z#cE4PpJyp*@MIVPkaeTRW*p0FLaAkqjs*jUtVP64 zsen(^j`kr<5tD*g&Vt~m((OyY=a!w%|ZoHQG0N>(}^`r~Pmi7_%rm~bjC|64} z`ekDIBL1`oLm$Y8YgIm&0>;KSstXm6dp95_u9hT zlBUH<3MfwE6U%%l*U6BVaz$^5RFjJM2z^J+80Stl+!P2A@kwzJtenC5M@OFO7mZ{)3 zW=oFSUKoLH~d3=gtkDr1icBEKB+J_Z}LAbJC%B1FKN^0=Ns=b+ah2U`sPmsRIc z9^S~5;R?OLtLCZW zhd)Wcn5Xm0QFjCXSJd)uddEub6^)7~Jx=b2N;Y-m*0cwI!a>xmtS>GgyPX@F1aAj1hYG=oFE=2z8a=LGgHy46R+r9c@L+ z(8b|gi!E5H2F0LA{0S4R6@&B?$y%F0ZcAOPemce-CZM!YltSK2yUGS4d-qoY#+Iix@3g)787@VwWjv7&dTiP+b?)R zlI9~09?puL&|!SFV`poW629FST*Z?YHjxiyD2aPM>d!7bgYRIW%`1Rpe@dKu%_p>= zP(O~11V&$Ef+Ha;iGY@BRk9IDeA~hE5CqUZEI|Apt0$-df3j_&ZpG{Y@#a%*y-m1) z3eSu2mHUtEpM7~hs*p)y)V%73Bk&Pz{-GzBW*vD<8!duyfi&1A(#+!;e^gEpDkRAC z>tA+O$DfxPG~LIX5+qm3S6yQ*i%R_796Z>b{VQ%Gl8*3^I;nGo?VSRp$OxQk;*)oK zSpJ_p8G`;88?63X%(Pmlt7c5q0`SJyqj*H468~fN>O}-eP<3(>;YJK2nqffA;bp zsm({wgBaJB(LQ9ag??mAbIMJsId)uumIPs@)>-$i#MXj6xDi|<(gZLl%*3ejo)zva z0N}(peD~Eng6YK+rr>y|-nnG($QG=}XP&zsZVMxxe*498mC~Ec0k2nkQ!Zm(X%88|{IS!jf^7fgQ_njP9+iP6PYp`N2F8mWi5Mad%9abc1pyA3P8CfENE5;h z{aMJZF8bR8IWS%aT)o~P>k#q`Or67%L&i3hf76Uuk+vK79P+28(CQe>w2Pd}O3;{; zhDgD6F%j+ji;09^gkl6Sf)%+Ea>v3o^=TBWg2`H%IwLMPOPGkiW!x2CY+v_Tg31;L z42<_8t6WUHR9DsFhOCAd1*~aI(xZBju%58m&*B^S%&rMM0dXwC%VXbW$SI?dEhlER z`;Ar_4G2+Q5l?OX5pxH6tH$J}%W+v^L7v!st$g;u4G><`dB&7x&rNp4J#d?LfJ8wg z#upp}gHj@I5dY-hT~klHE8^_4_fY`X1mrvvuT*d43PDApN7Lpll!U&s2ZPv8+m$_E zC{kcwhY=KOu#0s*N^FGsH9?V=u!*^`A+6|HMxFG(VHpX1kPKCY_?D(+^Ddj_|W)Ugy0MlM=*_5`c}P={a^sK0D6*6$Rf z&dHW5KxL6z)jApqYNXa6=}#_O1hSf-0g-hp;XgMiZ3IrVrB7=B>A5S$2Bp&o_^nVK z+1%p6XuC*0z z4a?|a_LmWIQ^@?bN79CzL-BZ)S8!hAum+EiJDD>MDXla|S0)ns(oz{PNJv&?%kXeO z(%p7P2Dt-<_pbRSWftUzDtf+rF_a&2QLR_?s@L_uGz&dge=@GflU{RZ!ETB;tof!0 zA6s7n{T{y9U_OGZga~{07}^vg zXFW$AW}AlVw10l}fUp^LKXG>x>IXR+WF)~G5iR;1XVC^ZxOCT-n%{if`DBBd{BZw~ z*fk&ULG;-_d;q(FiNJ2mlbms`!FeP?Ja$Ngi3FI%RRlN@GQyf)UL?>NScz;Uj_o{d zVtx%K!)BDP5M>d;IJQq}B@(5Ili9aXLzFT?F2EIdiZQs*mEcjr&g^X6j^ZOha*%Cm zl!Z-K;% zmZv-FydLMl(;h`hii~`LkMQ8c2fsUw`ePWZpL+gR63+-4Z~x_K6d9+gPM{cd%;`-+ zG^8Gls+_9(2siN&@~N!QmHf+pGco3ZSQhbbX7)>^`p>YD*_pA!_p=!Ul%)7b6eV#U ziGBoq4PL;dA7LZ8#zzj`f%8b&Yheb1^Xbf<)db$eq7r%~O2othVfBtH_%%%H#!Dtv zojZXW@|GWB>j>{47}qSO;DqKzV#aJ;ON7Jk;;RL)RCU6R6-ccy=Ap0Ieu$lSvPlFQ zL#IO-LlzlfvXmD?DE}N<7BFYiw@f-n2ZHW8Bp}${1abKrHw}?kEbaN#6B#U57_Y~N zqy&<+UoF1O6R_F=w!C7_#kgG`O5n4dF@FKS_7>kY=YA5ocmAw*b@`hivU*C+x)FxO zZ!xb^YFb<@yn~c7FB11Oge$S7@fTzm+fHc+Ae^-V2?kPY;$YCoEMkZ|urgGbPV^YoYVkg)V^v={uGt40SL)EvqedZo(f8nm|C3r}S#q zQ|Y1WCLh_1gPz+aGOlh8BHeN23c>CPl0 zt&fE30R9XNaeJpQ)5yJn!G)A3HRy#Y|O|%GibZP>=bbNF!F0$l>wnW zPIPDog98%wL98uiW%MKdSu@mD&F6{1MkDh*3Lo+NV#qbjfcN4#!rXVwd0-t*XOlbr zd3MK@YEIcQV8@l?x5Gz%WgM+re|d7__eIMX^YG!uW%au6^QE5MoPFC7eXRY)0P}{` z(L3;QcS{8jn)@0=IH_x`CqV+Dofrjyi-2$D@)fZWsyxY|UdLUp zgpzO(L9wqWrW419_9w2&D+F(qzA^zIk;yn<36=y7&)wi*O@^oiruD-m2ewt3`lU#` zu2nX$xd(paPY$@BKrZx3Ugp~eYcAp<&w?{~ogL9NX=zMo0WcvKCxC)~ zcyJZgP-lVJC^i;5WKPaK#um&+%|RMo2h$;06>qM!NwDr)6uVl0yPVMA^(Q6dglh6Q zJfa8%Xy3t*4yt5iOYw?Ua2@7L@qBe^#}y803N@qkxXy?&S*pV?__HS+bU|@6(u0Iy zi`4=WWK)`RD`YZh%rUwu5?m`a<4)Q9l@L+oUP#U30Vgx31Vh(V*ccH46&{L9uz_wqFs&mw@)}z<&LAm@I=E ze~g%gCOT1jAcCEc%Wb=ScITDu*oBJZ;Z1J$wqHMAglzg|;$HoMJGg$?E zf^T|fU%}GdM7W_O-sPmkN<4^C8@hox7T-s9X4j?N13ac@Xs$HaAdqn91=ZrQ*Ci8o zt1n}F^U-8WFZ-BD@Ch*X?|ZMhSNl6CCItNMGg2A~xWhYc(m^CHki1$9NDA^R$O!rl zq*&Nrff8SH7j=GBf@sIZjx+eJKfx7f2>N$mr0ndd+(81uUJ}t+OucI;8_ppJZUT~b z6^|f|PyJpW7OrrfOj5kkq;$VMAIVAMDW@eP!xx9i2znN7@wjgO|KZkwp!+Wa{4ZaD z8od32nvQ?|@A8G7U(|UQ7c1A}Z_-`&ii(f6o5?BMq|yxAYmqsoTztX*PXFLC*sa-caW|y#n3#~N5`cAY_RscvBYsTk~Z*?6@U#l(YP`EIH1P0 zk2X-*XmW9sW_WArKNm^6I}al2W|osHuz3pj!~vQr%?Ma4sItK@Kw48wB(hd+byt)a z`(X3xtw2@gBRqp6z!bO}zMwUWB;rqk)nXI%Y$dLXvL2}r`Io7f`KLVo=A?U8&zHLB zNfr=Lp9+!eNue=S*;BcHcf35RL8vvb5ZkS|sI$EE$k3$;4uVluf`>ryRK=>fknR!9 z&x3VFg_Ndou2jjBOj>TVXAWg7rG6?8mjL*7r3~b3dEH4oa5*XS)EJ}cJkDB#fhZQ9 z$TXh0clNnmSTl|v*e2Yo z;psbdipQaeJ$g{Gu+um35BJ?Td0>ZR=b2q@gYwj!S+|X!z$O<65x6z)CcEEQ%t{qnfgGvxaOMTJ0Q5T9A6-maPjr`#oY>8#>6J#DeFUDFg zK2QeR_zjKUv)tglJFcYb30$N2;`ZM_L#FOr%BsSqGt+D?uA1}11(z!Wq{n{ z#>=L6{IfPlaVp6cnMxPgB%)kd)sh5jO_ga!pd%J0TqHN;{8uY14VV<#i64+#jw+T1 zT6p*BMM!jk9IjfJNX6hkLe51bX#66N_#$Peq*v}O2tmev^&)^DIdej`T=W*6xtQ^Q zX*E(3;Q1Ex4p4w53ib+5iEXd~iavsul7{qvF-r(W5NtEj*#2r35J4M506EYm1YWk{cc*B&H2vizK5| zRU|S=BM6eohg+5{*^-#XscKR}ib_iUthbo|Xz73!O$kCpce!k3n zyq+%LB3ZE|BFVh6Y|((GAXo$%2i`b(Hm$9)CN!U>u1+a}W&+HjlC3S4o^1mjR=7cK zNE({UCWYo)8$Ol(r3}2tyP0vNEn*~4^3M~Pg7?BHU`C{b7gJzvQFqhF29M0`9de5u z9T&+h{pDP+R2wgLXH`OJ;Xm@suJOli(E?=Wm3S=DaWqslNHMjEXjt^tflxCZG@&%W z8jFTDmSs+vDNt=|sW7)o>sd9lwB&%5BlDWgvu?i%75~hx5kmN41~9$l2gGkNvi(h$ z6CVuw-#03AxZLpLwk`esb zfgJH70bP(7IzuL605-CRkDwe8lSc^xJK*HF_Ec`>2=IIF^`ay|_eqZ5J>~X=un&V| zK8=Tn`XDwGdy`%1pPYI9F}+hU>&hl3FcD0lCXdNMd{+hadheB-6XUB3^Cc^BOO!f# z?m!v(b0|r%kGdj5>8Of?B|ET_z`IbV<6qhX@pMWPq_RG1&JwY~iO>Iw(4MHdeN5n) ziGe05kf>@PFw+L~a~~ zDm$j&pB)lw?4Xnskb;no4x1?)ZkAM>-~=6p(xWv_v~?+N(uY)4_!72FV69rMtMR>TsmqE3GfToh-qlaqT~HRYjE90kg6!JPP?g5MRRIPqHJYVNqh`{ zIWqS-H^w;Y4^k19vKYm=l))+Lb@ldQB5g-+?RJLHY3E$u6*7|ZtcNk(L5-^pmqiIm za_srRYX>p=oF#HN1mTGtgU8}Ug>mezCDITgML(b8F&}0|~KI}+};)LQx@H3C94Z-6n`?KFVyLUa$sz7=CHuwv#X4O&>f-PCu z0Xm!#-&i41>$`5iNImSkGInyz$SXMt*caw6CZe{2yJe53=3>rQs13Og1Svy2JAC%k zqh}VT54ot{`cvwWopJi7hST)tQ^}J*>nVTpUQbTyUtdGr@`npkyH?pc>4vVO5lbzm zPpEfCJX_b>`*b&Bqi6^YIK@h`H3^i&ZyeKB{pNA4oy0n1r_TuuRs{^2DUjd zDyQI9?7oDBBp@pjBVJyX@{k+e(~&lRJ}QL)>;b6YBV@wrD)d5Nd)*~~B63icix;iX z?yGqz3=2CSIZ>GB=78Qb0v`b}Uek5*Cya(NXd+2mv9XoYqIs zlp=A9e7IPkmuyjL_$=k*a{zcl0GlI-q!b7Hr8F$dR7w;!^IC?Hcyx2%fyg1fWOSikJFq9=8C|De&8_q%PD{ z$Q{AH>GMeSf{#G-(iEK2q|}SXx29=f6BN{!I+aerp0!dDv-?zVzEv)!nZigg&Co{x z|DUq~2YF)fhMI{GQn3~E){2Fa;^aVL9w9Mht7rM;E(bkMvrs&%#@pdgqf*X#P`aVn zl8O~OCEg7*jwMgyc-?3NwiO2tdJ^tgC$@aria#0QjvQJt{lXOr$u9|*Xp9GP=De;@ zW(R82XBH_|c9aULYej$%gsYORF9PBFNZOyw+lWe=Tfz0;wv{qH8Fi$r#NwtpKgwp8r>K>(|pE+;Gxss}zmaD-iyWraecN6Q6U1UVHbG5?C-qH3G9Q{HI4(+A+#44-+f)m zBN=&_GVrUerxtZ);;m;e*5c^&BgIFWOMB-(!p03ZqaP_g0vUmT0PTv8V6Qd% z)E(1oqfFRU<`?^hiwgM=Vt$6E5IgKIWX4gu(pt3h#3K`HxQUx>ytj;~$93 z5SEjw@Mrl5qf+4&C!5h1MeTWwWv;iBZ%An+to|pl&(|j3>7Ug@#Rq&1gHN?cQMMwf zU^{gdmd=3F^@QjaECTOl4kD>WI{-c;(gk*va0IqG`uM0zkTH8d49JYiAUnFMok6Zr z8W7o5=aQ4^SR<-m4V86|o*G{3ROZ5!wA*>l6^z#%e}VO6=H-V#q&O%+jfowNSuD{P z(Uo8m3p+OaI|uPYV8?`aV>~>42aE9dQ+H(l_dSE>@5dpULsyh1?}SPdJy_=Q&_*^x zjBopOgS{Ksk#p?cYjBv7dOyC6pxo;CP{twOpb@Bb=a=N-Tn^f)qYP2F?yw1xceHRc zQ?Amlb=rsUks4j{kwy&|W7*55{v?y(WB6im2j9|ZI_kXAR3`08_(&!5dQp#hOZTq+ zS#%D(XXi@DMpO? zi7y%kd+bYl^m#rjG`?TbXYX?#v3)o2g>_q1f*9Pf6n%&GS{Q3>!$R*@gz3Xb53wN{ zSPit;s`$D~l~N*Cy#5Ecw6j6j$*q@@fUO4gqJ&BO=przxKoQe)T-r&4kQ0D}UI70Er7IYR2$OJ^E<}V`I9G7y<+sGYvw^a@ zqvml?r4Sn8z!Dcc8CJ6J^A%r%eH6syz2qr^YS=j7P0N;rlcNiH%EACUD@fOhg~G@b ziN!XE(JPiGB6Ko=FMypWk2Xb^18vk9MBs|Rwq&_0080fC7y49|XrPol2-r`Q<2AUk z`)V0k*5{z>5;fZzsF`}mM{|`s47K1zwn?y7Wvi%aeT?l>eGsWQR#PZJCYgggO-b=a zXci5e^6G^&i`5JSx zNEue1D^nX0VVZcEEj)X58x`;-n<5$nc})j{IjO9~`GhQ>wLzqyWn7Z|iY)?}lfD&_ z*ku`P6q_EQc77izNd^PAc*FwJ9e6RLHKSlu2Yk-n#|!{lK&tCW9$_p^Enh&HNk2Yf z39BwCNnAN7bJ~+iB{$h0Ey+M4rI0wtoKJ9#847Fw1g5~K!^XS$I&+=OsiQ~AZct*GNBAp8zfL_FH4VP&b$_9}sBpK3=@S{}6QAZBEmzvrsZZo8wDD8%_$z{Pg z7)rv$I^h+OeuUSs;2`v1x^4au@)iRQ6DR7Whp{~-nuosX4_(JsjLm!zsbPLhgwnxd z+lf4B5U|H@_G^g1?eUwzW3%Y`pTB<=-&+<{43nZC5h*oXr0&BH_g`XP4H>D$JP<_; zqb#@Iy1k((P=rf2nZ)MG4d9?5W!D{ziRN3d4^0cudK@id@6x0cPV*~Y`yuK&nxMf7 zKVOF~MGE@rBu??!9uXDB z+On|o6DJe#4Er6UVS(~=G{lo=WVkNQ;WPB^D~e(H^ygO#uBcJMP2zQqn@KnYl%$9T z#H4tH=KYhLQVzke;epOZ7RO+ zFYRSS;qX3tRW=ZTWoVt%;%=1&WcXJaQfbgHuq*M)4=FhPyZrkS!}O8g@FV4rS%PYHomsxXKQjki<&{`w7K5_&0|7=^M=srgN%(BcN zrS2Y4kqDlZz$a84fgQLg5W<4X08q-h!IlVkJroOr(5B)r(m+~7-vVsza)gga6MQgR z#nt0X;rN1)r`ZC%iX-&EjM>eV&R?vkN^z4EBdUZv($)mB&9cxzzWApE@6A06(hJxN z(u>bNf0pthDp3tJHPMVuS$;K)skmebvH>5N{2+1mm~`-cclI%;1t$muB>0)sx|p$C zuyj-`amXDnvUy`xntJuhnxc4(Qc;Uj4=P*jAfqXCI&4(-ukX91JJDYuM=q3YI1oBJt#e=txAKXEtXpFCv1rgE^C!@o=VZ=-mec5nAL2nyUqYDZ9 zQepRn)z-cnvE5?-cYY@y-blO!@$KC!++z`oM3(nMcR5cHvk-QJx_^Ak<(G9BK7z{w zoeRA>)y6MmwlmNwR$Dk9?~Eur98Pmbqa?)zy!8dEDz0=40+mM@T-9$QAiZW};^ZvH zkfviU&mnfx=Woi+j;m)eDO<;tBqoiOH+s@3=^FoXnxLl}o(s_rwvLVpuZFHPTn9PaXU1M-DBZwttS{%3k7N@iE z+pe6vV+nj@ddp?T-oO5PGwd;U-K7M8HR{?8mrZWD{KQQ^wR7?G-1a>gS9 zdjkk#^29k1MQGFrg<-4&Dp>;yRu!NkXC60YjqT$*?r8M25FQ=MpBa%E z4JU$tOc94Co@G=}nn?quU@5S*=EQnsFz`l3J_C$)qkW()6faHZ$gvApa~R}p+%7c3 z@IHz}N*+PaOL+4EyS?M6i=QKChYb<%Qpb>f-{vXgY!D4XL=t9-U{i_m1n(g1ts6@>r-coxK3iBT4iZ}VN;xNdv&+#5~<2C<;0)!xN z0)3DaV7o=&{U}W^*pRSD8D5Xt0&fmYxpXbZ^+Wv6t|gEZEwVuSIDPQ!>^m|GzvE;T zUZXDly*G0Hn&=Ul#+4L+9VNm_LRfg|yOxt;V7y(TTEw%yGC|J(c}Uv@~PywgUWLLhS!p9x$hgNLF)yLV1ADjHM@T#xhX9 zOB4Z1|F0PWYQa-R{-=h`o4WBZ;pS zdo9Qaff{Z^KLR|!pB*u>{v(7cioKS*X?OaOsokpyf~Tc7Ab~BeP!=y=psF{je}VzJ z66Tx8N6qyC$&X7t^0%{CSP)0Jn6sX!?Dk`U)0rQ#wclWSJ2phaM*t`SAep`8r<4l0 zffjcd8Zprju!s8>8Ig$u_Cz~SK_Rj=Nu=?bEd?nxN&=c{;%>yk5qE^+WfajcSfkL2GLNvX zOzdt0;=S^iCYMN5xK2C@mN@jA%vNL?WqnEmq9w_Gh%_}ds-wzT1gd`3>2%8-#TJ?t zQ$x$3?ObeA=_AQwXPX+WjQ|z3EV@f#WjwnJflss{TPB~}>A=IO@aelyf=EoB-^>29 z#K^)#*0|7w>c!57omk;AuRmr?a-#pSyW(Tp3E+k_X&lKCN|1*)>G5~}+L^nTF`O_5 zl8>}@2 zOXEp~;`?tP4I_}F@<3EwkKJk?p}=}n`3CGmdx<-Kqm359wGv|uD+QMVxW?x1g3zY7 zU+I>JSZ8e^P~qi-%iw(ahVNt0HNE*e69kCfa4CkP$kl6K68zK%1w0i52-*?q0DI6W zlv*Uha8{v0IE{=T_5{=gbFw;da2FsgnhN*_V}OM~Yd#nNDRRz14qU8rk`-=g2r439 z*avvBo~mE)3@5_Kz+nCaFR^CUU8T6n(%rHFpv9lcll5N^`LmF65nKc=Y$e7JNlgLAI3e#OMO(2F(tHt2gpnot2)FAfSm*-UZ28-VEw& zSTaW#V39m*fshN4Uj;;^l2+LU6Ai1O%x`AZs?pMCMsO&y=g|r zKmc%I?CE9m`O_M4H)DoT=a>xjAb-GlwFU_&L$$NeH-xm$ne-&)B4aqZr`U0Y$N6|_ z?R%s6KqJ#Vm=MZ=?lKWx9W4*Zs3wGwVbIz?)>HMA1U3wpa#{@X4@w8F7X^k5(bN>R zO_nz7eLEu)D@2AprKKxX-_Z5gM4_Jw-E)j(cBGY@fTgowjI}hO9yCRJFZIoXbVQrH zX<-UubCC?bgHt&{LDw4K*MUtVjA%YbA&V19Ptx7IlwP2ZECw-oy^_I{#C^ zm0$260dW`R7KZK8C1mykB4yz`@>U>6xt#`w7^qYGE>$PmO1(V&-YcMe7=#vCPzLlM zlosNEBRO0E?f^v5OiRs3hgd=o;0T0T{rwBpy;EL9e-Ya%T|&&tTOlBrkikBpd@&rc z=3X&J2WCuv`1;g)etPmNC*lgf03MQ+CZ#AzUZfQnna7%|Md^bRb^PKF1@vtN+#iPZI?zSz>~vcE*?f z2TGY!$i)A4ak^Y)W&tyP`tM&`d<0KnOtvH=`j5y*I%6&3Yq|Ld28ul&!CD4Bf&;Sq zA+p=ZZC9{?2uOgKHf`m>0v16*bgYA=i%u?GFg3D(na+nGOXvb-v@7azzj1@eRk%(7 z4Q7226IIXGo{Kr<P|oa#+c$kC$ILsNOk+0tKB#L>#U^ z{W3-E!o@6AY}k4;Hc!Gw#H$UUDlnIt)#HlCbI@h!6hnAvLWXzqQT^jsX3@Z+bUwIq za9}&m9op;>-sG8kC!e`*=CC_=YnpXI4oVXK8%L2T7A%NV*dev|5-ltRz({Bed|5Ayz&W<)8_QBg0CHhJ>dVYp$8)FGsjZ z#%)M?2?`*gSQJBY>2ZeTdq=eIMeWW8yXdo2X04B-(7M_Cu0ari1vRn`!y4>d!U{9k z_KV3|-1`zfg4P4lfsJS#m^!Fb&9)k|8!yDKWPHny@L``^b%A2=)#nM-R_Uu-8!LbT zMAS%wBa;Uhcz*49n3`bG?YcCgFDk?8Fpk}PW!67`tv!I=lt7vl`>9_&i7gv1s~T zBuw3j39KMa(=5{4%M6K73A&X8aJNGKHEeuVN}}hH*BQETL!hXpPpu_2%Oi59L6$Bk z{zzi-rzI&TP0CBT!uYX{dCEb~_jmn8JTjvKMT0SB0gXli?A&O7#z2GI5abcn5QXyr z+hpi)P2bULnNa#4@rvqw>P}rK18GQH`a7SZ7&LDNGsV`U2igL0$a_g_v7@u2rlj`@ zZODs4->4Duh4jR~Et4(!f`i`ijp;sLDxJJoUPc$UDw}ha--;yEn~hT?TxyX>%t32o z;R&SSV*w8$&J|QH-RB4-0gup(5>hhBK0(x!W~ARkJe#V8sxgEte}*3=q!ARiCI=ed zwxxd&MlD!-*}spdkL&=5PlTpNNBoYLYiCLLV}?ZNQLr=sTe~BD zz~dGE{%hzj_`Di!m$erKM~MV$adv&~f^!C(1fN83lw>AON=`hAsx$|{=t@co8mE$G zi&X;87-MLm@u|k)!Z08N-&4e!@90ZT356DkrYl@G+SP0Ns%tbAKaNBIHl%9wJJeT> z5W=F*0w%t;1A8*UUtZ%&T@9i^7hHz0UHqb>iE~Q*2LI@8Z&sBp^2X2;+OsuX8qjly zPZTSh$c!X+;eDMMnD8J(1AZY)YW7+(i{cb=#$4AFxt({sV~)I38p%kW(gKaXu7eNa z$zv}M(}BM;AL&TI-^?OT_W;-G_?vV2_YMlIl>%f*CG9o>BxZ-Qg@sYV;zu_a; z%n)1)919r%AHqi_xBYB(>yI&z(8ScBA7^9#P!pWxi^WuTqcx2CZz<0xFRbKBol^Vrc#GCT_qfj-$%eUa^Ru^F#{vRAq5S7Th9HM z`jitHSL6{Mop0|juy0nKBK%;v_UkKfqwd!M^btc=ovtmZj!6Cw${$qA#xuRpe4FjRkPv`5dDufNjF6Y5iv{W7Uv?sWG7hP z^M+aiqpFEJyoci98=_j|@E!swwBd*T8e;O?{R7Y}ifrt)UVYqcSYLclr;^8RVN`4M z_3$QKq|uTHcXzKG-1ZxG=R|T&OcI@v2*#DrTPS5j#VCo;zP3D7QP=tN%QU1X9U&28jkz@1QskwhA$i;!CQep{ zKGIPYuJA6daNKuBiCI@_zUeEuDkTa`b#En_$SsMU@g0K8)XZJ z5s{?3*I_;(>|rHw4+!1H0Y!qeQT(+R6z*b;a^(qP2M!@8VT}S2xprX|jx|*9B)3S$ zvw$CNX`liS5;)Bl-v&>zppgco%5}(*5Vylrda}fc08A1Z60BIrB-aa=fo*C%^z8uA ziVC}diImk!Ayw2Xg5GEomg}0qpJFZUW=-Zq^d)Kn!Yv$M z`E{1-Vp!x4q6TvorXs!dT7ACpPL+-8k5EOTNdaa&NR4=k7(~2L%t?6&Ab=6qwda8z zN$=~3H;XSxa3gtA;9OTq&!2eZ;BQDFNkCK6@*ZG6())-VWO=w7x&){rG4^TkST}z% z@!PRn)q{PF?G~K{Zp>$h#3Vo(MtMw$Q+BTKmO7DFR@KssT;vwDR^T6YMu8n9wi_(z zTw4IjIWpgurip;5)Y<~@MH^v^$zu4ksD~A&<allYPWij z_wp3OlDiz-r2xFeJmH?Gt}q46b*_*te^wRv(=^fCaUxNo1+0i zWs89Hbsd#W6}TF?3QYbawrXhWXyL2j9e(j?4H}sT$$_V+f;0`+(Tx`|m3Wr2MdN1U zIL11}*E(@#rAZlseB9U|AC#R4wdWWT7ak z&|G9%IgvAL$YBBko02dE7&$V#HRns)Q!0j&rA_(>RBeC9VNH!J2sw9&kWD9B-;xe3 za+KzEcyL1PeEZsO;4_Q6-0Z&1$lO3ydQ-;5TU1=wyEcX{O@#JKI#X!11z>(xckkFW zsdP|j0tLWKg2fi_qdMb*lC4FO8I&RxihmzVG~e`uYG%wQJ|3oBqT+#C)@;LQl%ZgNn%gk(`DDgTjPKZ0@uI9K`+$q1Vv9=v0ikIZhql#R)V@r%TAY7KVU=eV#a?l25Hv(nXj z`Fy?w%w-ndiKX*h$FfwqZm`}s`~ieFgj(crV*;w+ESWXq3; zwuG|AxC)FArYfTmbpYmYpmg+!{N@N;&*r4uLSI?LfjI>=s7Y7wQViC~gv)k?D}}_O z8L9K2tj^L>^@JOb`Z{v184BtS+-i$JOOUyrx$Jb#VKSagCS3G-v6a7MHDFDMP=br- zU%VR%qu)AD@veW>(n%JUspo3o%zBf#qu#ToHi8Ha=KIm-7~2Fd0?DHz&@dH$9L(N! zv5Uu*ItbtOFSGluBfk3JY2s7u#tfB!X4AhrtXU>mL2a17iD7P0=N79jTo`QyKrO9- zCoP<^&DHaf5VYzPmq(m>WK!Cdv3zN3T6_wj#|H={USW+zH;ucx>YHcpktV$IFb7ev z41{%fw^0rVij`eO;KfE%aI^J?awJF>9w=DeQd3y=BU@%3l9d2-$B9pJ%P$S&c=uJ_ z#bM#ud#9h==>~jLf^-aJ@qEs)Ok4wZaz)`OguG z(1BtE(i764QmK?3Yw)h;<2+jra`HuzGL*Q|^uJ$7DFNvcn_-uy>6

6fH>w1ZzyZ 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 diff --git a/apps/desktop/src-tauri/assets/wix-dialog.bmp b/apps/desktop/src-tauri/assets/wix-dialog.bmp deleted file mode 100644 index 36625dc9f46d8c5a41301fa67de51eadc4cee370..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2180502 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≧Y%D6J;ef=3eb3#XJH1jGoYkdfuZUq3m0 z{S0_A^UV#2200S9Sx;w_^{lHOe1^tdF;=mX=r zb(El`EQnu(yG$KVidxP;ACfNU4{gU955inN9Hm??QFeh{yz>cU10>H5DuoY(iM$Yp zH3H|>z3PrW()5QuoPGtoLIT#%XZ!jmcYO+a@KS;EQDmj-{8^Wv`@W|i5IPp94=1wd zDRJ4pN>ISWqTd?+@f6`16Ii!k%gR?wV@O%Fd=ouFQ)ukBmi6yI87^>2 z!zQU6L2!_43A<}Kz*2u#zkx}CO&D0xJR<&AXr8V^daW=l&%q<{g%wLe#|4{!9t*wg zPs&G7B+WoX!fo=u573fx=TY+u`e)S53A6fKUV~-Ifo3YEDqA)S2 z5W17Q`!7^t>@jYEEmMmYq$W~>Sa*|-g()}hiJJx(#*Nj!t786wr3Rjh%o% z7=()>_V$VJiDYt4S&T!Yn5(q`a*mH~Oz!U329N}gFm)m^R!Ootytb99r>{_n;fXEL zci|WGp5HhXpR&1^tNKpb02?)s?R_Cpio($c>N#$nwLNYq%`5RDHF_M`La&7!fkbA9 z`=^KcaWVWOy#uReM&ch~v=1$!%)MR%GyxjG1=Giq7zQsu7Ba6g&~h&1y`1#|JiAL- zx$pG67z~bgY8$^sUk?(&RSQFAIpK&G7shTA!^jRRkVN8w9t)fZYNm!^_$x1<^||8( z&L7ACk1w!#phUn(Do)J*ky@2vDo}#&1N0!!D#OUaLF*hTJfiL}UJntz^i;PlVmAWl zKn(Z-38E_T$Gg#ewM&wjx;pZlJ69`U@HW5dcX1MD%Zp&d&_th7x_8lFgz>f*tl+s|Bj4YG0fXs8~#v z`}!#}j$e}nv|@Zl_>tsdofjZ*D0q+_499ft5Xl_W6Nw;8Q7DyqJ>*^*WIW^P-P0#_ zKq3LfN4Eiqb0cdeB(UvUKDX!7$qnyLu6wJuEPXw(TrK_`l;Jv5BW`ic8?)=*g)g|S z+ASeI*1z5038(@lfEd1UE>3ehJTkTTX%}aL815B6y9QEz5Xa>%L6L+*S^b&^$Fn5t zcX>=Gnn#kORq0!9FNWp$W_a&aJt92fgn*8U{|%?Z3(qp9a6|*U9+TB?D0ZH1YTUFDX6d zpQRbDW)}US_-;q7v&A0p{Te_S5Rz%P=h zG+HqW*v3a>#O{5>u0HKehI3g)0E?PzAOe5E9l#l3jD$6GTaTIkAPJFMgBpupW>CC;@kXhe4D_ zw@e;f*Bf1{E)(G;oDEXWZ>pHMqFwgIdLx&Nn6MTZSIn6xiD|gQRzUwVVJHfl`KD>>^USkj6#}l_4A?32(y1pOO@lcJE}OwD2a;2oYOd8ZLaU4_DsL<{RsdadO8HV=CX_-%FVK!gJKDr652 z608x6dQ2b4ktYx%Cuk?Byx(vo@JOUtCD8imp7$uB%Q*`Z z|ij0-+PNMv+PGUItB#7mz~@h7;I!4#OYehOrAeaC8*X8^F`k(H&TY+u`x)tbFpj&}%1-cdZ z&sG2)G0bG(Srqe#(WUM28iefe0w$4*ta@3H>BNpt)3dPaqqzgCvu`XKR~5U#{_1g> zeER>2V@+TYc#=(fxl$Jn<1b9n3mef#BfP;i*bfg{;hw%xMG`u@F)X-)@d2CO92ubl z42Q;YW9s-|@8YoPz-Bzjk4)xg2BHnCSXWbSQ!BY%Fp}K=YS!eOtfxw^O%U#-D)K%T zbA2nD;0vJefk6hBD6=EhLXpsJX_U}Ef}@!n+Zz9fd6;;NitOl40Ksbr7ARvi;aQ-T zcq>T6>`gIs>tY%J;gO2Q#V;80BWL351{OdDjeFPu_H7EHN|Dm{2Z^{OQtaFT04F$$ zg3E=0;w;yH%H?^wQA>991NYSPA($*|`E(K?WhHM{#8%8BSAs`$8s1N~RqtV;38jZE z@)t!?{0P(#ZB>(OHG%`m0H8FC6zh>|NF+9sI`d(&eBBt*#D#ezXC?#~s0lgJ&UY6N z``yhLQtqb2Wd5Fxk|*UOq-!ii&_|pjdHs18av(f;D zhXIqVDq8#q7?~3jJ(`GSaFXR`X4?np{@C@|#I}!T_br>-^S9Z-CGIk-UuI+@(uC=Q z+6wSw{QxB-i`_jhtl_=*MK7N0>BE7&(9u6vg8@x<(yd*3Dm>e z0(vAv&k5^dFo~rd!?N}*zI7+$Rgq>@xRxKe#7?pze)j0f`jf#0p)CBF>1dJv?C))N z?`{RU73fx=TY+u`x)u1}E70^><|wjrm$r**da-Sr;cC^Z8Jw2C_;~aC^PB%Px%0C$ zEbRKIx8tL!fyFkct5Ca&8(KtC4D6}TPT;EA2$QEBZw?FF)ONf}PBS;Q&Mi~p2QtiK zQh*UjTo*@T84#x04C5tCGJ6Mu7}2pO31Us#onyniIhK`sK@lWc3N6Aq5or*DAjRSt zm<`y;o=uXiA2qXi%nQcg@qN)vvBnSe*+{pkKDB>YCRMi#ubDfvmR+THbW(Vx0n` z*t#WsRY(-Kd)_Icgh+|GMrAo~$XN#eebEkKQZ61!PylkoQs6{r;jCMsDhBPc%%4gH zeAS=_{y-&wKv5W4*PC9e{3CI)=knk_{nh;a1&Go6A=<44MV21{8U;6cf`ZfAem@Wc z=y3b@3j!^)NbcqK0x!LLU$c|6XAC8ja4ja3q-@3411H`*>t%BcX1}wsx8?o0Eq|WgyGT_!bP0Wc9vBmOyuEFI@c~w+-W&K_Hg8%&_NG)FHAbq(niP`= z$b%76K%?uBZ+&oy;`QFbIYFds@O(ox&R6QWOgYC^Zfr|?>@imsIF~+`)@WN zGD-v?ar++HVsqCf~Pd>PDZY=f0;bblhAiwI2kXH-Bi4&pf*^zq(;L(GNfO=|r z?YSvHQHotP4FfoZA<2CE?vsj`L_z%8ah?}`0n{Ujv$zsS;C;fer165gNKTX&6rDxG z9;d*2afrbAkxqP6+epuz$Re5un(Hu>R3}L;T(M!F{?bxB9RWcB)${T*Q)|zRUphCH zvGJ8WJ8!`}l7yERlM1*iO}u(MbrRoy$pC~K;Wa6GQkBHx38D@8->lYW0$d8mGGgwe44{%y16M_4^-{s{6&ruB&M*40et%xd3{wvlvYcYtpskRR(nNQ zF;S~O%BjH{Qa+Kt#vu~A5pRfG!HK`R@K^QO<1T4pB05VB;UXhgOwwv$K!p5W1WH&; zqy_aMgY1teeg59EbO$B|21iDOjs9A=-HTzGinCPR5YL`SKZ>Du;uxYBO3>OXhw*}q z7iKnb3f$z?<9Ri7Mgr}goiYQHba!zNPsGacdxFCc?A%zq<@}*NYJwmpsZl6`>)hD- z6AzO?rCbO~^7u~?6GaSD%JA4-%SZ0Hc;wz3RCe=ad7c+~CP*K*{`CHEmJ<(LI&hn) zs+8#|KH~G+@vHqbT43dCZ%L(95msC$!lk%KBE{M&?xW3e<_MkS8orm!Foo;-0dSf(Yen?W3tJgGPfh;6cUlX+WG8?>(OoqmmEvjEKe;U6TkXrK`o&EZhzX zt~@oJs_QS#3&;^vL&_nU6||}RM-VCaU7GiVS5+O85CPQVKk}LQC~1Td0U4Z60X(RbNBIluJI5dN=*Ay^*SRz~|E0ef%RE&rWfXY`(mB zN#fs^4rM9+)r{GGwCyh*UjOw=apfdu)NfIes6{^g6;IIm=fARVRPmCKfpdu9LFHe- z2$B((kKoUrqa;yP$V{b~#`G+Rl`4MJ3S{!qMoCj9H0y)c;x!?TRiY5nL~!UBv#$T1GWP)*-O!*|0^~ zp2batFQ{Mn%;d$>53iqn)DfOLkyOfXB-m+g_r>w+&1b}fTS1)R#^UX(Pdsw*;2u>A z$)FrD1J_Rdbn~&>HxJ!x0$-Tj9pSdx9E*E4&W>F~mY<*I0bDxqUuj?qKv!x>|cNVRe48-#J~T) zzooAIf-DNNM`3ro3`U#>AcP22O4D7eFG`Y>J#!WZ~aDfCL0`U8{f#rM_ zq#5z29KJnHz`32Mfk|#oas)`6m1wAi9r{Gf&g#&fxK8*uNJ7*Pnn}oI*GbGN%mo2afn6IlM{tuWKC-?TVTdC} zg?ez8pdOKpEbgUYryy%NiL_e2_7MmB&&xlCZ2fb8aE7Y}3k((*EHGGLu)tt}!2;j0 z06qe+*{t*mgXbvmnUud`MfP6}w=9icg^$>A3LXnjA-tWq#&WAQM?v|Swh|4y~ObmxLav~5fE565)7DM2vDMfK1v-x5l{pppf8;N=T8L-jR3Gm@SpPsqIWAglLbnY0+b2CfN z&L_!|C@)cDpT8F`2~N?<6xA3LMfMJ`z5dCYTsGV>;T_p)l|)Q5mo^60n{q9@Dxn+EjP$`cO$2Bqd`s*WR+7&&qgclZ z`D;FWC88dR7*rAS!c&@$Q~=xNU;dbwF75F|TKnYeeKwUoK6)cL5HK&4L{ezZpbrzh zMyk)%;vkAgK_k7`+S3#3Plom`J+)tYs{{(3`T6phM_117yD)!C;)|%9@R6EYJ(JfyCUo= zr>`B^i_2uRlaa2Y?LLKF`K{R;=cADa5AR4u?wR;X?4vv>R9Z*;W(8}lPTi2Rh!QGM zP9i@v+$6cSDkw`dm&zk}O6rfy?+QclGo0HIrlSm^!b7v$vuJSWRzGn2I7@z~v%9|Y zcVqZESYWWgV1dB`g9Qc)3>NrzSpXjq4FVm3UJ?PpbwODZ+bBn%tPXAuLW9A^uYse$ zw&19VYnKn*Zs~aC$Q`OwCnjG!zw(0WLCBaY^1n)Z$aEAi`P6vO{*z-?q&F?$&g=vW z1MMcSqefXh`*WN{t|Xfp<&{&HsN|{m1u%$sK;j$;q4@m0)bjrD<-qkXF3UzfjB^A# zP>>x9$bx+3`vtR8dHnYnjmGKZlh;E)UYO(is0w$-aE*9I9{=g`*}gc9K+%8q~ep=8BCHI;h26`#JvwZWScppG<^ zn4;56MP#8`x>0dV^f-A;{_@!>`hY})ag?Xv<5Y5y3M}zgUy@^d5WT!RN)g~wGl6Py z3v(F#AwH4ZG!biJ*nYS6#YcP`&B#{&`lSji#YetMLak;WMNC3Z&<(>l|1}3nh(^0e zd}0mQ$KO%|u?09CvXy>BZFO>Bl!V{J;~0Zf!<-s4KAc3J0uA}=A9JCsza>1W4*wh$ z)7^5!eEGga1WBi+Fk)4ZB$kO@Mkv|`uc$JKn+_FZ~vLI$lB z(Nkw0Aw^$#;vwn=z5_Ee6H*SVxt%Lh+js$x0&W77*^GtiO=%kr;*`dwx34gK(Ee0h zDw&WI@n|>;e(Md|cRce``Hw2WgWcG7&?Oq==diGw_XJh%(5wVjA?aH|S@DJxk1*9W z)=6K&s~C1ayUW<(VN~^_;=7_8Y2J}y?y1}E$C9Fl<4JkP(c^41CThDAzf-aoAJL5O zBUD1U_KMfqm=90seFvRYQUoQ5oMi5L>?8b(8welaKw%IL6xBwAave?vU|qSz=3J8f zuFP%M(oJzFjlc5;XV9~M#~{Pk!2*K?1`7-p7%VVY;GebtJ^KhON%bRKWbA5;1EPaW z&=3#~23t~Fpn_}j5lGE2K` zPLp^9Y&;ZSe$d(tl!R=dy#t{q3jCG)ByjijqiZi6y!hlKNBC%(b4xAdlS99du2tAU+7ELI!084*p z%|BXua5~9K76`+Dbo|FmCU`7{Fmd#V9>X3 z{^}r0R%qGplT!=zz$UarNxI@8F2n2e#cw%lI7gV5f`)IfeLyrK>n;+ZWv6J+%)Z%(5r~q)RuB!SJ7fe6 zaRk{B#MS%Q$NbG`f?taEusvT%xJ+%v;1nkd2aU6sh_Cv|9S`rtS%iTZgRcsgU>{h9 z(P|Sl*6j6;U>x`b>XDySF{uzDjH3!5atB z!-azd1`7-p7%VVYV6ebof$vxVAA!C?I_7rc7Zz^;sz9mfs~esHPL_f@YDG(DFYUjw z;nwPryFj3og`I003dF^yf4+Whf*@UiY;pdg{Z}f^9~oIYaqsFvAv&wm;%X{rUndr9 zS01~Qc0**B64;8+iKJ3tL}IIC`u*r_z#c>o^!@NnIhTY6C6|{>xm8OsNw+GxDIOE> zm7JV&ByjqR%|Cwu(`E%el9m_1+U2tobO$mr$>6F!g(R*kPdp-0OT~&rogLb<{@Cpo z9=j)eB->A;XeN9<`{<=7_b)#=ma>Tw&%Swz187YuFluX6#YG=|^49XlZ}aAi1l?6K|s_Oyj`%3Cg56-iLdMFygtKyPBBuZ@*na14%y*pT4 z=`bN`t)>qVOT1Rd8bJ*vl1~1hpKSO=3W5-xOR*IW5-KEvR{1HKiG+Y)F2=^Mfxmq7 zCq-Md34Y8?)oS#hW zmMA8SBc6!;xi<5EJpD_1yE=K5peZb-ohCkss*?FZFc{WT*|!YIakn70!Vdg)bV5bW zdxA39M>_Q8R0dESBu*TJ!mm$DwcV-ONj#KRuj9DM1^SUn8th28#%wCTYu{fDK#W#0 z{Z_$5dA9_rNiXbnSVqFwL|rOMGMj|JKJ&2rDYHqUWq3)qMUWzP>%h$&qZOZsz%Y%a zDLZ(}^0Z}#e~8x_N|1c#Z|LxKu)tt}!2*K?1`7-p_}46ek02w$<`E4riBBY}%A)bd zl1fOhy^;#PI%}i*fStfIunlU8`1k08Qm0-z^$>k=VxNRLJ;kB$i}f=Awdb1x`Pn0mn2{B}%1WEb&rs@=VDQm)qx#6*&=cg_){sHILUzw!Pv zk}|Em_u|@bQH&Q;e?-pPQhDPe?LkOo+wjw}3mW)SP!!1S!gns3BOF`_vmcke|jB_hRMa zElIn<;3++ICL>ltwqMc`2g%s| z5GKffn2aef26aCSqr8N_Nw$w09grQ|KYeV%YxdBU~Z!9c`X_bnR?{YA&7p*;2|q z>;;gBO|ob_mh47)_PI&hi_}zIh7l~ZbSz7foU7F3l;f`+2l%NxHs3gvY{0KRW?$*{ z)J%U{o%CmuCindps+fh`Q?I2!{O?{%DA!x3Q>_Cduj6iLQ`8j^?`tiFJRarW6&*qUkHxBQ?N-iC}@6z!HGX4_}UQkx=iAUD3s&fXQ0lP6r=chBJ|k6Bd?myY95RU$>_mBi%<*g{u$6rG(cwy4>Z(G`5a(LYBqm0D84CcQ+UHz{=q($?svX{n%*J9IcuLL8t3@1*NQ(OAMHz%w z*+E`V3=|*}UItwRoJ*xhhptR-zXBKLS5W=C%5k}HA%8>$ZSgfNr>=L36Qu8gnA^%*A+hH-sR%Cq6s1P&UzpmS?7k%% zNlnJt>uFW|tPHbB-CRrX}BB-!71km>Jhq8DYn8~ ziiz-*U?p5LXvk&#NQlYIjzKW;oxhyJ*TDjV1qKTY78oorSm57f0emDeEt7GJKxiNt zyMF{ zD2f*k#k)_X-lA+&{3BH=J*mBDpI=@~p+kve3BW)9&C2iI(8G!f-N*0c9FeJb{m5Nd zNmK?$@5uVOK>FhkC_;^l*p`moMGLd^cmyw9EN=-i@)P9RUQST z2jh_@k`p<*K7MT-b;4rAUm+%c`ed2Yp|Xbs|CdNVn7`yC@kd0r@_v#G3rItG{vhG7 z5{mD}V^X=TswQK^>D&#erH)_xwcIhN%@-f&;j*7av{iIEH1cFAbSN?a@UV#E`pXw< z(V_g2G#*XRVVD#dyI?Ier1rDpLC_h=%`_;DNhrz16oPM`NYRk0#^|6jC^`OZs7jhu znRrSTBZ{Z~3=I+>8dt4uj_ zEWuOzktJh6%2qeNR3#6_HrR)k#04?OFMQsEuBNGw(aSvRCM;^EtKq(trrNGsOe+S$~6 zuM#AQ)RLP^GofFV_16(8wiqoLkBgY}Csx8U!b;+}E>~vvm*=amzWR6l{TcchEHGGL zu)tt}!2*K?1`Cvbq_7sCHMPxhap1>+J-y}(fI&*|m7R8%*s&;`wPiT1u;0@0`vhyP zo_X}*;+<-XtVjZEi4`8Jh3FA_S9Tv30Y3v}i$abDmi@q^I3+k}J(KdXv|9x6QxxGJYilaT~&UHW$8()r#t?cgJq zQUfKMf{eIHmt`z65{|y+zwYjuaUY^e82-S*^nou-iwAd}b@cq7Ay?E_g>D$70$!64sAs zl;6auytXFTPIkK*sT6ILn5)Dn>2$D5obi2ZM1?m=WQ=L$v-F!Yh zvvzjh#gji%LUH}Mnf8)!Mx+qDautZl>*BZ`SG8p0y{EVv;)ww4KYCSJ6VXa?q7Z3V6^jyMzMp`B9}MpoioD5WRxW{y5I!SKdeJV8pKY5GlgyRD*pv#D}Bk@$F^$<^mkKh+{>!=F7j5xIS zoR(qx=R~1z1<{Sn%~jH!BwFcoFmhj`Mqgx{x?CcsW|ed z;6Vvllw51_DzrWN^sb^_HADRz^p+V_6dM_7KGN*lL0>TiJI1MSA$}1WQe}NQKr6>9 z>6WkD8Qd#=O6d(rnZ6-ujVVg1Y+`g+GdKD@x?Pw;*j94<8j>AL0qrr_oXbHGUmnsJe=&i#X){d@XLG;}*yV6ebo zfx!ZU1qKWJP%RKXvK6BeoF-pr)k=Vc;wzcm1+Zl~I$3|O7E3QSrlYwW5qyG=WJfcU zM0mU+ag}*Boe&Ohg~vc^S=qL(3k8)UM*r5RgAXA_i?lxHQeawd0$n6-)f)wh3MQ$I z{@jA3uL^}E&g#@p)?YZN6v)Oai*jx8FTniKZoL2eCQrmC@2p7hOYTih4(C5RA?%96 zWI3*vhTMCKry}O@u;zQsVdfxkxro|7`8_Luw{2Ju@SsEMaEvzqI+@IpxUl zk(Ia3uKwy7o`}#8fm=$M|LSRD7KbLxENn$O;*U}i(fHN!NZ=NB@I?vRA$+N~iWhXu z74HeorRSA*&aAv0-_VA;{PqRJgatb2D?Sup-W@Ao%)Z`~O-XWHcVV;-Uze;bl#K^R z=$D&TH4rWdQBqNts9jh&yYQNj5pE0YqsWo*@V4+}@R)?={86Qqa^*yQuPWsi$)J^d zTz`qM{n9t#BepHi$a;h%iQw9}nC!=uPk0mwM*pIRgr#vLefIw5Z(l_}E%GOTs{Csv zhIM}XSO)(EG7{dQieHt^Avuq0g9u0*pd4-{whJe3)W!t!m21y_Z%&~A__E{JK~Fqi zc?2{tKiwrvCvl9|Mcba<+M}0RCv`kWR_1r>$|WW4^z4)-3%lvr z2MNg!^}odM+=B%M3k((*EHGH$`?UZ*0&4+F5SdfRBbpx;z_Bn7T^l1aTi#qv88op&AzpCys+D~mWzQ* z%g7atQ@$9LO^)3Y`M?VYH(!{$D0}en`?xmL1X+G+f`mLNt=@Zf{a0r>7XrwYbXU!s z%9{s)*Wis~_{e!`k)wAn9lj@ekjL)OgU~Ewgl7aWiz?*cZo=MRIY4yb=FLMl;~nQ0 zZoTx_9mzy=09)C`MPlcI$_@v2D|!%Z7_Hhc7Z+!e(2#d;( zQXGFyqW)2w4&u9@^oq8M>g4zD%3!?qo0m6<_ibv;|MCo(KVGA^xMO4?`{4DJkAI~; zppq4*z&bI`@83>lFgBFwt9A5TGiXRT zO>%cbs;IL>kyX0jlrLm(*?J`q;MXAfpid{LPeGGEC-E=#!AsJyTNl0=MRSp~<1Ctq z3ggWo)z?F(8pCb`kA3u(hvI&Uv*WSb%_8s1Eb8fGLYj(Te9xt32PPzLf;HnQXp6bm zG^7YZ7_Q0b@~EX0Smz3Xy%CmtUTe}i72>QmVhgDdXC(+xkD?oR^oU}QS1RlYo|I(z z9sLBVle$qw9&+VK$yFswQoaWbk^e!0YgN=yCoziAVdXdFM~6xn8{XkFJCpHWX zaugIN7$Mo%{M9*>q}TcR2v+3deQst&84QOr@xG?>}3a`axlp(sj@-nF#2ck}q&3YtjF zMH=spo%eY8>G9=fXAPUEWaH(-#SmzB+l&(iOWvPqMQW9zkV8jS$^-im0!Q8F-c3(f+dtI3A^|{ z3}Ed)KEX{$OQM38D2(2OV5C!l{YTP*At{_M8SS4wO2Ac#O`?lOOv+J$gVdH(z{#wo z@Zx`c<~8XpxseU>!`I_K`s6J-ny@FD8li449R!a?w8>L=09{ zk)??%F_CtZBv7hyWC_0FnCls+pJpOk&W@`EBk9dlbQN0R{l-sGGt;NdS5w;URwk}A zCC~(0DlPhr9J%ABva9|qY@~IHlg;yy0GT*j4TQ0s$eYy1p7cwnysK4ecY!<#z7Fb^_f&2 z*_gS>B==j+zH01MV=-cO9+0_RED$C#w~Nl5yd4d(9Krs*|Brh1AM{_$;o%1h3>FwH zFj!!)z+i!Yw*~MKXbm1C!UaD-CJ-(Qzwns2`pP~8Uj>$s1{W-Z-Z!A51+iOUA5a+_ z*t7*)zc_Y<2EcrC7KkXgjst}-mTXocqtdqP+={damfWbYrGAoM3qnI)%Kriif`|z? zS-fTQ#C>sa9KUz<$+6T`e};H`L9SgmeEEe%n;o!Ry^+8P)MGa3RTD)sUZuCKSIg-No1tSfH&Oobq_u83XkgM?HTs%8Q-oEn8?9x-y3bL#{J-H^{?Bzr2FD)7*%CPy` zV@jEz2Ju$#n|!9W$ZtAD@cKt@C)f-rkn;-uFD*>suL$J%JG3W?;RjKl<)wv<7w2u5 zS86rBIN#zL^(fa~J+}Uv=fhbrk=IZ0(Wq0I2)7pxuD&p<+~lgDvR99-ynTA}&BrBG z=4!Et0o&wWaf=ULF^0`gBan~Jf&~8a3?PFwx|8Jb&{MvYVVNL0o*N-keJHPS5jL0`xlodb>?=$h_ znoh-#I5jdYw-g^>zZvVeEwfHcAX-hwfv50LOl~s?waJv8i0`9_Oe7>)#vfreVjS^w zY-yt^cb1u{LA+z;;Z!Av%TDeRrZb+3istjik-e)eiAs6*@_JxQ{C&u5Cd`elH3M@C z{zr8cl|&b~g|nOdmDg85qu*3$$YdoGrgN#Z#-<|Qih!iOrQ%0mqlp~6mE`%-O0735=0`hA_)P3ih6{<=-Iu1 zLwyP(0p9F$zGN5^H4;)^0bm6ewa*IkY)OEziF0GLBpXjpK>Mn?TS8xbW{M}m!L^qT z0_XB^Dd(N~j7Rov9==szRl6|6a|_%=NLc6KQkG@& z9QHIOKbW+@g|rXwm0%*Gxk`io1Wh{dA4dj%gh;Iln%em2P1PO+acu~)5{LHg3v2Jw zydw*z1%RcT%1eiqX=+|P#4qF7H)Z{LX54o1OmY!BqA<$FOLI!4pem9lUwUc+QDQXY zXz8i_Nm=&%!rBW9%dZ^KVLthoUOk8jexEs z@(NMn4f*_ix_Dcnt-gF;G?$nw8j$43;vA8QtII4_3SuOM1U{*K`BQ%PVjM=V9EjWN z`2Ac<$maSpQwA(N&v-69JIf1}FeEQap>PuQeG#gAJKPE&bbvjN$-!G906|8ufSDbT zzq&TQW*$}zbo(yMwbN>;$5-Nwi{{EDGPwc#(!?R-L?z>i*E0Wsf6A12OvY08&`dT< zO^MvmZv>mD#Mk(EcItMTp%o^o+2x=KkCNa=sDuH#+@}lUgNva4@3B~pU`e))Jwr5f zNG%dVQN`S~N6E1dKRPuHHSmk$!qA}|ZFot1a*o)+VlrAfbUu-iz#G8YV(O2?V^Thm zJ{!(5yFFAzWLNPK?88$aCZR2yBemI4k;J}x4Sz8Q3k((*EHGGLu)tt}!2;it1%C9S zAAw=ePO}d%44{J6@+m~WBDlajKrYS^Ayv&j#K0)=*2UbXk|;Qf%)wDP7@tV>kR|IB zp1aQ3^uJZX0jf`fB0&y!H9T3q0zrdR(gVh)Jkb7vhdGdZz6y>UM3UHKqJ#`=Qq*(xmJVUSO=u1Zf zyYl!LjCiwZ6G{d5It%i}dt&wY;BH-6y%+tpdiV}-TKLxL$)9W<*{fIyQOUCXQ$JZg z{fOc$YtPNoyr@yK`Qq&A%ZtSL(Uqw7`0g`_r~2?sAU}av^8KPDzZ0NQa&D#XQ%Xe>GUHTk3Ya$gkm5*(bua=`Sj#wB_F;dumATYE3RZ@e4IayolzBxPuqbp;;Hku1Y$%smw|9Bz_QcFHVJ)EVZFBcUfnG0*`7#1Zr%z7lkXuTTUU zpPe;bpv^OTqho!awK~8_(4z)o7X|ll?&%0Z`YL_N4h@yplbh~{tge_4i*?JFj!!) zz+izNvIX!FYrn7-DiOpb+3Dm}cn6FG`0-glUyl14q=zOb_b^s9Ab>G`jdF-M?vn1o zCx|E&aKNF!doW(Itip=o_Iov3M2()&jYf}Pl}n&3DAB8GAdMnlN)xuPe0vIoW zH?k_#0yvZiI;LXvq1#uUpV@r%uy7+0O)F|6z9$Lrsf|HY} z>{MFLA~uYg^J9?L|smAj7!^_W1agp#-pa2~=g^v(!$csfQK{p}K6=FhKPKPd$?}mCM z@0^x6E28+953Rm<07|Fd-=LH~H=c4QPo#W2t+xkUKXw1g$p>oxCj!HeAc@E}AAca# zWI=IL%^OO~UU^j_r@v}NO5G&md2PNM+0Not@!HKG62v-|lbHlLpj#e3xd zaY~EsqjQm<`0Xbn>mXoJFr`S7gu{wChGl&As}K_$Bw4vW;XO$@FUt3-CNKQVGj9Cy z2~0%cNbGFwR0g)WkothhNwu&`F6E8q!il(VxS4uxWb({}VgS-TVN6Kv6M|I^HGtU> zF9m^Z9uaqdJa1!_DNE%MDYN!p$-80Rh8GtJyX0nFG- zMQYqGq;)H2mw3g{Sk8;$L~*@U0e8LOBl#f^p-hw}rP2cHv!_!tk^V$L;`qp~?^^W1 zQ}pqU#j?+H;j92EF^7V((45dE8MkVp`q~V|s3Z`M1^7fNvTJ=hfrZY7R!k?M76Rgj z5Rr(Q>;xur3Ec9V5@0D7Yk7KSPR&FiWvBLcaze%FIlE&C@t@oAL;fQedhY*Cd;vTA*J-&OEi5THmcYuD;N2%f3n z9HRTR{b5Ybiqgrwin**xjrG(x(7yEg2@(=J7W~x^K^y{57j{H}a_W94GyrdYJ2QE4Tix%JTeug4W~GB)gq^F?lC-ip@gti60h7!~Dc<}*BVCM=xVfO zG!?n?rbl5~2!&=Cbe=`9mck$MX@}ih8)rv_lL?VZxKyRy3IRc`%XQ+Wz2lMTo9vsJ zeg*jjaZ~an61LB#Jfb6Lhzweu+E zu{$hEWP1U&&?o*=q9;=&Hu(q2#s@Dm7E2b(YW%nqoDdhoVz zeSdEN$({M^Y62*u6-{ip~|F zeC!?=9#-UzL3~y@fT%DI?_GQBPP8jYiMar{QRrwicJ*2Ueax5E}K`ahm%0Yf5Rtbt0h4 zrV3=Jhcv}s{5DCqNKWWtK6zX2<>lYJAOLKIivQeLYE#bcxR5F^w_aM@LyY5HsNc6j z(KEZ9E<0vFw8uM*uc}yZ&qkk!D*I=ewF6@Rl}M!*#{<%A(Y5B4vmq&>yrwgGG)TMbO0^YxgA@-^u#Yslv{)ngy_SBEW&HrqcbUMQn`O~TtrqSUI5pz5`LDR zBu=@tFh6t5NOF}l#dEuI3xluh+1Q6}4aoy$-j)(1j>y{YfUfW(bY}WmoFYdm8?di@ zc^s7JjG^{fM8l2-Ha-+W61T%FI1OzfBXg-sg4CGp_=svL`JIyF$h9@IL!g&nE#%rI zct;XnSLya7{`FH;6_I+yk@=n9`SaM}Ti-c2d>t$>SYWWgV1dB`g9Qc){1X<4p1p_w zNS0N7NUy#I9l>UW{~e5ocrrSXz^d^p`3}<3>MWen5R$UjnpbYYJwtL4p8X0L_+ z0X&yyxqe|+sKL>@f>b5%;)Pk*VPM7}X>f|lRk0GpftMup9C=C3?h_w{lBiD7+V=QC zP!hFB-aJK4z9P-kONYuQGNWarBBvg@bn<6wCmtY#xhOVFwlPV)s)R&b2~rBsP1_FL zcK+aA&mKw3fn8$Qa1dv`R#zo8L0)q^?_S1%_q5;7hy73?MDy2wRj)KujLJ4dE>`K zI5@l@6Ul`MH(E^5Kxa)zZ?GjlQY)Aqct>W=xU6moFLcC~YnwrDsZKabathBqc#8>n zUWR9$J8@PeHnF2Yrg{-}ehoWu*PIY&atMRTF; zTk%@5Yz>LF{)s=c!zF_S1`7-p7%VVYV6eddV1eZOoeby-Fsm(YiXD{Q}C~Hda?s@SM)B^{xs;{tn zQxTs6C6?dZMON>-Q&9~LS$=|mh8qLt7h0*WNLswgbp5IPaHNa8DzefQ-< za!e7JtG7bpp~m78J&1s-V-F%D%BH0H$${HMi*d&!-f7|Hm4&b&*~3!3aeim=Dy!;f z=druv6*{=fB_(ya1?kYA+GIH(}t`_ZADdkVzFQ6-m z63!a&W=~J7zj#0@;}EN7r|I9hd(?uFyjS^>jgOU>|7DT`e_qLoc}4_xRc5d#`k#0> zbz8WW6uYn7%PYt3jvTvX+5(o)0VsWOZzjfAqO%+cXDsDTqUJ;;RbU}%;9w>R7u7x? zg_R_sq5*+9-K|i4o;23cjVS4d@0|t5~e6>bAu_pTE93%L(7Oq&>3uQIzFZZVu=cyrT-!i ziKC?Meq-X+{OAZnl4m$#cEMUs<-to-;wKz>BJr;za`j2fj+Oik5&7zG|HEIB;nZM( z!2*K?1`7-p7%VVY;GeSqJ_37lf51p_WDpD2!Y>a6+=4qmZoraLa2IeMtQBaNXcN=} zQVJBbmJ5lErJ|xXdT%V-m3&E@Tp4cr#=ha($^NID>lA{=l;phn;Idn6(2r6VSiFup6 zK5T+dWa0+aPN-GQoV;%DZe~a3$=J4_`g-+_Wc~;xDnGl&)MpOP?g8E{w zVgh}2)&$l=yFZK^=`d$R9%}Ct`r}k@Qb}hd%9IfGSHTgNCv>VRvm2)@a3LwACG)$< zMv#v9N)F!Eaa`Oe8k>qsvp3@)s^sv+FiQ(Y)K9UKr<=JE-4m>p$zL@PV*!f^vo|^G zpbpJsFMajU?Y<_0t6U@)iU=-q`?7!JpZg;-Ts2r=u)tt}!2*K?1`7-p_#Z6L{t-xN zY2Q^@BP|gz5NIc;%u;X_Q^?o$Q;0$-_5rAC@1Qo|aY9xK{@JlPn6ny_*^cK(2fA2x z&SLMx4g4?KYhcA2M$!Q0?>q8z*gDs)yC-(5D%|Bp<@o5OY+pMS7^vAC1Z0H_E$3#a zygW5d`_N++)uFyl`Bf=Q9tUw){j{*p$S~_}{0`Az*vJ#4?9z<2vPu?qzYw7IR zvTRq+&&JE}+;oy?!TC=-ymsP2@)N~PDAp@8*e-nn)jr!n11O?Ymmb?2sOi*!+epQe zHWt*Bg%}W>S83v=cv9w)!O6Y`*~bFZ$eX!I=o&za2mrJVca7ZAdMq}#!-XgVU&%$9 z{iD&CbQ0su$A2b*D?)g2Xiq(w)M2VUs%-d6g-cReoq&DqH!q9Sl73kb+A>ed(-V=> zKRLE^@`3Y*_9Uq6$X$kHkV2SxZ5KN9D)()JpOGVTiFY8E6i+c*nvKRm z=BmjXGjB^|lzGz5GDM@OiX)*ULJ-k9t~x2X_9yPkOp7C<-!J!H?PTtmnH&T~a6*Cg zGrL3ZmE#fAb72(yF(sP68#$s8_e}?1A@W>!YH^Oo+ZY-W>U6DnWs;0223j$(E#X@e zW;ARkHlP)AulnP7%iS7;*mo#`c~zUE?lCHVLL%k z7zD%%oxUFFaL_!-bLoWK$@Hi&EeD&J_-Yn~`$$mCd#F0k{C2@?KL7D=$PlvikJcFW z2MY`q7%VVYV6ebofq&5g_(+gRMa2VM@HWr_YzKffek%zHm=qKzMytIi{mEy+l`wV% zHsNeAp}bfEmMZ!S00Ra4xNxArLJ?Vl-YewBK5PcgN$I0a2#Ye-7)LtHDz^rKd|RYr z>Ct#(AiCU9ZMg9(fln=$7eX9h!X`&hS@-M_cr-#rD4i#6@YQD)w;@vCZ6T#YNWvky z>UMOm#LtwsrB=MR8J@5G2<~D>KZa^w1MBeY78QeVve~SA2R$N3a<@v#R9S+*o!$ZG z<1E||{1Iwufa0sC9;5-`cOcMMKYFLMx6)pvuFA715h59{&xlPV6D}Qk^SdxX z1~e+m-NX(V9=IsHpH%tio$ews2l$j^3I5b3HNF@3avwM-k|!DjJ_3+<;yPV%xL9(J z>H#i|`sUgYnVF;=OW@hm_C}}e#fajDxsKkobmWfZqxU9+?C}SPap>Yx@bT$=DSh(N zg8aan@0_7+Cm6v*QvCg;MF+Wo#Gb`Da{ONDk#$8`Lv9mt(aP)jU6D6kW@b9#^<{p_Xl*i- zmn6;(DkqiJtG)T(M|=$fGSw01;^RpxSH8c@DCh%C%Fn+wm#a7Pkl{vdZ^VB*h&aF~RT`MC@{A ztwG8DFaF6LE*~r~SYWWgV1dB`g9W~00el213lb}Dg?&IMoI-|M>!|P>)LJwFiUR_{ zIcREm{0hiQ8|(nS1;wdufiW$EJ+L@IwwyKif-2kX2XQdbHI@Gggw6tZ$y;1yBtoqaA_pTMC~8wKl0e6dvr>e) zs>y|~j(hOcv51!q-sUVw)?h{Nr}k1DiHl(>(~>cn#i{nB=r&oU3agzxM{Q7TSedkZd@O7}jV1dB` zg9Qc)3>FwH@NcmIJ`x-_c17SQ8UpEAo!*j%2lWA~Kq|zGmB^9?-ka9f>^I!J;kQ03gI!+=yGDj|5bg*d&_Y(go*iEn+*4WsH~wlrFjM z)J=^!ooaAsr|DAnmd2%A2CfYFn*!bdp=J=H{yW+S6tsD+{>sk+2eOP#Gz;1+=00x!wLHM|=Lh_UGbcSs(sDsQ0;XH8rQ zl_%LJU>N}gu}qV!B5NMs>V1#XP(?#|Yd#$heXDhQPn1P|>9*)5j@{SV6N9IM7r{jZ zZ$n9hlL_3~IQy`YCCV%&_Uzmgg*&B*dMm2NE5&FNt0v-WQ#jc|E7_ypQLO{UhP{mh zdN)EZ6r^XyB?6yBI_14^c^DXA?=hQoW}Na|X|Hz7*cJ9& z%OfUI1xK29u%wo2>WNg*_VDFOy2UR^t{!SEz-%U2wl>rjNqLd$<=0B^UXc{XX^W5} z>_Z42FG+&6FqD}c`4kB@b0abPSAYGt{K*+Q87weZV6ebofx!ZU1qKWJUlzbe)KYIC zX92d?jeX5}J3mMzI|XKJfoI$hj#nXUzz-Vl2C!Lgz*sLhI}Ts@+75{BHdwvjAN;1h z=oesb379M_7ob?_RW)vZgdM|xrrgoBP?H8Qo@)&istkOkd zK0OyI$LWyv%vDYk_sS!qEiPBCkvo7CUHu3dTo0xk_8NLp_2T8=T_$or9F@ez!Cg3r z_Fep4+AbcD0LZE)Sy(!Cy$w&}ct%7G2{9T^teuf);7u7%kBR|}*}hM2B=#I1HjmwnTM0ZPCs{uJ z0GWv##hYgyT7Ud!7wsRdLXC2=aTX{~NriFn=42k5Oi)?Cb~8}U3-qewKN-N}jo+xl z4lox@VDm!hnVGx^tZMUIKt2M+=JBM|Q?s_?-XpUxh5ZYvcsNq~>A!MN_vHzi^4|Ad zWe#8>dO%3ITYx0Z^8K{8dyi~OQ_MpAYs%q2U7JXHYE@yrCimGYHHhG1uHb|;=<2P5vk z8UrKwzy5j-Uj_>d78oorSYWWgV1a*!1@IAg490`3l7?65w5o88M}TlSwLh+t#$Z0P zvxVoI0t6u!4lJO!9lwCI8$;>}NXu2gu)urq?L{jbu^bI?xlkv)F0BpJ2dDL2j3oCh zmr8RS6$yJ(iLU3ksa0srtn>G^L4#<{WavzDkiNB+j`twDL&pPF$F6`Ile-v+;0CG( z(F}eI90f$juYgfq;wjRG2VsB7oVSeN0n;A3Yn$v(?xV;VdR2e?swfUD@{9bRO{k2( zr$opIzezr;2sA{(WVGO767%9LN!4Z3o;W?xGIfb-n>LOf#Cx&eUs-Um9>guu5_rC( z{Qxj)3seKqGwi~>Mi~CMu-j(8P3mlqJfGA<^)*)&>&$~!bW_5F1W+Z1SVDJb^Hq41 zD2ygu)LHx;>aD9|a_Dy9SR6eUj^4NY*u5zke>`l3O*T=Og)i{yig?-c{XJMDPFl@JS9oRRET#kLjVq@9TxKd_m z6E<237xNbWx1VWauA&&GS*QH10wp2K-eYCG4pXSiT4tuMFcDIbs6|-7bnj|zwGqR8Tu4JSbzMbg-JmdH2nanp#<8U?@r!e$xkACP*C$f|o+( zqlM^F#5mq`3Ha2&2!q(SFfD(9KD&W6b1Tn%>p;z7Nr0m9!0;K^M07O$EN&DhP9y4) z00hv|Zd5`f*iu&vXsF0nR2~PE0@=GZzgtNW@Xy!8|I3MFjUBd}t$Gs}LFF+H6c_Nr zvk*>{fH6DpwwK8Mo+9tAl>4DV1PjHOdZkb%xLS0dcO@qNs;3a z`~GV>eSxz!HstKcm@`}AQ1i$Pw^DlkZHr`g5uRq+-FK6$}LcoCoO67xgFhi6wSpinKovE zBg`m4for`lBVif7k_1ytRuV0g#E(VF0>8w=dqrxQOP<|CGMz9EDv}t}Pf7HkylRNN zWWNX|36FzLRgPbyP3#t$Grv9WLhj$gd+_4^dog6z8vb((78oorSYWWgV1dB`|8@)D zBQO$d2F!H4O0N|=2&Vyr;7!m}!9w7+m(XE0$AhKm(ADe^VU@yj0kJOB9@MnViy8zc zTfZ$>+la9nINoSk$+^amu7Zm~B3v%zR|$>b!{#8Ci*=4o1g#}rYgcs8e@kjKE+ulHJM5S7Y=xY)G#)4gfYT3oGPR4aH=+!8vEq#bT7oXuLzc3WeQFXpNJB@KWwWzs`1;nf?P)CvS99=1M%$p<4~i?HJS9oT?mX4tm!K zIV)?YPj8>1L~+SHY|@co3jG$mbVmaFc~wxJ1GnM@L50#XLv&399$cZ_rzQ7gY^h`gC-R#W$xh%>@&aHgmgTPKWD2<38O*X%+*t| zUsDq$SJ{lv0DRhF6EjfZj|vQPR{N0}#qWjor)N=#)*VM|HK6;5V%;-xWS}9HHQM}< z->j7Ek^DH`Bqg^H-(~U(qC=^s)e=qGhj>OR4HrIwjQFYt$`Mxe7Sv}qf4pn^b~2sH zMIt$iW4II=9KqKL$I5sr{g<1bAN#Qjb(>qmrL&dgxVa1+jU{@=i=?>3 z#y)wdg0C2gd|4XgURxt?8@3}Rcr|6gN1%7~;8j*qSFSRWzf%d?2jMyy#@{Q^GXkPy zgl&0eF+qjkb5q7F01q(R=}|fBcEP#=hf20%3>l}Y^up4(P8N3eRj{#VVP^-8#Bm)4 z>a)qdE4{?gla~9yF!ZJwP#SV4BuSJJk-NHz7Ty`Wq}J||eUxf0DhA<0pfc5o3{eV1 zg4tnM5)>!TRXIMUM)b$6-M1gA=N_kq`R0h>dBAV?BOufwMOdaX8Q8>GJML&Q2+!-Y z-l@sp7N(-l?$aZ0)?)(O?KVPe`;Kj{jj2qbSd6a8fVCnS0({&4j#J=JmWq^n42X3Kgxq2mSbll;m2xhqF72fM8x_QjvZ|09ly1 zZ1{)<7zgUXMUuwoMx{0I3gW_$#nvLTFhF-G7yZ-(da%fq}T=r4z6Gy6qncOr1*jR=fZWy+-Dsm&zD; zgYoolmEKhijJ3b8@5hbwKv3Y{ts%_dqCnp)?;{jh5H3iW7?lc=5j_RFS`W|h9;DiH z!QlznS|e&GJ_WdDy&dGP++2+t4M{V3mUJa}2?X^uV}K;Fo49)-G{|^R0Pc(uDHy!L z@BuM}TX<@X*)7<(%u?I|tlhS{Kc5+u4%Iy&$w_=pmg0L^J~);z8MUFZ=hP6))1=1g zJx7vw9yF>*na_SYojb_pz6NziLy=+h**@+F%R5pidK4hXCJd;-rSYPfp**2`I*Gf4 zV#2yTH&(CblX5GAu973lFn)bB-Q^A&A?^oV@pK4UI`OioX%MJNYALV2-@r=3WD&C~$HSUJ3 zS?3l!9lB!J5d$rek&)<`VeMqJoDSGZF=&P=wvr^kE)OHCkV22cAl_X+louo>%I)$V z_l39xv@@LsI)#n z_(VkgDU4VZ+kM4l_(yoYyeaK*5M%^5*^qx=OV9p2{S_Lz9V{?dV6ebofx!ZU1%AjD zz(*|9!#r6WE&y!V8IA?FfIY_ps>`RKnd6NWfi%#l!2C7PX=AhCJz&h*`&Omw^N-dt za;T5JfivMrsF?F2%i0AXp~T+lVFbc$(Gcs^8AFMk zgK5EgAgb;KRR-wF*ED_w2SLN&up6YQE>G>hYN?q9b&CvA=Z-fBzc`h|VnL_)i_Q^& z!pU(}a2qAVQZ9j$y9Z6XdpeG?18OGK_`z0(g`fm%5iD#^#}bKzB)20g(Q{ z&0$ZRK@|cAoU^btmxt;tIIAm1Y(|(>{Sw!BI-?5G=JhCSAEZ6IGwv8iD9UAmDhUH* z%gfX5tx$~akhGzQPX?^?34B{FB-f5^S0W4c!6w?0;cfxpX<+<51NGV8#QE-5OyOIyF~YqGA9U zRShx?W^c+{C}PzH&*N0?WpBZeOqHs)I1-P9_Eu;XiB0YBfY{N}dUJtQbij@=}@o7U+FcDr71jMKP zkP-Dqbcl}mozC3!L;fQ`$G+s_7m2!g^kmGi!9i;>BnL(yq9h})(h|}83@PMCn{29`?={+bGwGk9=aCny-_OB=*p!qJS)y(*toTnubgbwk8lC0SN+;hrsK zMFz?Em0it+eQodbY|OwaNw50~n#w5*MK)kUp=$3{_RAVgxoV(U4DLnq1L6q#uTFGU zIZZT69tPf%nUFr_imgg>w8Rizyz2@{#So;M`#2w$6v8pzbOfk>@317=u84ysX@wfCUG9Ipta+^+DRMfK)ffy7nq z&Mo?Oj=+s!H$@o$HW0n7p(US%TQvUEGJq}P59AKhncJzcTUd>U{Cxtkx@85_vWoS@ZbT5cPqOk}!}2kpU^a0?H6IGY zXPkM8V#oMwMzU|@L>tYzdX{Lqy%7$M#oJlid(R__;yb)2T&#@A!r3Vr&eWbd6lUL# zMfznr$uqWU5Y-avJql)XFOTe?u^Ei3(tl-{Go;KAJ}FG3!tk_2MwmhywlaS2 z!sM(URQx_hV!B4pE|Y+1NybGmB7Rbg7Osu9B*ZH`g?U3aQr09%eWeBV8Cz92R!$MH z^NbLoPbS1mP*xlzMMlt#xJc+tilWAKA`oll2KM3mNN5(9jrclW$xkprnvts29{z%x zptsXE4wS>1(2lDtM^_$*fnqSGzOdN&7C($V&r~F%&oqGIU3=Ko*k^+zO_)zo0M7Bf!#B zsLKNs8<(Om<-5tJOG-F^8mv#oSF*JK$AE6%&M< qQjHU)jekPylo+ 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

P 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%0A;V|ews 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�&*p$sv>@|8HBg^NI?vCG?PtNJva7}%`r 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¬cG$*+l}{aTmym{9ycNVc}l|118R(_E^F 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, - skip_reasons: HashMap, -} - -impl Summary { - fn record_event(&mut self, event: PlaybackTelemetryEvent) { - match event { - PlaybackTelemetryEvent::WarmupComplete { elapsed, .. } => { - self.warmup_ms = elapsed.as_secs_f64() * 1000.0; - } - PlaybackTelemetryEvent::FrameSubmitted { .. } => { - self.submitted_frames += 1; - } - PlaybackTelemetryEvent::FrameSkipped { - skipped, reason, .. - } => { - self.skipped_frames += u64::from(skipped); - *self.skip_reasons.entry(reason).or_insert(0) += u64::from(skipped); - } - PlaybackTelemetryEvent::RendererFrame { output_format, .. } => { - self.rendered_frames += 1; - *self.output_formats.entry(output_format).or_insert(0) += 1; - } - PlaybackTelemetryEvent::RendererPrepared { .. } => {} - PlaybackTelemetryEvent::RendererDropped { .. } => { - self.dropped_by_renderer += 1; - } - PlaybackTelemetryEvent::RendererSendFailed { .. } => { - self.send_failures += 1; - } - PlaybackTelemetryEvent::AudioSegmentsResolved { .. } - | PlaybackTelemetryEvent::AudioPipelineReady { .. } - | PlaybackTelemetryEvent::ClockStarted { .. } => {} - } - } - - fn record_callback_frame(&mut self, bytes: usize) { - self.callback_frames += 1; - self.bytes_from_callback = self.bytes_from_callback.saturating_add(bytes as u64); - } -} - -fn arg_value<'a>(args: &'a [String], flag: &str) -> Option<&'a str> { - args.iter() - .position(|arg| arg == flag) - .and_then(|idx| args.get(idx + 1)) - .map(String::as_str) -} - -fn parse_resolution(args: &[String]) -> XY { - let Some(value) = arg_value(args, "--resolution") else { - return XY::new(1920, 1080); - }; - - match value { - "full" => XY::new(1920, 1080), - "half" => XY::new(1248, 702), - "quarter" => XY::new(480, 270), - custom => { - let Some((width, height)) = custom.split_once('x') else { - return XY::new(1920, 1080); - }; - match (width.parse::(), height.parse::()) { - (Ok(width), Ok(height)) if width > 0 && height > 0 => XY::new(width, height), - _ => XY::new(1920, 1080), - } - } - } -} - -async fn load_recording( - recording_path: &Path, -) -> Result< - ( - RecordingMeta, - Box, - ProjectConfiguration, - Arc, - ), - String, -> { - let recording_meta = RecordingMeta::load_for_project(recording_path) - .map_err(|e| format!("Failed to load recording meta: {e}"))?; - - let RecordingMetaInner::Studio(meta) = &recording_meta.inner else { - return Err("Not a studio recording".to_string()); - }; - let meta = meta.clone(); - let mut project = recording_meta.project_config(); - - if project.timeline.is_none() { - let timeline_segments = match meta.as_ref() { - StudioRecordingMeta::SingleSegment { segment } => { - let display_path = recording_meta.path(&segment.display.path); - let duration = match Video::new(&display_path, 0.0) { - Ok(video) => video.duration, - Err(_) => 5.0, - }; - vec![TimelineSegment { - recording_clip: 0, - start: 0.0, - end: duration, - timescale: 1.0, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }] - } - StudioRecordingMeta::MultipleSegments { inner } => inner - .segments - .iter() - .enumerate() - .filter_map(|(i, segment)| { - let display_path = recording_meta.path(&segment.display.path); - let duration = match Video::new(&display_path, 0.0) { - Ok(video) => video.duration, - Err(_) => 5.0, - }; - (duration > 0.0).then_some(TimelineSegment { - recording_clip: i as u32, - start: 0.0, - end: duration, - timescale: 1.0, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }) - }) - .collect(), - }; - - if !timeline_segments.is_empty() { - project.timeline = Some(TimelineConfiguration { - segments: timeline_segments, - transitions: Vec::new(), - zoom_segments: Vec::new(), - scene_segments: Vec::new(), - style_segments: Vec::new(), - image_segments: Vec::new(), - mask_segments: Vec::new(), - text_segments: Vec::new(), - caption_segments: Vec::new(), - keyboard_segments: Vec::new(), - audio_segments: Vec::new(), - camera3d_segments: Vec::new(), - }); - } - } - - let recordings = Arc::new( - ProjectRecordingsMeta::new(&recording_meta.project_path, meta.as_ref()) - .map_err(|e| format!("Failed to create recordings meta: {e}"))?, - ); - - Ok((recording_meta, meta, project, recordings)) -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::from_default_env() - .add_directive(tracing::Level::INFO.into()), - ) - .init(); - - ffmpeg::init().expect("Failed to initialize FFmpeg"); - - let args: Vec = std::env::args().collect(); - let recording_path = arg_value(&args, "--recording-path") - .map(PathBuf::from) - .expect("Usage: desktop-display-transport-benchmark --recording-path [--fps ] [--frames ] [--resolution full|half|quarter|x] [--startup-delay-ms ]"); - let fps = arg_value(&args, "--fps") - .and_then(|s| s.parse().ok()) - .unwrap_or(60); - let target_frames = arg_value(&args, "--frames") - .and_then(|s| s.parse().ok()) - .unwrap_or(300u64); - let startup_delay_ms = arg_value(&args, "--startup-delay-ms") - .and_then(|s| s.parse().ok()) - .unwrap_or(3000u64); - let resolution_base = parse_resolution(&args); - - let (recording_meta, meta, project, recordings) = match load_recording(&recording_path).await { - Ok(recording) => recording, - Err(e) => { - eprintln!("{e}"); - std::process::exit(1); - } - }; - - let render_constants = match RenderVideoConstants::new( - &recordings.segments, - recording_meta.clone(), - (*meta).clone(), - ) - .await - { - Ok(constants) => Arc::new(constants), - Err(e) => { - eprintln!("Failed to create render constants: {e}"); - std::process::exit(1); - } - }; - - let (frame_watch_tx, frame_watch_rx) = watch::channel(None); - let (ws_port, ws_shutdown_token) = - create_watch_frame_ws(frame_watch_rx, Default::default()).await; - - println!("DISPLAY_WS_URL=ws://127.0.0.1:{ws_port}"); - println!( - "DISPLAY_BENCHMARK_READY fps={fps} frames={target_frames} resolution={}x{} startup_delay_ms={startup_delay_ms}", - resolution_base.x, resolution_base.y - ); - - tokio::time::sleep(Duration::from_millis(startup_delay_ms)).await; - - let layers_rx = start_renderer_layers_creation(&render_constants, &project); - let force_ffmpeg_for_editor = cfg!(target_os = "windows") - || std::env::var_os("CAP_EDITOR_FORCE_FFMPEG_DECODER").is_some(); - let segment_medias = - match cap_editor::create_segments(&recording_meta, meta.as_ref(), force_ffmpeg_for_editor) - .await - { - Ok(segments) => Arc::new(segments), - Err(e) => { - eprintln!("Failed to create segments: {e}"); - std::process::exit(1); - } - }; - let layers_rx = finish_renderer_layers_creation(layers_rx).await; - - let (telemetry, mut telemetry_rx) = PlaybackTelemetry::channel(); - let (frame_tx, mut frame_rx) = mpsc::unbounded_channel::(); - - let frame_cb = Box::new(move |output: EditorFrameOutput, _: FrameLayout| { - let ws_frame = match output { - EditorFrameOutput::Nv12(frame) => { - let ws_format = match frame.format { - GpuOutputFormat::Nv12 => WSFrameFormat::Nv12 { full_range: false }, - GpuOutputFormat::Rgba => WSFrameFormat::Rgba, - }; - let metadata_bytes = match frame.format { - GpuOutputFormat::Nv12 => 28, - GpuOutputFormat::Rgba => 24, - }; - let data = frame.data.into_vec(); - let bytes = data.len() + metadata_bytes; - let ws_frame = WSFrame { - data: Arc::new(data).into(), - width: frame.width, - height: frame.height, - stride: frame.y_stride, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - format: ws_format, - created_at: Instant::now(), - }; - let _ = frame_tx.send(bytes); - ws_frame - } - EditorFrameOutput::Rgba(frame) => { - let bytes = frame.data.len() + 24; - let ws_frame = WSFrame { - data: frame.data.into(), - width: frame.width, - height: frame.height, - stride: frame.padded_bytes_per_row, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - format: WSFrameFormat::Rgba, - created_at: Instant::now(), - }; - let _ = frame_tx.send(bytes); - ws_frame - } - #[cfg(target_os = "macos")] - EditorFrameOutput::Surface(_) => return, - }; - frame_watch_tx - .send(Some(Arc::new(ws_frame.into_packed()))) - .ok(); - }); - - let renderer = match Renderer::spawn_with_telemetry( - render_constants.clone(), - frame_cb, - layers_rx, - Some(telemetry.clone()), - ) { - Ok(renderer) => Arc::new(renderer), - Err(e) => { - eprintln!("Failed to start renderer: {e}"); - std::process::exit(1); - } - }; - - let (_project_tx, project_rx) = watch::channel(project); - let audio_output = Arc::new(cap_editor::AudioOutput::new()); - let playback = Playback { - renderer: renderer.clone(), - render_constants, - start_frame_number: 0, - project: project_rx, - segment_medias, - music: cap_editor::MusicTracks::new(), - audio_output, - telemetry: Some(telemetry), - }; - - let playback_handle = match playback.start(fps, resolution_base).await { - Ok(handle) => handle, - Err(e) => { - eprintln!("Failed to start playback: {e:?}"); - std::process::exit(1); - } - }; - - let start = Instant::now(); - let timeout = Duration::from_secs_f64(target_frames as f64 / fps as f64 + 15.0); - let deadline = tokio::time::sleep(timeout); - tokio::pin!(deadline); - - let mut summary = Summary::default(); - - loop { - if summary.submitted_frames >= target_frames { - break; - } - - tokio::select! { - event = telemetry_rx.recv() => { - if let Some(event) = event { - summary.record_event(event); - } else { - break; - } - } - bytes = frame_rx.recv() => { - if let Some(bytes) = bytes { - summary.record_callback_frame(bytes); - } - } - _ = &mut deadline => { - break; - } - } - } - - let measured_elapsed = start.elapsed().as_secs_f64(); - - playback_handle.stop(); - tokio::time::sleep(Duration::from_millis(250)).await; - - while let Ok(event) = telemetry_rx.try_recv() { - summary.record_event(event); - } - while let Ok(bytes) = frame_rx.try_recv() { - summary.record_callback_frame(bytes); - } - - renderer.stop().await; - ws_shutdown_token.cancel(); - - let rendered_fps = summary.rendered_frames as f64 / measured_elapsed.max(0.001); - let mb_sent = summary.bytes_from_callback as f64 / 1_000_000.0; - let mb_per_sec = mb_sent / measured_elapsed.max(0.001); - - println!( - "DISPLAY_BENCHMARK_RESULT submitted={} rendered={} callback={} renderer_dropped={} skipped={} send_failures={} rendered_fps={rendered_fps:.1} payload_mb={mb_sent:.1} payload_mb_per_sec={mb_per_sec:.1} warmup_ms={:.1}", - summary.submitted_frames, - summary.rendered_frames, - summary.callback_frames, - summary.dropped_by_renderer, - summary.skipped_frames, - summary.send_failures, - summary.warmup_ms - ); - - if !summary.skip_reasons.is_empty() { - println!("DISPLAY_BENCHMARK_SKIP_REASONS={:?}", summary.skip_reasons); - } - if !summary.output_formats.is_empty() { - println!( - "DISPLAY_BENCHMARK_OUTPUT_FORMATS={:?}", - summary.output_formats - ); - } -} diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png deleted file mode 100644 index 2fc5a7a6f95e1102ead3b646070c4d982d012b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5945 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 bS5Nr`oU|lS0-KRH00000NkvXXu0mjfbUuRM diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png deleted file mode 100644 index 4fea3c2440b59e5d9627e6e6146707600075e88c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8353 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 KMNUMnLSTXjs)^+Q diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png deleted file mode 100644 index acc87c247695ac1673f609f84da6e1833b88aafe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18233 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

bKjAw6^5$aelKtzOy8q?WtzcD0+%?F| zg%?jD{k<%0-yp3@NO|{W7-ehhp@S|q*isi@bwoK$C2xi5rI7mmu<-VDN&o+u7L2c0 zUk82-9dxn577Va*Sn`&vN~vDLOil@JTBUhiDkVo38*ISuCA@EIMoO6XZ?^FWR;joEu^(Nb_~}B!$1B<( z9jWfTdOy!t#8XJKlPSv)`43#oEO%T4(t-it?=?0w81}S4IugyH*>IAor;y0$kzE$8 zu;~0a^c>B*>{4BQn%;5&<8BsS_WTuyedcL%v=!5S?%N>E!JkvAmqPM6^%#))o2KVl rvg_^AUg$&RAO01lz}Z)EE^4 z2~X;qsPsYGgn9*}g|;HldVx~84VUBo&EA`{XP9$1gAFMwnJ}EQ_qV>Y&bQZIYmYDM z%>0!Glo?wKGKe@MiSF66r?#o7=~zuo%}z5u;6d&O zIf$8{%?}L?-RkPXA_U8M+{{`;SMkfXx3{;R zJ$v?@k&%%w%S+7haECP|El_ zF){J?@#Du|>hA9TsW8%>t66kIWP5vidmx)tmdRw&5~1~+$~;Xb(V~KVfUoFhS1#w2 z&kUsN>+25^Y0E875>}T=2Jbm!Gm(xZbbR}+$j`S#ej5=PRo^FP^!3ULksX^wo~#x* zq*lQ8&H4eC<}ZIx51;sk1CnfRZhq5UGoBk=7Fd6O|23o8pjc!V)N`f`(JR+QIxdU+ za8u;&lp_l}%SZxZn>UIa-X(JKIgz7#mtz%18W=QPS68>6g=jhYE@83(WYMhEQ!%om z<09{WA#$nDXfGv_R-=7&FXQX~bJ|zR%PRHHt9esdk#BqT=hy*}w_jD!-x5jMGaNE- zQiMOtBW%s2uHq}$0Cy>e=Q~91sn@e-!5uIBwi&0rd zX?Sa;$Ub$yalgpGZISQ#)%{65_S~nM^lJ^Vr2RO8!Ag;Rs6t2X=^Bw2pHfj)$$RM}!l?EGjiyYal$1OU$nkV^Dwozj&b8Pki3rw)_L7L2AEv|yF_1N)! zC=V7GuXI}J^+2`W5CSLHdJ!<(3(Qu{9Oi-n7MM1Flha+_i3tZLBJ5zKAMz@xi)?@< z(d@IkLhvLzv*!?DE*M~e2{u0Ph0ojr1|<--Pt119qQc0tWPz-Do%?r$GAO0a^c;DB z0T!5GWB9_S%OSZWMArsPWCvqYm^Ti{26DWuIDFl_Dy8z1<>XUU7s@}Q96J3AM- zl1+i!NlJ3Zl|&CV@PYfM{z;MZ1q zQE$(eb!L%SBKO)VbU19@r2ZxG7p*&2A&c2LjcM~bz&W2%VwSLDDZcq`tFClP*9N2` zcid{^FgdHSzyv#M0)fvs?ARw;`26SRC+^jo}kheA^nJZ$u|~&kJ|3AOgnjh>T?p zb8Srmri~{3p6k0L!ewUXIl52xV3dSole+k}d9(Y5$kiL6JlqP+t=y~(7|R@+*TDi4 zY<$q1F2)}NB+J7zj1(rkHaZ1fWv+l%M0)-Z85!5L0o!e5d49^9EK24`hY@1IxNVFo`{A}?ypWPA}5ZRU5{6bE7ggNqzn&n2ChS5Dpxig_Z zaR({(f2?WcgOo^4|L>@3K(|otQI*?S6g{eH)S0Ict4KC1dQ`PU$R2C9w6uJ5`t<2D zi4FtJ?2--xCH64TNJ;x{ZEgKn9R{9pk6hQ?aqGG}&`uTRJSGhf5075Fc=5y9+S+RM zl=bRTx-7<0sj@QPd{PA;_xehPX4~4@J~?sX#OrD;4Z@}HcaEaZ{2>1*v+jQOS}{`V e=7+@x)&2u(o98x&pvmt50000qKv4>XjPmtP@yvxXpu^_Nuf5aeW{Tqn#dEz+NLjkXqxn?Nz>QH z5F;;YqKS3HHnnXkJgC)zA53K$EDVl-f#E}7_=fqq_jLcx-fPajm-)Eo%)Nl^?BsZz zd(YYHzy4>fwbx#IZ}KwT#%=j&3Z}`qyyWCcn?z9*e_rWkaDXl1GxAy{9n(G#hGAfY z2P39kaV9FY0MiUV#X_M_tO+?m04KS?2q^G(V`F2!t*x!~Hq+|s>zgvZoSHN&cxY(o z+yT>idU|>X1_nmliwz;kw2Hu1M2;aoG=>tBa__zOwy$5m{)yGAS3lU&(sJij!^k+T zudi?4mMvTU{;X;5z4u;sxjs`O2b08w=FFLM?K96j^Yp_HKm54=yEE0R$80>^2m$^V zue|cg3r{`u)X$C|KYmvA%1n-Sux#0~#hW*8eq;9R+4C?-a}@&Du0m2uTG2HFH6%3j z_xB%Kvu4eAySln|PDW5}GFMu5(y4l^p&CC}v?Q8ERKLO1kjYHGTE{`~p#Iy*c6r20;3 zIRr6+?1~jD+P7`nwgX0CHY+KZiAJUigCgCZiEQ61vip$8k&_~4FB)?oxhHZi znGceFxu4&)-$z%8Jhp0TR@Bkav8cPdd$;O8rd#REktsne%=ln=A~hK?1k>Ma68WH4 zWY$cP>t-ead3eO$$D7ARZoXdRh9;3Fqhv$g5&^V9bKg0me7|w2Bd0`;ow1fj|8;r$ zO6eDZH@qS8;in?M{*lO>Yts>=`k^o6l+T>Y$%#LiEBTgRDKWeIM1J+MA;>6m13(M* z?UO}t&s;;sq85=mZnVmUp_Ned@*Q=h*hO--4@R~9$3%AZS+Zi1LThQr`DmZW`i&yL z{jtcxmQ(~qzP{yEg%WZoJGjr(u0+uMBaZ_&VAg^b39K!It8JBG$2~irL>4*Nb$bcMmG2{<8%K{hp z$k2VhuR2^*?uD{W0&0bxJ?>mel3vNj)C2I`eIkhDEqhI=07eg-c z!J(ra_?WtYH^^W;smM_i|4WyPCqd%Vk;tf?g_L9KI>7=&Ep zgF|CXY*mAH((B`|IyqXPLfE@Kb`lx6DFK=9T@GmIvfAsfG3I}#r&TqZ>ks!r4)Q|x zLcjqYaDfk;R0DMS5!Hko_co}>`QTwlDbNwT)KA((;;GrB+bj5t|Qj!IKnwM4?Iqvhs zSzGgs*>@vQbCdgj=VsS5+9|AxWS)>om7^?aHx5VmN2Q{MIO+`DXnZ}Z^+db z!iQECl{|2RKLt1GlE9H-sYUQ`5R^*0QVYZ-a-*as?OWJL$_Z#HRdgPuO^e}va4BY- z0Z+A+$aO{dlCRu+tLo&a?&J<(E-_UthUYv01H)H_9G!3~UU2E^26(}(3xbsn6qRx5 zdYgSsk_qeRL_sRZB^m3v!Gyfa(Ny_!gJn1dlF>^uXYi+CCza(;>8AiyaIORpDsy6BF?rc{o5F7N4o=S>cIX7fd z+Ps|XAP>2tn+*zf2`6xMXYjI4cHPWUeX2r^NHURIIL`{B5#U%NA@j_;+AeR8nr|8q zQ*kL8@z9Yh*pzFLIFQ z%Lk8Nq5vAegAuaRIPse= z{0&}k`+8K}a)U*aHW6 zzy&^VvTemLVN{(Q-^U+XYK3Xy#am)2gxy%02LZJAsK}nf-ttQoIMFnLetX>u(L4tE z;83e6x|hj*9v^>A$O&BH$Ht)t?zYu^zM~{S0DQlkmiJZBR6uU6z zE!}n@ZHDJ|WT&4P7ufvoWEAsPBgR_sRDIMHc#h{kIh@q5=^r`BL+Qt!C_~(q&m)4rbK&(74?nwwlE3p#2WzLbvx%-zxWE8G7kx6YnK-3i2G!(+~a8 zZ(Ufr9Ly&;Qf_@4SO4-ofsy-6O%SV(?1#}kfx7zwrEsIaeN^2&f%L0$DXb4y%CIL; ze0^2_G09QpILM{XrL~}q>q~c&=5tm7uFp@~r5yq(>q?N-Ji%^Kg7DF@-J}h5iR+OS zpE_%UZbpJ8II>T8!$a`j(%6sQC_d_VK(~gB|Q-=<8~{gZ+|IGaS$|W;U{9 z$&$8?j*jJ==}be8hAZA8ab?vab1k%_Fnsr$>*8?f=@;EK8h8gibbld3Gr1B+#%?ls z(f95V`NcyvkD4_j8O74hn^dpYUVH83O`A5oj^6(6^-C;AO^u!1oaNlQb!*K|&9LTv zxPntNR2~=B@YIaavxBprW1X7GjpK_U%yxEmKF^uXsh-f&iD-E|ARe(q%V-91%!l&s z6b;32YQ9?99DU=((lc9Gn(D)u&$VmUu2X%-jMnANKdHB8&z{|!?fhRmsmEE*wzjqj zPwEBZnH*;yd62`RoQb4E7@rC-Ey^n~I!DscNiR(hH8kpIHdSK4j~?iQUhb4|INmw) zU;>5<1St-Qu2`{RIZR`Ubi|3;^5B(;EH4;`-lf608U=cAI=m71IorwMSMPLiVWO8g zVe`+GZ`iQm=|>)Ue+8wow=@0k=lnmn>3^1Lnw)Iyzcp1rO<(`)m;e9(07*qo IM6N<$f~pWh@&Et; diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png deleted file mode 100644 index a2aff6fdef4f0187375327bbab78a566b430bf28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4719 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 XbDkI)-poR-00000NkvXXu0mjfh*SEh diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index be9ba4d006c382b51d55025884810d7cdbd5302d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10036 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 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index ba19ac5104d08a7c6216c5030801ead545518c60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2093 zcmV+|2-5e7P)URA5m6nVOnP*+6Dg%6X=wCorB){NCBw*}iMnuHEhJ?VFpMo0lpH{|=ByPfSc)92^`x zx>sdjVBlDxT@^`K!$cwxJ8T_z{%(jUvkzi9FLH z^1>RCw&mqJoSXBm7_KPBKJ@48=gz>EqWjT8&GLQ&6zzgRBWprd@WO(Jul}`|l zg>JLhwpjn}jL4qDZsZ%(u%z#dffzAwMCeoad`4%T>6G)#?l(nVd8!!ymV68B85 zqqViQjq+=DdqIqOl;KsYR<#Td505CBrrgVdL^dOky}cqmM@1688`&E(?z?5N$O`=| zo-fkWkRRpAX_3pJc50}gnQF&zeL<+O9i7BBJhma9U(^!u1Cifk-Y4rDTp2*%)a7Xh}>6^%{X2A{!r90_&Zj zgiA`>&DRiGy+mY*dTE|kBtPcJT~}h4u8LfsYH8bqK7UlnY=2dW42hihQ~NA*_=F>& zm5ETbB9XB2M-r0*gb*Hpz_#Z!L1*3Q6G9txWS?H<61+&%z%@%V{+381raKHl>eDt6XyQi7Q{ z8O#TiQ4*CTezqm=fmn_EJtIPYaKHl>{KjD7L@)Zo_S8he)`+Zyn2>O3OE8Je=-=}i zToRi>^@#NYhpGlH_~=0&deI-YrzR2>zr>~`H>D5Pr!*0lEy^eH?T97&V~ZRv_~@~u zMt|6znn>6vu^NpBlNO_7Ze<>rOEEm~(PL{Y^jGtCMNd#Qn0>tfuqo*#AZB7FvbhxS zzy;sdTn^c?FmtjaGXkuS%;ndo=9UC_7OLnmBIr-}byg=!V78RD zpF2(S`M^7dgIP{0@so4_q$q1p4EoCOCUhMGthw>h=_$~Lb zQKdco=${By!Qg^F6`X>gH*7~;By1E=%CQPFCnLt5m@Q>8o+r{b>=7L+yMcHH14-fR7ba-r# z2&s49h`u$AL?Y*|0Eo>y<&RSdHuV_&_hj*D$C>l)Q|`2bWjy3C*iJhja7q8(wjqE1 z6pOgDDJ0=ZiBl3!^5;MtryzFjL^(V(=yqgw?t}-To;&e3q%H;o48PpDQz;vbMAmi8 zrnunHUz4Tg4XY|BV$E+SfIRzNPeX=A3r+x&ZqZoj1Q5(ClP-iRodB9(A9jZAejv_b zV*$G?mwde7ipe@jnVCE*`DEX(XXqbo*M0H|7aDBxZTXxEj&xG9`DMMmr#=1XZ%G_J zX)Hy8Z|uWP?6r%QqWcN%==%Elj@_&G6KdaP?(o9*6SwfT7d<^apK@2n4NZeewdzpL z2)tnL5`Of4g<^Jach}wB{eGB05s3%l&MucEDm0`6%%j}dnH1j1h3D#0?<-08&JH__ z0M85Wv#Z`?bBD(j*xkCv-nVbxp5S^b`aO0ywC?NwC@4&JxyS#vBl70*zWy(p`$7H# XbDkI)-poR-00000NkvXXu0mjfh*SEh diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17fbfc0c89839b974990a13962b73434feb1650c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2117 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 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index bd570f6311002cf0c18ce016cabfe14fea4a2688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 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 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index f543e05bf6e4cfbf2ba0883fbfa2ca9b6f531ec6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21074 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 F#cH>-B_Z{AgQ^d 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 H%o*`NkIa?r diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 885d4f2e4fb5204f28744c0b35f1be8421e12f4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8619 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 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 7479093a104a801d9de3a71bdd077cd793225a7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11836 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 M$f!zJOPU7#F9R*(Q2+n{ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index d06e450592b3b7aa94fa4d02fe3859cdf2909afd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29013 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 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 7479093a104a801d9de3a71bdd077cd793225a7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11836 zcmZ{qRZtvE)TU=}4-$d}XYioG-Q5Nc8Z=mN4=}g}o59`P-6cS9cY*~7PLROhEZ^?d zUToEW(cL#)-Bn%PZ=ZACCsJKi4jY3M0{{SEE67WOU&r468XC%L5B_D33IMPnDo9Ic zd99vi`8H}TWbJ#3oJp|_BhyH9Nrps+gi}oit%`DJ7~p3S@<46o%xBY8S=z1o)6<^K z@pT)a*~VS;a}CNqAbBBDq3Sc`lUqF{p>8s12fR;syLh{wgKvY-h_(^n8rofA)_+YF z%2=G>0^0Bm&6XR-s{E*SZTHFNr|zDvfSRW@{k;)Qa?QntUZ^fWExQxJ>vET{7s^lr z$QJfSKHnW-fiS2U1BJaWF}6nj|0%%|Wm-|Fus=D4n>7qY7-5$;0yFS*dGVsTLzsUd z&lR}Mu$nItelYT#D@|Ev${@5eZ@@;%>@8%(h?}J#t?(Z$!(TpL-f||*;!IJ8)y5Gh zy-ND8jg1XnpMM8)jb<_6jSwY_u=b_{97+9=- zZ;k{$6i_OTv8BM0J`WQD8c??2wux|hwQS+97rzHnK749C7)8c>XUOmTd%)w#LkXYw z&q$h}=iiMkhlzBa3f*eMyI-1WBGYnF& zvDUq0gKqDOK_Zi?J8t?%@3D%^OPE)`$y~ik_~xA1nb+N3N{-J~AS%VT+wJh2)1xD1 zW=XHh3QS~(0O_=$(9-H^c`}=k&s-u%>G|jF#g272G0HNgjTBRodcfcuI(mm#?NdV5 zkhkn}t@TtkHheSSCGKzAes+$ho7-Cs1gE=?^q0YY&|sYMu&^+DH8r)|iS!V!{h2mJR2n`}o>KBs^k5ht{4WHvg_v6K`5>D}X zMm{~fg7I8K=?r142!$=*&4&$2Te9;ubOQSE`QE9ze=F)~ITyzv_FKUmo~mlMp$(IQ z$5)?#Ul(dN^_y%LP#(=5;6U9VAS(G7d9jMCYr|~s%SF?Y@q>wX&aG&#ZuU_&hc8Ef z*VFAz+{R?lAsv4183@Suo0T+uuud{t!hGIx`xsQ+Gnj60^-qKeh>L5D2 zv*i8y>-L`vk&)X(o>aICVigi%F@XA<@5^~lX-9scg;+0PCs|rja zw$79D?H?kDWk`tm0N(L5N*-G+tLvi$cA&q=m8ae5SFLQ}YpEc@3Z#@Tpkegla=)r_ z7QhU}dt>m%@ls8I?P8_AuJ5^Txj1@uwphUPZ}XqAgmu~z$6D(+OU;J3`aZh}a{gGi z`m@5fA~GNjM2~Ya^z7x93W}!6qc^`t2=1m|;|nQB>~L5cr+f^XwP*rq|B*s{5*U!Vf;NH*ZhGET^g^6|TLjbt#fL z+@#IzRx=#C+e*@(#TRHJ+p8bPUd%DXiNIhf!V%J5jGg)+*YK5nfwOMp+^Y8t&vg5tksE9V)Xh(-E^RYhdV ziV2Wn_a5?Df21L4dj4|$=`QxLGDT6Nv#vg9FGZ-J-_qP%>~Z$1&1O86b56k-daBCG zXO0g+y&a$SBZ1FI6nUI#+U^@)Jw5}AL`4e;w@Sfs9ZdjNl-f{98x;0`c#@+C0_tcJ zU7BtOrZ64IR1EkihEvh+>|@a+Ma?|j)8opF-&=6nf`m@ywro(@(*xWwo6B-^JT^9# z=7#xhJ&IgwWxU?)a5e)d7U}@r{U}WA2+66lip;%6#6DN?g{{^qm2GxA0s`cNv)r($ z#?2-z6s=ThDFm0VezQBN=tNDj6>U7|l`VOu`{u>s7l~6BXXVXjDkx3}E0xU|jOo(A z9QdoSAe8nR0?K!f>zYLxS>ik>!GbFjSH8r8D-F^@E&CSg#UI%Z_mk>17WQ(J^ zzVWU<=YcC)n3}9Qz(S&2R@fo!e4lDDw=g&}H+L`?v@b!soa#SE(cWPY+8P8X6+{s5Jlz8k=r&D!Na=_}8&f|}Kndq;Tdl*xOlunht za%Bvg>PI{xYn1$kE?|w=TExt^_^zn&l&74d99D&3%(bL;t8B3Al<0+?oucYHa~9p| z$GyE2lV|t7ty~sJVbMx&_p2hEej+e%mba~{7Q;dj?{a~H>i@)WPqOaNj>hZ zsqjpLgB5JWr;#p2zn9O5{tz{^L_FjRIB8VZepOCy7dOgKf9lz+`NLd~_R(F-_&%|3 zci3C9`mLW>q=+~DdhfN6#qqq0-6K1-F|l^(L6ZH5B_1O$nn$QTI!Bu~B;okIXc4wq z(%vKl=4-{|cO5SpWou{L=0u{qmpnARiNw*%1gt~gQazLUY(ABWW{0)rufGQ(_ZKAp zG32nzNNp_S4em{mpr)kZ?$;%cXSUM)!J8D$?a>k%Z86T9*WVGHDA*mHF?E2Stsj@D zX8BM|wEUJ0He-S;8_t!eiJ4#hnQ&y8Yb?*jH*Cgl_N4q&G{4znZ+0>;zd!}(sc~LT z(ePszo8H4ooTKub$xnKTW79b*RuY@22uU=pa(z`^%@+HWO9?|Uszr{Xp>U5-$mTnG z5dLFgmrU-O8}Svd!x^jZq!aD~zlr68aX)mM?ro16ed=X+H3f8wM(Dl&xpB&egslb_ zwe8LF;_#%FCZ%&g5W_91!6sj);;pUxl-^nOpqct$l~6Qc{afo4qM=XQ$+D7HuHEcvK9eL=nl8WeWJRNJZ&e4&ISXyU$vD z=Q^R8j&;y*2u?ipqK_YM|K;0a1Y*pkeRI(vrRZ0g^Tva8Fuop*B8ge5?-ElC&GI3m zBoXItH0gYO7tm&nJn&#PQ@VJ!vX}R-#?e%O!;xF+c3~zz@A(H?SAm8^ z4sL`??jh!dV#*ykvR=TAxC91>KnYUive!!xieX3eQi6-Kmbs~{!qGEm3z7UQQaYdg zGI9F);l-l;`D?6o)J>%9Ai`~K2$d5@%GK9kl*+CSB!J z!M1p)^;y|6sz6bZK8Zb53~C?w@d3-@Nww^Q&%#=PRHu>mj^ltfl2_Pfkl0nQ9PB<< zs?5>&#>SWdQZIF5nH2vE9U-}V!q>lG{zuJ`zK`?>=Xd1Jpt7>OQG)5I?9%x|^B7u1 z5dDq!W`1K|?m08QaZ>8Fb*7DCA=5&!az>OkjN3kOaGQnY;Bvo-JzqDq$fTY}E_A!@ zZ|~1MEc&WjB8E(e1<0(XH_iOJ z_kbuuf6sX&A$55JYfgBHGHdv?qU-*3QBu)jQf@I}E-pS`@{IfVJ0j7~jeXBs3T;>_ zKW0h#NolJ1=UPyb1xQi@cxod^b6wQ7N|*j+l!7CH1HIw!fQ6O-qth~In`hElFp1x_ z)c-BjxR=#4;E`78rjGl|iCC&oiEYR8Wg7iapwo}u8W6rr5;k`widGOlp}|u#dKJn; z3~e=vGVVFCF~Jv8ko-a`{(L98+mu+EK2z#lLD1&n6e4WOZtX5{>bUk(G!Mx3n-%mz zq7v=Pz=^_;jlWIw%1~qg?E_h65|!>JbXi`qoZRFx`W6}aGWHpYlUYvAaSAIh$dt0j zo_$fbuv$2G##q)`IU+boXfnsAG?24yw}pgkgl))Y!Jhjjd*i@Exbs~OfJA=8>jQYA z3XqsYH>pF3vi%b(Z7j$U1=PKA=r1j=(f(mDk@Ck{6CZuqxHUN86p++t`F_DnKeho zoWIxt#0($9%Ba$`4|qd6qBZ2iE7JA*mqsB%Lrh>HwSenJ!UQ8|AD3FE5ZUfJq-wOp%LSMSIO@; z8bD6)3IH;cC@^k#x7)xW)B!tG&Q2B>lJmA%12MSQP=(TlJ))O=q1R;k>lh(3E=*di zHqYCNUierG%36SX27~SnV-5z4I)ZgDyT^hfqU)2`7?F^j>Dq4m&A56bUhNt6hD(V3k{X80Im-xc1yhZ44xdj}tE}>zm zQB=aYNEwm9(0oquy3u(J(E>HjdI?z=cqxhr=7k(@%)49)ui;R#*D4|E1w7wDC)jHF z8@eIMoBc?@f0_)-`t9snnn^N=u_aWWi^ON-7cZ6rmQSuMqZuZv7(c(4>3tQ9mB*JH zKGK)rB$wuccoCwWk^u9g!=}0}u9+4F9IAgyYNT?U5e9O&M|H;Lf4Q%Iq69E1jbl9y zx~lY$_p-Y`UbY2}zJq_qQlPV`JbiK>-k4Jcc;e#7n{BFo#hbVaM2?7oUz-$?l#?=T^5hkaTDsBY*M`=PO6dpvNI zq?OUqnO3=vf9)3;e7Ngf!!jofV4PLs?eV<(<(m}m$!Ak;Y%VCg6JY?@^ZmZIu^~_- z>X244)2g8ZLd`56yc=6JQgZV=>Z&B;oOrJ$S*&*^br$WrYizw5Z&8iwcp{ytCxA{Q zr;R|0O~q}IqQKBGC;NN-?WmJ}GI#8l&FJbOf62g|ulajMYHG>%Q!;{rE@uOm=_z!{ z@*#wQ)@NzH)>!M#l3gCCWWpgS>#=`uD~IGHts1iw1VLb6p)(<2Oa*TS$)M$poaneX ze~|79T?$M^8i?PpdFVk8(ub*Ssg^?5#Rf!N|HcH?y+4ke49{HGLS)}Aqq{NO#;Ih3+UZiyQhCPlddbXkcT-O-WZ$^|QUPg1}tf3j$OnYO?m8>rsS;nHHXUyF~ zQy$>z>rZeeW7FpqEBhGE(fEO!-yOGFm2_<1uva;4SGt~2-Yh0R>Vf@m_7`$wI{rqvwRa3S6Ml!1{=ce{Cbsc>7%Ueu9@a#R&@ZiOR)|+QCIH1R$TnLe5S)7USfDawR;czt2uOXC$Qn$fal2&hig;Sm z5%4CBW{s!?Huj4J!W{Zj9k4XbfMq|uDdd^haYr+Q#mp%1QT1(^vBD4zdtp(G>$P>C z+t`?(N75Ge@H>)lOJg_&6*n+koq%nJarW5?H7&IE!Fe}-ph?E54C%uUDFw6VKB!~} z072LqZjkYz?=0>`LcXzYm{@*Rc} zMDTVzd2D8z&!RHLr_T3*;ao4h*s#v47CRY>Jb02{Q37TcHPHq%N=w!H9gse1P^0K$!mpn}VJguy=sJ)gNh zPEp_by68jq(nUJ6RaMU!p&m7TG-aWzhHyJ7bBit;H35_(Ffe3Ua$iznCW9vc&97bh z1Kp+T>cK39vDgD;j#~ZC)ag=g$1B-)Rn2@AEOvbSqu=ycAR+w+w_6r31{?%;=!&5C znn#ak`@3{Zly?H?1>*#rE|!`b^lkhM6EZdfZ*m13i4omP00?G^25SX$p0UrBd6 z#N8H5gVtWW{cbUM5?5G@n^sFiOVr1{*4FYK)4d4LPa6w&<5Bu@!TGiD!Jds}$XfQg zt+d1+8F9+c>K~xjV_^$&ZWv-;UkQ+6O3u>+_*&Xar0gp776#$PL{~9^T4&SRZppIm z_Zzr`S*jbK1e%jK`=m9Ulp?5IE!gYJS&-v{b5;_hf10cEv-5uoAKy+*I2&~I_DziB zScevTW3AaT+NdMP-rT-oJHt!`9D1|kI0d|CT*etLNMA1? zKr3#N@s3hBpLBOgj&;IjGtif^7Ww8<_|JHq5fYOWliSGd{2ef&A~RG(7xX^b&a@>)0~%?LTj5G;91`p95$e;jpgg;MRP>33)v{0p8n69K@y3UCGTM*aJFZzj!#rZ7 zFyabC&Spr`X0jx$$`aA(Hm~1)53!aZ7_M6jt0Y!kVuay5S12KyvSeW@;6O;2Br!9< z-c3u8WzzTw>0@GQJP}<1cOf%VJ!Sx}>0A3Kft^m|DYEAzM7n-Fh0i2y{??@ipv7Mb z7umwtWXKU_v*KI^{0!Eehnk!+A`!x)F~%R$jWp-lwVmj<^znkf6P;4us@(oCCdCj;I{La2VAT>tv(yi zChqoDUgqQ&>ZzPLvJds))&79n>H1aBs^MDW9^*oQL){aD>M^f&;u@bCkR{!} zh$hu1WeJSe^x@A~QTS}H=d0y<_}^J_ee%HN;ddiW<_0k4oSiv4j;Le< zieGxBC2ryh0qB#RDq2B-9P~$$K#NMOZoT!xUei>AN(%QwF5rvUNLVhfc`sCe*L6I*<2)452TC{iw{OueZS*4*_R*cjt$! ztZE8V>e)t~(qak%TF#fsBNw`IduI#O83wuaj5A(v*U)ypY#Hp^yAyndN97E$ zL@w=UX55yT)92^^Gh9@VihP~@gol#K+YxIadv&Uz8e}<^@rI{Hup^(`G*H>_8@C3B zMeNC5SGy)ae)Mf|*0#qaj5X`8ofYMw2N7=1gX4qbr`P&*440Zji>NN3=sRn?!eU)8 zQXbfXVo2PG88V%z@HGc&WE>(X=3D}{pDrbB2zI@I&p=J0{M>`k^zN26_l~# zr=g{2C87AqV44Fxtsbk1D0(Dz+ZAmKn?;|e{cp>)@KH@Mq&OF3co45e1gqg#aRO(3 zc3Ql-ZMYVYE>8w-h0|HAV!XNs360S&t~)j2)f)D^`BrINPWv1&zjJ|_%@N)6Q0L2@ z8*^O~A3El_>vVO4Ui{#OuX;>!?;2Ev-I}{T^G2P1KCIT`Y4Uw@yk{jn=&s&-N%A;WoTTp7~*Tha7HMeMC^6wHutg#BBg7XzIii#xxUqk zF%0J~il6?{uBt*^wjGXop3h11f|lg6)ik#K2bW8xIUk^KxB^2%jz60saI-M|+@Vm} zD(x9$B%fY)yy0wDXWW_H^(URY2tU#XjKKLqEF`?;{#LmEIAGnT`!B+7j7l=KL4D8N zH<$6L-Nx7^z~iMb96B1DeCUJX^MFXuZ8zG^zHPWui?dy|p#$#snGSS1NT{O}{bQ_0 zj6-Mt(A(+ZpgqM?ot7f(PbUkcE93cVUu2bKsYFiWM(gE}K`4t900`aAEMMAGz#u}2 z+wl4s)5SD;QJ1__(bAR$#2x0bw1`yR&o&Y~8fEEqmY2+3Vs{b}lm^#6GO7d)p4?gSF)91;cxy0D$H}^U%=Xaqya>pF~FQnzC^LWWm8%yOXmL3X3Q6)`Gx;5Febc?Q+9~dUM8lupmIIB?%n~eTsQvAy$ZYk zUFkydUyQwHuXp+;Km;YO}UgW_tDX#J7k+Yjc zRH8~QynjU0-hXIu8P~xbdIS3AIrea7rOEN&NwW45w+@y5fZ*krE+o%lXho0Ie`A~<(Hr;^mp|=&Y|^LXrFg0a)8nr1@YZNk?n2$`dTXQ?^QvcrGo*Wm-y512uKL_q=CvpAM!RIc{v{ z3!~rPMG|zoH0VDMW^M^YMp3~*@ZtZGvph~RKf)c)E%MhqPXnjNyjjkw#W+D&bRhK?~btrD|D`;@)lG6Q4GwlKm}B>DG3hU*?z)HsdEgAK%h& zd$Z3FD_u?~2uUCG_^o&(#=4kAen{#Kq{Zm3fB!QtiJu?Y6J{vfqhX!hl5#^_A}JaK zXgGrC?p4Ij|8;uvYYfwIW!-S)i+6nYR=C_bhKy}2*y^k0VAIP3&kg*N`9cH@KR}*> z8*S`BD~q^e&!y=3k4@1~GjUFDQmu)uJy#7=6aOmtIy){^#D{U-UQc&fx5qDYn_(aP z{o=kcBdRfjIVzN+qXunnP@UwR?b}e_8{Pic#bxNG>|Vn2X$~-|NoC<{qcj1995$HL z#Jud;&RqohmWN7>+Wm&f*O@!ckJMcYCR~j=s#x2kOsB(8f$;$wUp~3iKGjz+Y!s(< zM?ogAiPHpr=Gflw&HW?f{w>|JPPFAmNQdP?4NLY2HH-#Zj7y02$7GBQ!4*PKQw39_ z#vl6vcSrp|QlhCw(kJm6)O{K`=~@tu_#WRZ@TPHf-+81jShib9nG8s|u+?VKt}pXj z&<255U>7^o$2!Lw*QM|jPTU1sjHgc&+}EdR zD1n*$o`9pM6|+{@|+T@V_ziScD0f@>_3!a2vgNp=r#5LM-Sv&xZj} zOF|j`!H=z!I?>iY(}r29VFutSYn z^;!*_eqU)-#x_&h*hcjlivn2<5*V;^6t+PG(F_O>mebF)se^08Dofot>e8;#^#pds z-Q84vzlw{%1MP^~L|;CU#VE(CG>7$?xW3-)TKA}$bn}z#y(uzHTE%iBNxxK$9%F}b z>Z4d(D)B1WU+wIAK#%r_e*}JICuK+53!UGs`lP;t%WdfhG@B;<9TvIcIYB1aw=3a`6;z_dRF9*}C>&PVs<1@Vjm1^0d@ZrRr6 zt$&LdRi_cv@}>#?Ng*B+eVQLu88B(_&JFImtQZEvPMM)}*sm z5>W<{34}>%PQ8q9a$9Lf=)S|g?fJNL-o036MJPUs=hc1X!wnT{5Dmj$4gete+38`Y zk~0n1kr4?#>Mz97gwSd+a-nlI$J9fk^=^1-N;6YN$~PJowMb$|wfeiR-qORpdM!HInX z&|XxQhmNioLB)K9E?D8?ld|T0ucOjqck7H0&pCRo)4Q}=A^Pu`K@Cq*a~05LP--A! zeGIj#G?T;5<0J2Xyuh|vP_M~MmR32SD{@u1i{S}hdR?j*kf!`cJ&~U}C_xy=eUtOD`hJspbrEN3j4IUwb9a&khleWzaVUga*}YkrrieE^E5HGFsbxTfOaSv& zM^d1+x|sRHHsae!w&Fh4gu$<1^>)=@ZIqKeKU9@}GK7k}f>C`1Wi$M*1~VPEEt?5* z5LA*s>KNb?cT3FFAvH1ik4d}h36%-cpIN!hL@~YW2g z5v3TMM#m+`yF_b>G-?d#@t+-EWR#^h@@@&L<%)P(&sE$Tc*Hiy$^u(LL?i?n=Mjre z3mk1+n)ud-Z56^&jXdpzX&%X9*+2;&|IB{I!k|Z00wOHiueT>fd)f+B%77X%c*!p@ zmrE~N0?Oy5Dq!~yDc`;O0w>DVyA~NjMX!Zj-uMN&2@@Q&_Hc^}jn6jIqf^wTd>%ee%fLUXKb;y}ko7KDMp;&;P|svm^DV~G@^a~`m3;GOAd+Bzm#1iMWCIw*MC~bG zA1&Fgo$gq0KnWcnaS4ADVbbx3@&MBw9WAY{^&cE_dL0 zrb9Ztxn%?F+deb4lxV`lI1$4tXP~ssD}j~Sne=Tjss9^(IuZcxZ&23pQqb(jkjfa{ zXjQX-@snd@h-_$JdD!HeNjPuHQzS{pIrTRE)Wv|_K(;C8&6~m5OgF(I!a5G`%U$PH zoVDZ$tDjpDh2SP_(gnWmr+(QY3IlT;r01ci_=)+&a;gRqeOm#Ca2-%%R>_seGJ81> zh$!az=Eg%ggSX0We=>^_^Bt--uoB!fpN_>loD%+-|HCi%+`Rl>e8KeBW(8Vh>f`k8HiF(Ieo$So-H&eZ6k=n z+TSWeASRX#+a8W}ra`iYwohrBfMibU`X-E151oA{V8v(S!-o%w>@vqc<0!>4oL~cA z?9xI?K9|Isac5fbE?kjjkYd!=k=V(FXISX(4r*f^Qp~j6>BN5=Bk7fbd8^kcb`YKT z2!!(yhpTWd1-Y*ED92oN{0F0*o}Rk4x3``;kwejg>SrFOyeB^WA77u^ToXP&CSOR zX6E6_R33ghe)_7aD%+p2EA4~K9>s06Z+eE z6-ViRyB+WuG`%&YeGIR#Em`Bx_E;#_Ci^p$Bj$1c?~usD(-VFG#Ig(Mo$JM2wz0P# z7k$3n5o_Ct74k3C4XO;&({9<;@e4|5bbDp}e4ehCa?&}?hZa@?4;L$>3NCj?5|Fsb z_`ZJgnamW3R`AdUUph&4>O2g!NhDXkvUFvkm_)`6-dBHgNzf3%*}^gavE{ad{OD@n zqiOl-79}n+#7VueELq^&t&=KCpJhH0tIa611d2PJahMaVnzpWEZ{ z<5ZQ}-|50iwk39BdXbjBen&F+=OZ#k*HNI5K2AgAtOppj?sR+Z7v_A#9glGj7gqCZ zWOuGz^Riu$ua)T zrLdcLJm2k`jBPg3#Pg^!_$d{Ylmo>aqv?Yc1Nk<;U5&hgecqA&8&bqZ8hZ(pndC&l zDEuf$w=*8~-i3{2$GNTOvH<^Nq2wQmn3=*L{C~)=wn9}KbiF6w#fp5hM^!Ci3BiAZ zc2Z1lD?E&D=8FoAPKcqMot^FBY!P3;vBBoOs)50p{asAc6NZh{kJ8c>e~vftS5_^G z7{12OwXeJ^Hf(CsbxP!oyPKPyyah(^=SwmSmoZ9{Zx@5n z*Eyel0!*QFP8+1qeZ{%=Co3cghkCwi}JZEcN*=&?YV?yCZ5K=>M3AZg!d z(&=?!8U5}^Nai@G?RFP}M87tfCX? z$*NBR4x4RSbS*V5Gi7wlA@HlAP|rR}Ke8Ij{~s9X{~4+Nk@-w+)W^$1C-{Eol?n$a M$f!zJOPU7#F9R*(Q2+n{ diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns deleted file mode 100644 index 0e34c203e22a2d0bf8045cff0e63837630b42fc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 148492 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 XEa4xtxWv<500000NkvXXu0mjfFoI&Nr$M=9?E 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 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png deleted file mode 100644 index 74deb21e810c2d187ed242ffdeb674c003a8ce64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2422 zcmV-+35oWJP)_j<499RSg4iOhFMB;|Ti3?XGE?l{AfeW`B zKmrLkkQ^e%Mp@VrkhAZJodi2%wLLSB{$6#>JWp)TdoyDa?3P|^&-APM|E}ul>grA| zbWLVK%gqLosfl8Io?@~1KtX3R83}?QdXlyviR(g)CzpG;93Y?1N2Gb`CW9k_lf1;XEQ#0-1yiiheLMo_Nyc17+wi2Y6psSJ#Adb{tVNJ#(_H ztt}d@soY^Qg4tO2d!r&3heR&B=lU&?yQBK~JQqh6wTL{`Ci0AX<~GMXs+f+jVZ`f( zhK7VWTZub2(F+8gZQHg5ylAdbtpN^#SqJX+h{*nakskN-UlO@?(-9s+{`hymG1rNI ze1S;k(<0kCMYg)M$Hxy=`p6xOyuhF~F7@;NsU%{g^f2;&h? zwTisEL*$K3$*pUcjYHh3xk|Ss8SsCHE_wykNHXU>`C8rL#|_}wYa*Y%;|6Wv)MO3w zL|I$vOY$er{MM(&w!B z=LMbH$T@peb$zz0I@w7BR+`RXSaw$|2}uG5MDP%T$ZH!#7R=K=SZcJOb2OLHzKa&< z!|!~?cl4nz@9-`%kcCWS(|6E&4}HLpb{AdKc7(ZO?g|j*)tj3|mM#*xGpd%tnpkAn zPtcn5el6(Urhs z?n=ajALo@c1B5JOBKw;Is;jL@QWFi|K z=t3vD!2p&tE}V#H(F@z%r%M5ABp{w&qQS>3iBiC>Y%dd-Drpa5(bxL(E;20dMK(In zg-y5+^t!|=bO&0MVe$^3WboohiC777IZd|UBvlxzFK9V%+&%4Ojv|MFI^Sz+D z;=0Mcz?9UgRf*b&k-L(ZGom9dI6Sw+FChh0j*G2oFI%G4W}bm%ndf6ZYNHFC=mrD9 z3rtC^nn^O!G!k?haWWo~fxJ)B2_Ea0O2Zq$tjTXJ(S=TQo5%F@kH=Uu(a?`DTP?B{ zMw00F#6rz@NCp6at6AU)8c=tzIg3nWTjGIEbb|pbV7h1Vp)R8SZT6F-XpAmyMD9vj zXS*)kcDkC$Mi)BK4F<4)32bq!brEH5TJ}SzWMu98IXO)RN@287&C6o#M2BICbO+_E z>2f28TB9-$ZJI;fYOxEk+?8y{ssq!&GjdVnTd4)z(Vi+)OkhjW5@kkK{)pymMqMcM z33{}tp2m70vYWH23!Uf&Ls)^WAw*|FOXRxqVy+pbet92VeWFmD#`CzSO5rOMQNo{B z3_%69h7e`PZ-I#N2ODE6WfI=+#az2l&14a9YzaXpx}y?|4@h9Eo2adc6s)2ekPk?N zb^CtAjquQInKq_u<3{pjERk*>h`U=*?#St|cj3CKIyPg+J(w=sQ&g-m3lBkL0cbb|pbV6vsB zTHsH#d!bQWwuGG&jPxt#-)3}=oNv4}k%?>?y3mPkOZ>qU$5R46!Yp#&l!CPYm86tH-`->Tu6YD9k&O;Jzd<)g6<|qf&1jA%buBAdP<`W=V9$|q zxs#G)yxa*W0A3l^c^#EITXfy4R_-)+o$pB^n{65IGEZM#rx?O8T@kycT$i&DjtQus zTLGJrB-J_!SpYMajQU;Etrkl`v#%JC){*gy{2&;x1eF2z_r>dy=pfiqJ6x5)`QY_*2f>Cp#-Th24L{oN z)A@C|#-n|rG>Hjo_Ou-B)9l(xPft(Hmo;er^<|A{DtN4hz48jqths(O>4mD;<)r`3 zg{s&ku&FOpl}27E2S#bjmMstUrN={u4n^LV`qE>iWoPs?*2+0co3R&@D<=HR ozG_|>0A}=m9+(Mj)||ck3myRe&p8C)tpET307*qoM6N<$g2_mtcK`qY diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png deleted file mode 100644 index 761b732cef6bbf80d88d7e40bd602bbd7e29a3f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4292 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 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png deleted file mode 100644 index 73c63d85b9799bda9fcd2d5e6e039241d5c993e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1525 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 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png deleted file mode 100644 index 1e46db6cd644021b9cb116d0cd883499ecf70deb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3822 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 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png deleted file mode 100644 index d1303e0ce7ce87b1d255ac1637d0358a50f67174..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6558 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 V(q@iBKt56OO|}l}bFAIM{|g*D=B)q# diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png deleted file mode 100644 index d1303e0ce7ce87b1d255ac1637d0358a50f67174..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6558 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 Z{|9XmX1~Gt`#%5x002ovPDHLkV1k=?F(?24 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png deleted file mode 100644 index d60dc4fa20930ae297c8014e87077bdb74a0585d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3610 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 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png deleted file mode 100644 index 325aa1a7526d31e372dec60113f77a5ffa88d505..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8462 zcmV+pA@SacP)1^@s67{VYS001D>NklWCOiOO97+|=+voG@tHGcB7nAe^Jep!m?wh< z+Q0xNYFTS0^Z(()ht1!+cI}$?xq7{BMxC23WB0oNUf*nwMXu(5&GUJ@L=J%K=9_O8 z{twh#83@1tmW+&(Cr_FV?bxwn9`I1-`YM2w0crjlv%?3{v17*!|F>`7{w|s28eohF z(OUcW?^k>G?rptJ=dzmn3tE@}5;7Of)+r;AT`mEJv0klK)!DOW`8(dUX;YN7QCU!W zpot6P^y$;FmRq!b{rZ*+a|bYK*wLd$TXxn`H@2XqCy^0=J8|NKk=;_tt+bSlJap&~ z4K#p7-4@`=KOrMA^pPDhCR?mL`<^0wCmjYoe~YReCyGs9yIf<{Q@apDh9XmWZ84Kv z-vE=Ik0r2Sv{a@+TN{e9yc7P%KGyat*9sT8fLPR z4BSpYjsl5{ag=Y{wyp1}vM-e*-e5e$svVZdH>gC|ilDFqm6P~8vr3&lr_@WY>Faf+ z&b+B@khhe2>ymvBfXzA01_8>d1K6rbrLLS(>gv@>ZM;gU8?RPs^ELKw-Ya{kGMZ5( zgE~(nsXzTv&l*gSeDL5w6VD=+*ctE26@#3nDtij{q6W>8(@Gtf*0H&lbTC*0>y5XS zx>U2DMfiLkAOePk03m>Fdeft5#1#`tt)k@-LYCi5@Jf(%Z{pd2OxI;WWbJEYWq{?ckLrbp}^Y$9<( z46~iG3T1&XIc3|l|D;mC(Np)$+E)AA-Aa9Qj~ys7#SeRe5E+Zid$qw*Pm#X%@>6h! zI=Z|m<$$)UzVlP1p8bOrAixsv<(ULTEIj74E98#%DD{Pp+l4`AABRmM`(CtI>ZS`0 zc?je(QHMgl);jl&QvdR(QvdKD7GxOeOo!s|?3a9pYN^ZRf2Lm&%}SeTJAI%p^ohRF z$6-5eMb;wwUK|gpr%1VJtyXKvU>Kd15zWK@=LMzy?I(IgsC`yfu57>G7nnSOi|Zk7 zCM*CVJ3^z=sop>#HleI8zyGXKPajw6OZO>t->vieJ_Itok_?N108FS_t@=6i zLjf25s&9N>sUJUQNB-KW-hm~cLa?|i%=euGl~pEF?9_z4KgKz)i*WfY(-~g=OL}U% zShG_ctHfEC<-%uifdIH(z$RK3ZT9wMrT*z*ZKG+RJ^1OiX3iR9B5QVjsU8(eYJka{ z>$aB3Ak742JgDgy`=WM`e*F=p{&>~`s~6*4+y;CH?>xT&&$U$)qggLKUbyD$)d zbQ?=DRTD-6To*3c72>Pst)EStx5o2dabQasB&Sb2pFaNRIlE~2+5_6HvB@m}N|{a} zM%E(xfvh4ujg_eelaUX&@Mrz&W0t{JX~x&n0?TPDv<_fELv2~F)Yfa2x?!EZ*D4$K zQL62g7tJOe?gm)7KiUpp-f*>j4FF?9{pk%o7+$qLIKXBcyXACRO;(=tmG6J~A*;zg zd3zhES%_gVvhR6{^ftu~ylc(4ZPPsTGkdLCVFi>1TmmedoCu0|;cl$~c5Kjpud$#d zQGk10s(GFAoTO;i?%ON~)CXfJy3;@Z$=t&Hnv=?ezPV zefG7CK-L%K5@0^Vu(y8rJ1d|@O05M5(MV`1;(og}T3d^0i`QE@UIXrj{|p1_hSi$4 zY_^&dZAN?)?MC~g?cGRHjNkwEuu ztj3z1Ya83c5`usY{772>+K%h2bC`>%ocyvRZN@-@C-4RmkVz&;0kCf1^8RL8M02G9 zSL7sz;K;}N==*d)d98CJ3>jG627+=xBl7Z*oAmUp?ZTfQR%-e+?T%o5Jrxg~)(R^XTEHEzgV#W{C0Dg(x=~OdNR#>b-nqg0qZ&N|t5t5rxdoby_B3U*U&*feT3-5u&&j>g`n%17q;3In&rNnw@`R33 zT&~%bW@ySZE}PYsXV+_&%O`HjSNt*;0Bd+oR6O*!B9fNw2n8@<$bagNw$|z;13{+N zdb8aIY&Oz*@TB9{;(vr(Ew=$?d}Iv^ZanxGw8#DYMVn&H|8lR2i6f=+;!B(9!w$VD zxl&JmKX}$I0B}(>6O4a&hFO7sPn@xN0NKz1Ch5%BBN+8CCTZD@pf71~U4NBQ`}SHJ zL~O0Jz%n42vswp7D@lNO^6{Kk`rLWPO7YE$){Bp06h|vlR>kw;Cz0t}&Dx%y)zkfB z`;~h6oVClk@x<^jvik<$;kUnN!%BHD`iBoNJIIaZBK(u&p%-v%zS@EdFk;N70hhP| zSXH7Gw(2Q$>w2pNJKOZG`7K(Y;T`V-Oq{D+@4LZjANs^b611(KMVkaN`a~N)^Fa%; z>DR*`<8=la&x4m=y+0oY|4@~vB-K6syj^myO#!aUp5_8vOjSwX62Di?**SY|u(rwv zwza_(#|e8ORmT|u=pUGa(!Ph5q4ql9C%5N9K7{;y=(v` zsW)VA{`i1hwFOnVO8~t07Q0$ZV{;(@Y%Q`S&>Wkt>Ht=T{cY_oasZ$$w28LO&ZPqx zeWvfYV~npW!F53bCcK5mCM$MGQ{_+q7P+UAL!ZXr#i+kikNlk*)2|lM4s4`gAENk@ zhP{2?&qt$8w2d~_>L~=}R+sqt;6;)>rW}~zvCs3d2bfXs%7#UVzpY-;U4=((0Lfw^`u{qw^Ljl7U4VX=}Za?tAb6UJw$S4NS4{6yCyPc3MShI0k>_02Alm zcb{^JcL@$eKEqb7w0%RKchg=aXG5`#i@kb;JU;xg-H7bVU@QS9jCk~!utpNFuq-!BOfc%jpvS)@nroCQ@Cz?!JAFtp{Ke>p zH}EI~O5p{&+UPsi);!7tVB)o>gc4P5R!)Sq7hZ0>?MbOB%H=;X zRrs!mQ6=AnK{jsF?bYl?vh9xj28}qZ#o#%-M+RAX_cH;QfQ0;WvJ57>c<7~yM||TjfFwR{opygM+Ux3#sXl{X^OGM3Gqgj z>bo{M+bdxS&PW$u*I1p;=K4b4LLoE=9>FWjiF(2cl>1PCFH3d}9AKs#!tgrWPmJ6rEJQu4<2By7e{84 zI&;4Jh>Tp?Fo}OGy+fn11;mXWm&c706gv_eWcHgfM3k`&s-_0ZVp-#{4QBp zP(a0((P#q9wVvI|?D0z~D+P0s5R0vGUDD^qkmz$cFirZO3mK&)w}XpQO?(-RB*28& zRTFaT@gAxh_1+AGM;b1!w$>e(FwWDXko1v0PX&B~W|8D}dM9oB160T)3mPcEME*>X zNgEYdimzGOeHxH$@uXK*%HwyGUWn)`eWvftZu$7md@$7+tH~Q^IEBQ0ZG-{lT5k%j zmAd=CB;qfO3B}Y}$Gt2!gelwiPz=`$)QNVcN;iKPvS6c)FQbtL7~ASY_7~NhrzXN~ z3?WA7a;2ociyOlD2SfWyOSa5`FQbtInBatz5|+eLZMoZLaVVBj?5^=oEL(0w-_!8O z?D-7vy5w9(vk5h5a}6+Qb*-L|72!r+tHNKm+c^j8#AqI>yCg*4OMz4y*^&n@LlF+i z!l=!8UmQ7rIg-+mQpu(pRZ|okDM#Mtmca_LMNyka(P+G|wp6DPs z6e~%IH&PZ%E{}S!+aS2?#UKM6noD61$gwSkmZ2Za)NW1D?;tOu_J-Vk3+?3tU=B>I zd_n+jg$ris&+Oz;saCf;< zQr{)74qk?;=^zUUxs7%{m}@+Bt-2LWlkH~ayv17BN^)_ zyO(B=Vt#CM^JO%e0CT@LtPOWW7{dtdmh&W@d}3&!9Y^DeFZxX1yL$3n5SueAlgeE2 z{Z`WEfZLu)J{X%8l8Sk-@*&eQw4JK#G~JsQ+l(4_l0MRB`d$ucFqf@5mryK*_ins8 zoAzgtO>yHju3SJunM}4+g>Pupd*bKDkmz$Gp_BmhY65SXuGTF@{d^gXCcxZpw!xEW zIoIsI83FYUb3%b}nFeOgWy732ko1v0hf<~`fDI74ZTpiLB(jhxa^w>ZWry;XYuvub zg>d8Ym{9MO-mY}9GfZZuD}Bx(2lCxXE#!ufazvbQT@Iy0JBKSLS}Ygh%0}QVKH@ZsT0Q+?4S6Mr%hDJ6R7xln zAfKhETsa|z3(xP~Wo3}&Jn)eMn1Br8oW>QEWX(jex}!4*<%m?fxbQP4=09f?N!m_d zTE%+1d{)X`F*VM-nXKv9)cncYvgc7|02A%P!JM~mZAbjO0SCt+1#2(OhBq+?AX&=4 z@KXD=QKQ(iyXZ?&ZB}y(^{nMlr4-g)@cMRb+~0J4_QjGjJ?TR<5yu3FN{&lPFH3Kx zipffHF%L#FMeeZOM$H}*UeNYKe`@b*41FG+z#DiJIM=-z#!T2 z^0yyOckmOR`T3#rhb*vE|IP8XCKI9L5m?ek+De<#Y{C$Y#Bqpe`S-cI=ktE70Vam) zpTEzZ(DC-=u$Ow$*;h{=OD;>~gKYoFwvQ}tQ>H&yXd`V+11>Sv;RQwgCM)4+2fT&H z9ET_iqOk{<0gf9kRuZN@bKHi8R!*b?3)2X~BKfd)0Sn9W$9`*#*R&urRe36Q zrG_>;aLR4%isml_mI7@4edPZ1?fGf@Y}!Cu%29f7(bvcJ+rB4(vgA^HzI31EZI)j9 zTmcMXB*MhMyhW??+wI1NN-4~6?6H^6S%#+unFmmQb1>)Q2drCw7@iLX|MT-M)p0HG ze&>Av1kd1{MtpRB87aw8(*X@bIrRFYmk6ezrVR3Fn}?jts5`KWzm+b1wN% zCk))_*X=RE`#xY(fs&407VQFI_fX@36c^HA80iFp$ycMAvsuwaDI4w)*`f%TymD`3WWmZ#bap;d6P5qbI%pHQH_u12VC!HH zvY+#yKz5PbG&-&~)eY6!-A2mAdy9d-~etx%8VEZUX<{+2xVh67g8=X!O6NIz&hePCLo zPiguAkKh$N!!3~~aGAlIpQg08#ltMNUp?})t)WnFhDUzcX_#LfwVDUdwIntgA5f3R zNN96Kn$PtDEZRO*wXsD)Vni*_S}MgV6K%;N<$=2_&!oIUu4J00#`pqcs{HHsYoNUo zURjEukpv;*ai@IjGMeCuX=t~Cd! z<4)j`s-A-an}CYjS6s`>{SS|kDUs#(f@8IeSV5L$?24zB7-=zVWjA|H4 z5tVOz-xlz|-l``|nW8~hjbVzR(SCJAso$Nnj#bXM<``letu0%vX#wGDl)(9@_P49p z44A~oW;({VMnIb#*En8rp#b_`fMtZ_DnN>#i*~5MC-@ zi^G-a3r{{;lfPwW6!%&=(f&@_zVqF47p;f>Yo4DAAZMkxJXuE_SXerMHTBTe2v!#^ zTxgjt8THRR$altm`-@hS{pdNX$uN%7WNZ4XGragx5ck#d?UGOc0dV2L=iY$A__@k$ z{}Ru_lMi@s%%WXP3D7xLLfp9@jJEPtg}tm~`98id?oVuy@XruE^hVYqd%?nz0!*l0 zuV<>kI+-qErLW$v)CYGc_07j^ScUM8Qt5^0J40m(mG7H6r!||T7jhX$+7AVHOHSLY ztj-5>ukI0<@`!1Y#`KSWW0L^{*u!w5imd16=6Y5tsR1S`v0APA3L$gzQgWeqI2i+X zYB$8Uo>1z?2kh&sSM)B}PS3kJ1;fP=RiKk`$31!h_(H&nT$(e#_uXOhxv&qV1pE*^ z^hVYb6BF~WkP0_(N>wCkab{*lZQQs~<$?SWO^(-herk8xFr7_J_NPIzCIJ>xEYmAF z^rSgUiKRS+enj@YOp&PpW=E+t?oORL6>r(Hg}FG^4;tzsSwJL0V7JY8pR&2`a)7D0 zJo2);NQ`$mSfc4RB(u|s%`gJ2&;lbX5*drkdol2#o@}s&b{bVG6?Nv!nMgm3hexF$ z2fxHcV9F7dY#fs7$=@sW=rc+^cgosS##nc~9R}F^qX=i6@y=pm?>4&;nZ2TBz$k0` zxfq?)_z$8;-xtzgiN~zoC27wa}8%?=heA(J(PaU-@#n`3!u=0$&okmLy zxYh!|J%be?K<9saa1Q4c@^-HhWd@NHZ_H7$l;=P)4 zptiDCOIG7exRd1Y!I84fkvW{8!NC)RwN_2ee@?U6PFBo&nJJO!v17;LojZ5- zQNiwE8~8nqBu`WLyMholKp+6l)kzrS0IcpE2FiVyS^?N(B?YJ*phk#Yt(Bs93HqI> z`qE&K_UN0{#Qy#J)!w~(@$&jk*pZ@6Ltl*Zv|47tt!rv@i{XhZMYbaAOabQmLr$MQ z9dFvSDOyZJatprJkA=85ey6^P3`M4$vhJ1oR@yv}c^cAaH&3Tw^4hqNug%_dja5(E zeolUiO!+%ngeqQ?Re%u);@7WVuZ|u)YQWBG;|8+jK;A+vK5^oNktwoW?BZ?FW+#Ye z&fc|ampXLlP$VFuaRrSAc|eMhA+kiKOJVNxZG&}^7=~!F?c2AT29L7gzg)i?7U0 znQ>v8xrHO9?>I#&htOtNp0-9K#3!JYzCeu{fmz{(t8LaZve2C0?{ znopkHn|Xpl<^byi8;_SJ(D?#%iwBeBZ^297JQ)TJ6<`I8KaE%FLqP?=3Mv3rP(cO2 w3Mv3rP(cO23Mv3rPyw)l3Mv3r&=BbV0mlo*yFje%wEzGB07*qoM6N<$f;{Dn-T(jq diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png deleted file mode 100644 index 0ab8ebc83725820abbc9f84719100a3cab3e1555..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9274 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 SaKGjR0hHv_Wox9(LjMm;+6p-U diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-default-symbolic.svg b/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-default-symbolic.svg deleted file mode 100644 index e2bd5f67136..00000000000 --- a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-default-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-instant-symbolic.svg b/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-instant-symbolic.svg deleted file mode 100644 index 579dcfd0aa3..00000000000 --- a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-instant-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg b/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg deleted file mode 100644 index f54b7f06f14..00000000000 --- a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-screenshot-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-stop-symbolic.svg b/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-stop-symbolic.svg deleted file mode 100644 index 127c60a62fb..00000000000 --- a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-stop-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-studio-symbolic.svg b/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-studio-symbolic.svg deleted file mode 100644 index 2f096179a26..00000000000 --- a/apps/desktop/src-tauri/icons/linux/so.cap.desktop-tray-studio-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/desktop/src-tauri/icons/macos/icon.png b/apps/desktop/src-tauri/icons/macos/icon.png deleted file mode 100644 index 15fca4afa5122fedd2f250c2bdf7eb466a44f745..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54427 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 L_wjn?;M9KsTK*P# diff --git a/apps/desktop/src-tauri/icons/tray-default-icon-instant.png b/apps/desktop/src-tauri/icons/tray-default-icon-instant.png deleted file mode 100644 index af33c96c656c66072b98b3c737ec8d7852e36380..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4316 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 YA{_R{ZPZ!w{vRM8I>Y1k7IxvhR8TuM@0 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 diff --git a/apps/desktop/src-tauri/icons/tray-default-icon-studio.png b/apps/desktop/src-tauri/icons/tray-default-icon-studio.png deleted file mode 100644 index 27aa4ad59c7deb93387f5dc20b793776137fe4c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4550 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 diff --git a/apps/desktop/src-tauri/icons/tray-stop-icon.png b/apps/desktop/src-tauri/icons/tray-stop-icon.png deleted file mode 100644 index f2995afa2ef7013ee2834ca355e92dbca1bdc6b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6274 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 CnGT2m diff --git a/apps/desktop/src-tauri/sounds/screenshot.ogg b/apps/desktop/src-tauri/sounds/screenshot.ogg deleted file mode 100644 index 40e297b416e9d138256ed9b97b6d0f8416334a43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16122 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 diff --git a/apps/desktop/src-tauri/src/api.rs b/apps/desktop/src-tauri/src/api.rs deleted file mode 100644 index c468376396f..00000000000 --- a/apps/desktop/src-tauri/src/api.rs +++ /dev/null @@ -1,492 +0,0 @@ -//! TODO: We should investigate generating this with OpenAPI. -//! This will come part of the EffectTS rewrite work. - -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}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MultipartUploadInitiateResponse { - pub upload_id: String, - pub provider: Option, -} - -#[instrument(skip(app))] -pub async fn upload_multipart_initiate( - app: &AppHandle, - video_id: &str, - replace_existing: bool, -) -> Result { - let resp = app - .authed_api_request("/api/upload/multipart/initiate", |c, url| { - c.post(url) - .header("Content-Type", "application/json") - .json(&serde_json::json!({ - "videoId": video_id, - "contentType": "video/mp4", - "replaceExisting": replace_existing - })) - }) - .await - .map_err(|err| format!("api/upload_multipart_initiate/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = resp - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(format!("api/upload_multipart_initiate/{status}: {error_body}").into()); - } - - resp.json::() - .await - .map_err(|err| format!("api/upload_multipart_initiate/response: {err}").into()) -} - -#[instrument(skip(app, upload_id))] -pub async fn upload_multipart_presign_part( - app: &AppHandle, - video_id: &str, - upload_id: &str, - part_number: u32, - md5_sum: Option<&str>, -) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Response { - presigned_url: String, - } - - let mut body = serde_json::Map::from_iter([ - ("videoId".to_string(), json!(video_id)), - ("uploadId".to_string(), json!(upload_id)), - ("partNumber".to_string(), json!(part_number)), - ]); - - if let Some(md5_sum) = md5_sum { - body.insert("md5Sum".to_string(), json!(md5_sum)); - } - - let resp = app - .authed_api_request("/api/upload/multipart/presign-part", |c, url| { - c.post(url) - .header("Content-Type", "application/json") - .json(&serde_json::json!(body)) - }) - .await - .map_err(|err| format!("api/upload_multipart_presign_part/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = resp - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(format!("api/upload_multipart_presign_part/{status}: {error_body}").into()); - } - - crate::upload::lifecycle::cancellable(resp.json::()) - .await? - .map_err(|err| format!("api/upload_multipart_presign_part/response: {err}").into()) - .map(|data| data.presigned_url) -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UploadedPart { - pub part_number: u32, - pub etag: String, - pub size: usize, - #[serde(skip)] - pub total_size: u64, - #[serde(skip)] - pub object_identity: Option, -} - -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct S3VideoMeta { - #[serde(rename = "durationInSecs")] - pub duration_in_secs: f64, - pub width: u32, - pub height: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub fps: Option, -} - -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum MultipartCompletion { - Completed(Option), - ReplacementRestartRequired, -} - -fn replacement_restart_required(status: u16, body: &str, replace_existing: bool) -> bool { - #[derive(Deserialize)] - struct ErrorBody { - code: String, - } - - replace_existing - && status == 409 - && serde_json::from_str::(body) - .is_ok_and(|body| body.code == "REPLACEMENT_RESTART_REQUIRED") -} - -#[instrument(skip_all)] -pub async fn upload_multipart_complete( - app: &AppHandle, - video_id: &str, - upload_id: &str, - parts: &[UploadedPart], - meta: Option, - replace_existing: bool, -) -> Result, AuthedApiError> { - match upload_multipart_complete_outcome(app, video_id, upload_id, parts, meta, replace_existing) - .await? - { - MultipartCompletion::Completed(identity) => Ok(identity), - MultipartCompletion::ReplacementRestartRequired => { - Err("Replacement upload must be restarted; local files retained".into()) - } - } -} - -#[instrument(skip_all)] -pub(crate) async fn upload_multipart_complete_outcome( - app: &AppHandle, - video_id: &str, - upload_id: &str, - parts: &[UploadedPart], - meta: Option, - replace_existing: bool, -) -> Result { - #[derive(Serialize)] - #[serde(rename_all = "camelCase")] - pub struct MultipartCompleteRequest<'a> { - video_id: &'a str, - upload_id: &'a str, - parts: &'a [UploadedPart], - #[serde(flatten)] - meta: Option, - replace_existing: bool, - } - - #[derive(Deserialize)] - pub struct Response { - #[serde(rename = "objectIdentity")] - object_identity: Option, - } - - trace!("Completing multipart upload"); - - let resp = app - .authed_api_request("/api/upload/multipart/complete", |c, url| { - c.post(url) - .header("Content-Type", "application/json") - .json(&MultipartCompleteRequest { - video_id, - upload_id, - parts, - meta, - replace_existing, - }) - }) - .await - .map_err(|err| format!("api/upload_multipart_complete/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = crate::upload::lifecycle::cancellable(resp.text()) - .await? - .unwrap_or_else(|_| "".to_string()); - if replacement_restart_required(status, &error_body, replace_existing) { - return Ok(MultipartCompletion::ReplacementRestartRequired); - } - return Err(format!("api/upload_multipart_complete/{status}: {error_body}").into()); - } - - crate::upload::lifecycle::cancellable(resp.json::()) - .await? - .map_err(|err| format!("api/upload_multipart_complete/response: {err}").into()) - .map(|data| MultipartCompletion::Completed(data.object_identity)) -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum PresignedS3PutRequestMethod { - #[allow(unused)] - Post, - Put, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PresignedS3PutRequest { - pub video_id: String, - pub subpath: String, - pub method: PresignedS3PutRequestMethod, - #[serde(flatten)] - pub meta: Option, -} - -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct SignedUploadTarget { - pub url: String, - #[serde(default)] - pub headers: HashMap, -} - -#[instrument(skip(app))] -pub async fn upload_signed( - app: &AppHandle, - body: PresignedS3PutRequest, -) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Response { - presigned_put_data: SignedUploadTarget, - } - - let resp = app - .authed_api_request("/api/upload/signed", |client, url| { - client.post(url).json(&body) - }) - .await - .map_err(|err| format!("api/upload_signed/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = resp - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(format!("api/upload_signed/{status}: {error_body}").into()); - } - - crate::upload::lifecycle::cancellable(resp.json::()) - .await? - .map_err(|err| format!("api/upload_signed/response: {err}").into()) - .map(|data| data.presigned_put_data) -} - -#[instrument(skip(app))] -pub async fn upload_signed_batch( - app: &AppHandle, - video_id: &str, - subpaths: &[String], -) -> Result, AuthedApiError> { - #[derive(Deserialize)] - struct Response { - urls: std::collections::HashMap, - } - - let resp = app - .authed_api_request("/api/upload/signed/batch", |client, url| { - client.post(url).json(&serde_json::json!({ - "videoId": video_id, - "subpaths": subpaths, - })) - }) - .await - .map_err(|err| format!("api/upload_signed_batch/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = resp - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(format!("api/upload_signed_batch/{status}: {error_body}").into()); - } - - crate::upload::lifecycle::cancellable(resp.json::()) - .await? - .map_err(|err| format!("api/upload_signed_batch/response: {err}").into()) - .map(|data| data.urls) -} - -#[instrument(skip(app))] -pub async fn desktop_video_progress( - app: &AppHandle, - video_id: &str, - uploaded: u64, - total: u64, -) -> Result<(), AuthedApiError> { - let resp = app - .authed_api_request("/api/desktop/video/progress", |client, url| { - client.post(url).json(&json!({ - "videoId": video_id, - "uploaded": uploaded, - "total": total, - "updatedAt": chrono::Utc::now().to_rfc3339() - })) - }) - .await - .map_err(|err| format!("api/desktop_video_progress/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = resp - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(format!("api/desktop_video_progress/{status}: {error_body}").into()); - } - - Ok(()) -} - -#[derive(Serialize, Deserialize, Type, Debug, Clone, Default)] -#[serde(rename_all = "camelCase")] -pub struct OrganizationBrandColors { - pub primary: Option, - pub secondary: Option, - pub accent: Option, - pub background: Option, -} - -fn default_organization_role() -> String { - "member".to_string() -} - -#[derive(Serialize, Deserialize, Type, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Organization { - pub id: String, - pub name: String, - pub owner_id: String, - #[serde(default = "default_organization_role")] - pub role: String, - #[serde(default)] - pub can_edit_brand: bool, - #[serde(default)] - pub icon_url: Option, - #[serde(default)] - pub brand_colors: OrganizationBrandColors, -} - -pub(crate) async fn prepare_recording_segments( - app: &AppHandle, - video_id: &str, - segments: &[crate::upload::preparation::Segment], -) -> Result>, AuthedApiError> { - #[derive(Deserialize)] - struct Response { - version: u32, - prepared: Vec, - } - - let response = app - .authed_api_request("/api/recording/prepare", |client, url| { - client - .post(url) - .timeout(std::time::Duration::from_secs(20)) - .json(&serde_json::json!({ "videoId": video_id, "segments": segments })) - }) - .await?; - if matches!(response.status().as_u16(), 404 | 405) { - return Ok(None); - } - if !response.status().is_success() { - return Err(format!( - "Optional recording preparation unavailable ({})", - response.status() - ) - .into()); - } - let response: Response = crate::upload::lifecycle::cancellable(response.json()).await??; - if response.version != 1 || response.prepared.len() > 32 { - return Ok(None); - } - Ok(Some(response.prepared)) -} - -pub async fn verify_recording_complete( - app: &AppHandle, - video_id: &str, - verification: &cap_recording::upload_verification::UploadVerification, -) -> Result, AuthedApiError> { - let response = app - .authed_api_request("/api/upload/recording-complete", |client, url| { - client - .post(url) - .timeout(std::time::Duration::from_secs(45)) - .json(&serde_json::json!({ - "videoId": video_id, - "verification": verification, - })) - }) - .await?; - let status = response.status(); - let response: serde_json::Value = - crate::upload::lifecycle::cancellable(response.json()).await??; - if status == reqwest::StatusCode::CONFLICT - && response.get("status").and_then(serde_json::Value::as_str) == Some("reupload-required") - { - return Err(AuthedApiError::ReuploadRequired); - } - if !status.is_success() { - return Err(format!( - "Recording verification is unavailable ({status}); local recording retained" - ) - .into()); - } - verification - .verified_receipt(video_id, &response) - .map_err(AuthedApiError::from) -} - -pub async fn fetch_organizations(app: &AppHandle) -> Result, AuthedApiError> { - let resp = app - .authed_api_request("/api/desktop/organizations", |client, url| client.get(url)) - .await - .map_err(|err| format!("api/fetch_organizations/request: {err}"))?; - - if !resp.status().is_success() { - let status = resp.status().as_u16(); - let error_body = resp - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(format!("api/fetch_organizations/{status}: {error_body}").into()); - } - - resp.json() - .await - .map_err(|err| format!("api/fetch_organizations/response: {err}").into()) -} - -#[cfg(test)] -mod replacement_completion_tests { - use super::*; - - #[test] - fn restart_requires_replacement_conflict_and_exact_structured_code() { - let restart = r#"{"code":"REPLACEMENT_RESTART_REQUIRED","error":"Restart upload"}"#; - assert!(replacement_restart_required(409, restart, true)); - assert!(!replacement_restart_required(409, restart, false)); - for status in [200, 400, 401, 403, 404, 500, 503] { - assert!(!replacement_restart_required(status, restart, true)); - } - for body in [ - "REPLACEMENT_RESTART_REQUIRED", - r#"{"error":"REPLACEMENT_RESTART_REQUIRED"}"#, - r#"{"code":"OTHER_CONFLICT"}"#, - r#"{"code":"replacement_restart_required"}"#, - r#"{"code":null}"#, - r#"{"code":1}"#, - "{}", - "invalid JSON", - ] { - assert!(!replacement_restart_required(409, body, true)); - } - } -} diff --git a/apps/desktop/src-tauri/src/audio.rs b/apps/desktop/src-tauri/src/audio.rs deleted file mode 100644 index 6087a9ad6ce..00000000000 --- a/apps/desktop/src-tauri/src/audio.rs +++ /dev/null @@ -1,95 +0,0 @@ -use cap_audio::DecodedAudio; - -#[path = "recording_start_sound.rs"] -mod recording_start_sound; - -pub use recording_start_sound::StartCue; - -pub fn prime_recording_start_sound() -> StartCue { - recording_start_sound::prime(AppSounds::StartRecording.get_sound_bytes()) -} - -pub async fn play_recording_start_sound(cue: StartCue, gate: cap_recording::RecordingStartGate) { - recording_start_sound::play(cue, gate).await; -} - -fn play_audio(bytes: &'static [u8]) { - use rodio::{Decoder, OutputStream, Sink}; - use std::io::Cursor; - - std::thread::spawn(move || { - if let Ok((_, stream)) = OutputStream::try_default() { - let file = Cursor::new(bytes); - let source = Decoder::new(file).unwrap(); - let sink = Sink::try_new(&stream).unwrap(); - sink.append(source); - sink.sleep_until_end(); - } - }); -} - -#[allow(dead_code)] -pub enum AppSounds { - StartRecording, - StopRecording, - Screenshot, - Notification, -} - -impl AppSounds { - pub fn play(&self) { - let bytes = self.get_sound_bytes(); - play_audio(bytes); - } - - fn get_sound_bytes(&self) -> &'static [u8] { - match self { - AppSounds::StartRecording => include_bytes!("../sounds/start-recording.ogg"), - AppSounds::StopRecording => include_bytes!("../sounds/stop-recording.ogg"), - AppSounds::Screenshot => include_bytes!("../sounds/screenshot.ogg"), - AppSounds::Notification => include_bytes!("../sounds/action.ogg"), - } - } -} - -pub fn get_waveform(audio: &DecodedAudio) -> Vec { - cap_audio::waveform_peaks(audio.sample_slices(), audio.channels()) -} - -const WAVEFORM_PEAKS_PER_SECOND: f64 = 10.0; -const WAVEFORM_SILENCE_DB: f32 = -60.0; - -/// Playback reads each track at source time plus its timing repair offset -/// (`SegmentAudioTimingRepair`), so the peaks are shifted the same way; the -/// editor then only has to add the user's clip offset. -pub fn align_waveform_to_playback(peaks: Vec, timing_offset_secs: f32) -> Vec { - let shift = (f64::from(timing_offset_secs) * WAVEFORM_PEAKS_PER_SECOND).round() as isize; - if shift > 0 { - peaks.into_iter().skip(shift.unsigned_abs()).collect() - } else if shift < 0 { - std::iter::repeat_n(WAVEFORM_SILENCE_DB, shift.unsigned_abs()) - .chain(peaks) - .collect() - } else { - peaks - } -} - -#[cfg(test)] -mod waveform_alignment_tests { - use super::align_waveform_to_playback; - - #[test] - fn shifts_peaks_by_the_timing_repair_offset() { - let peaks = vec![-10.0, -20.0, -30.0, -40.0]; - assert_eq!( - align_waveform_to_playback(peaks.clone(), 0.2), - vec![-30.0, -40.0] - ); - assert_eq!( - align_waveform_to_playback(peaks.clone(), -0.1), - vec![-60.0, -10.0, -20.0, -30.0, -40.0] - ); - assert_eq!(align_waveform_to_playback(peaks.clone(), 0.02), peaks); - } -} diff --git a/apps/desktop/src-tauri/src/audio_library.rs b/apps/desktop/src-tauri/src/audio_library.rs deleted file mode 100644 index 561b9673696..00000000000 --- a/apps/desktop/src-tauri/src/audio_library.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::path::PathBuf; - -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; - -const AUDIO_IMPORT_EXTENSIONS: &[&str] = &["ogg", "m4a", "mp3", "wav", "aac", "flac"]; - -/// Built-in music tracks bundled as Tauri resources under `assets/music/{id}.mp3`. -/// `id` doubles as the bundled file stem so the resource path is derivable. -const AUDIO_LIBRARY: &[(&str, &str)] = &[ - ("lofi-beats-mirostar", "Lofi Beats"), - ("raindrops-lofi-sleep-bluelike", "Raindrops"), - ("sunday-mood-lofi-cafe-upbeat-bluelike", "Sunday Mood"), - ("good-night-lofi-cozy-chill-fassounds", "Good Night"), - ( - "ambient-trap-empty-streets-dreamstate-openmindaudio", - "Empty Streets", - ), - ("lofi-study-calm-peaceful-chill-hop-fassounds", "Study"), - ("lofi-cinematic-pulsebox", "Cinematic"), - ("lofi-hip-hop-leberch", "Hip Hop"), - ("cassette-retrositive", "Cassette"), - ("lofi-smooth-pulsebox", "Smooth"), -]; - -const AUDIO_LIBRARY_CATEGORY: &str = "Lo-Fi"; - -#[derive(Serialize, Deserialize, Type, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct AudioLibraryTrack { - pub id: String, - pub name: String, - pub category: String, -} - -#[derive(Serialize, Deserialize, Type, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ImportedAudioTrack { - /// Path relative to the project directory, e.g. `assets/audio/`. - pub path: String, - pub name: String, - pub duration: f64, -} - -#[tauri::command] -#[specta::specta] -pub fn list_audio_library() -> Vec { - AUDIO_LIBRARY - .iter() - .map(|(id, name)| AudioLibraryTrack { - id: (*id).to_string(), - name: (*name).to_string(), - category: AUDIO_LIBRARY_CATEGORY.to_string(), - }) - .collect() -} - -fn probe_duration(path: &std::path::Path) -> f64 { - get_media_duration(path) - .map(|duration| duration.as_secs_f64()) - .unwrap_or(0.0) -} - -#[tauri::command] -#[specta::specta] -#[tracing::instrument(skip(app, editor_instance))] -pub async fn add_audio_library_track( - app: AppHandle, - editor_instance: WindowEditorInstance, - id: String, -) -> Result { - let (_, name) = AUDIO_LIBRARY - .iter() - .find(|(track_id, _)| *track_id == id) - .ok_or_else(|| format!("Unknown library track: {id}"))?; - let name = (*name).to_string(); - - let source = app - .path() - .resolve(format!("assets/music/{id}.mp3"), BaseDirectory::Resource) - .map_err(|e| format!("Failed to resolve bundled track: {e}"))?; - - let project_path = editor_instance.project_path.clone(); - - tokio::task::spawn_blocking(move || { - let audio_dir = project_path.join("assets").join("audio"); - std::fs::create_dir_all(&audio_dir) - .map_err(|e| format!("Failed to create audio directory: {e}"))?; - - // Stable name so re-adding a library track reuses the existing copy. - let dest_name = format!("library-{id}.mp3"); - let dest = audio_dir.join(&dest_name); - - if !dest.exists() { - std::fs::copy(&source, &dest).map_err(|e| { - format!( - "Failed to copy bundled track from {}: {e}", - source.display() - ) - })?; - } - - Ok(ImportedAudioTrack { - path: format!("assets/audio/{dest_name}"), - name, - duration: probe_duration(&dest), - }) - }) - .await - .map_err(|e| format!("Audio import task failed: {e}"))? -} - -#[tauri::command] -#[specta::specta] -#[tracing::instrument(skip(editor_instance))] -pub async fn import_audio_track_file( - editor_instance: WindowEditorInstance, - source_path: String, -) -> Result { - let source = PathBuf::from(&source_path); - - let extension = source - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.to_lowercase()) - .ok_or_else(|| "Audio file has no extension".to_string())?; - - if !AUDIO_IMPORT_EXTENSIONS.contains(&extension.as_str()) { - return Err(format!("Unsupported audio format: .{extension}")); - } - - let display_name = source - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or("Audio") - .to_string(); - - let project_path = editor_instance.project_path.clone(); - - tokio::task::spawn_blocking(move || { - if !source.exists() { - return Err(format!("Audio file not found: {}", source.display())); - } - - let audio_dir = project_path.join("assets").join("audio"); - std::fs::create_dir_all(&audio_dir) - .map_err(|e| format!("Failed to create audio directory: {e}"))?; - - let dest_name = format!("{}.{extension}", uuid::Uuid::new_v4()); - let dest = audio_dir.join(&dest_name); - std::fs::copy(&source, &dest).map_err(|e| format!("Failed to copy audio file: {e}"))?; - - Ok(ImportedAudioTrack { - path: format!("assets/audio/{dest_name}"), - name: display_name, - duration: probe_duration(&dest), - }) - }) - .await - .map_err(|e| format!("Audio import task failed: {e}"))? -} diff --git a/apps/desktop/src-tauri/src/audio_meter.rs b/apps/desktop/src-tauri/src/audio_meter.rs deleted file mode 100644 index db477c78d5a..00000000000 --- a/apps/desktop/src-tauri/src/audio_meter.rs +++ /dev/null @@ -1,242 +0,0 @@ -use cap_recording::feeds::microphone::MicrophoneSamples; -use cpal::{SampleFormat, StreamInstant}; -use keyed_priority_queue::KeyedPriorityQueue; -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)] -pub struct AudioInputLevelChange(f64); - -pub fn spawn_event_emitter( - app_handle: AppHandle, - audio_input_rx: flume::Receiver, -) { - let mut time_window = VolumeMeter::new(0.2); - tokio::spawn(async move { - while let Ok(samples) = audio_input_rx.recv_async().await { - let floats = samples_to_f64(&samples); - - let db = db_fs(floats); - - time_window.push(samples.info.timestamp().capture, db); - - let max = time_window.max(); - - AudioInputLevelChange(max).emit(&app_handle).ok(); - } - }); -} - -// https://github.com/cgbur/meter/blob/master/src/time_window.rs -struct VolumeMeter { - keep_duration: Duration, // secs - maxes: KeyedPriorityQueue, - times: VecDeque, -} - -impl VolumeMeter { - pub fn new(keep_secs: f32) -> Self { - Self { - keep_duration: Duration::from_secs_f32(keep_secs), - maxes: KeyedPriorityQueue::new(), - times: Default::default(), - } - } - - pub fn push(&mut self, time: StreamInstant, value: f64) { - let value = MinNonNan(-value); - self.maxes.push(time, value); - self.times.push_back(time); - - // The duration-window eviction below stalls permanently when a - // device's capture clock is frozen or non-monotonic (duration_since - // yields Zero forever or None), and this meter lives for the whole - // process; without a hard cap it grows one entry per audio callback - // until quit. - const MAX_WINDOW_ENTRIES: usize = 512; - while self.times.len() > MAX_WINDOW_ENTRIES { - // A frozen clock produces duplicate keys that share one `maxes` - // entry; only drop that entry once no duplicate remains queued. - if let Some(front) = self.times.pop_front() - && !self.times.contains(&front) - { - self.maxes.remove(&front); - } - } - - while let Some(time) = self - .times - .back() - .unwrap() - .duration_since(self.times.front().unwrap()) - { - if time > self.keep_duration { - self.maxes.remove(self.times.front().unwrap()); - self.times.pop_front(); - } else { - break; - } - - if self.times.len() <= 1 { - break; - } - } - } - - pub fn max(&self) -> f64 { - -self.maxes.peek().map(|(_, db)| db.0).unwrap_or(-MIN_DB) - } -} - -#[derive(PartialEq)] -struct MinNonNan(f64); - -impl Eq for MinNonNan {} - -#[allow(clippy::non_canonical_partial_ord_impl)] -impl PartialOrd for MinNonNan { - fn partial_cmp(&self, other: &Self) -> Option { - other.0.partial_cmp(&self.0) - } -} - -impl Ord for MinNonNan { - fn cmp(&self, other: &MinNonNan) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - -use cpal::Sample; - -fn db_fs(data: impl Iterator) -> f64 { - let max = data - .map(|f| f.to_sample::().unsigned_abs()) - .max() - .unwrap_or(ZERO_AMPLITUDE); - - (20.0 * (max as f64 / MAX_AMPLITUDE_F32).log10()).clamp(MIN_DB, 0.0) -} - -fn samples_to_f64(samples: &MicrophoneSamples) -> impl Iterator + use<'_> { - samples - .data - .chunks(samples.format.sample_size()) - .map(|data| sample_to_f64(data, samples.format)) -} - -fn sample_to_f64(data: &[u8], format: SampleFormat) -> f64 { - match format { - SampleFormat::I8 => i8::from_ne_bytes([data[0]]) as f64 / i8::MAX as f64, - SampleFormat::U8 => u8::from_ne_bytes([data[0]]).to_sample::(), - SampleFormat::I16 => i16::from_ne_bytes([data[0], data[1]]) as f64 / i16::MAX as f64, - SampleFormat::U16 => u16::from_ne_bytes([data[0], data[1]]).to_sample::(), - SampleFormat::I32 => { - i32::from_ne_bytes([data[0], data[1], data[2], data[3]]) as f64 / i32::MAX as f64 - } - SampleFormat::U32 => { - u32::from_ne_bytes([data[0], data[1], data[2], data[3]]).to_sample::() - } - SampleFormat::U64 => u64::from_ne_bytes([ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]) - .to_sample::(), - SampleFormat::I64 => { - i64::from_ne_bytes([ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]) as f64 - / i64::MAX as f64 - } - SampleFormat::F32 => f32::from_ne_bytes([data[0], data[1], data[2], data[3]]) as f64, - SampleFormat::F64 => f64::from_ne_bytes([ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]), - _ => 0.0, - } -} - -#[cfg(test)] -mod sample_tests { - use super::*; - - #[test] - fn unsigned_equilibrium_is_silent() { - let cases = [ - (SampleFormat::U8, 128u8.to_ne_bytes().to_vec()), - (SampleFormat::U16, 32768u16.to_ne_bytes().to_vec()), - (SampleFormat::U32, (1u32 << 31).to_ne_bytes().to_vec()), - (SampleFormat::U64, (1u64 << 63).to_ne_bytes().to_vec()), - ]; - for (format, bytes) in cases { - assert_eq!(sample_to_f64(&bytes, format), 0.0, "{format:?}"); - } - } - - #[test] - fn unsigned_negative_peaks_are_full_scale() { - for format in [ - SampleFormat::U8, - SampleFormat::U16, - SampleFormat::U32, - SampleFormat::U64, - ] { - assert_eq!(sample_to_f64(&[0; 8], format), -1.0, "{format:?}"); - } - } - - #[test] - fn unsigned_samples_preserve_both_sides_of_equilibrium() { - assert_eq!(sample_to_f64(&[64], SampleFormat::U8), -0.5); - assert_eq!(sample_to_f64(&[192], SampleFormat::U8), 0.5); - } - - #[test] - fn unsigned_microphone_silence_has_minimum_level() { - assert_eq!( - db_fs(std::iter::once(sample_to_f64(&[128], SampleFormat::U8))), - MIN_DB - ); - assert_eq!( - db_fs(std::iter::once(sample_to_f64(&[0], SampleFormat::U8))), - 0.0 - ); - } - - #[test] - fn signed_and_float_samples_keep_existing_levels() { - let cases = [ - ( - SampleFormat::I8, - 63i8.to_ne_bytes().to_vec(), - 63.0 / i8::MAX as f64, - ), - ( - SampleFormat::I16, - 16383i16.to_ne_bytes().to_vec(), - 16383.0 / i16::MAX as f64, - ), - ( - SampleFormat::I32, - 123456i32.to_ne_bytes().to_vec(), - 123456.0 / i32::MAX as f64, - ), - ( - SampleFormat::I64, - 123456i64.to_ne_bytes().to_vec(), - 123456.0 / i64::MAX as f64, - ), - (SampleFormat::F32, 0.5f32.to_ne_bytes().to_vec(), 0.5), - (SampleFormat::F64, 0.5f64.to_ne_bytes().to_vec(), 0.5), - ]; - for (format, bytes, expected) in cases { - assert_eq!(sample_to_f64(&bytes, format), expected, "{format:?}"); - } - } -} diff --git a/apps/desktop/src-tauri/src/auth.rs b/apps/desktop/src-tauri/src/auth.rs deleted file mode 100644 index 5ae16506dba..00000000000 --- a/apps/desktop/src-tauri/src/auth.rs +++ /dev/null @@ -1,131 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::json; -use specta::Type; -use tauri::{AppHandle, Runtime}; -use tauri_plugin_store::StoreExt; - -use web_api::ManagerExt; - -use crate::{ - api::{self, Organization}, - web_api, -}; - -#[derive(Serialize, Deserialize, Type, Debug)] -pub struct AuthStore { - pub secret: AuthSecret, - pub user_id: Option, - pub plan: Option, - #[serde(default)] - pub organizations: Vec, - #[serde(default)] - pub organizations_updated_at: Option, -} - -#[derive(Serialize, Deserialize, Type, Debug)] -#[serde(untagged)] -pub enum AuthSecret { - ApiKey { api_key: String }, - Session { token: String, expires: i32 }, -} - -#[derive(Serialize, Deserialize, Type, Debug)] -pub struct Plan { - pub upgraded: bool, - pub manual: bool, - pub last_checked: i32, -} - -impl AuthStore { - pub fn load(app: &AppHandle) -> Result, String> { - let Some(store) = app - .store("store") - .map(|s| s.get("auth")) - .map_err(|e| e.to_string())? - else { - return Ok(None); - }; - - serde_json::from_value(store).map_err(|e| e.to_string()) - } - - pub fn get(app: &AppHandle) -> Result, String> { - let Ok(Some(store)) = app.store("store").map(|s| s.get("auth")) else { - return Ok(None); - }; - - serde_json::from_value(store).map_err(|e| e.to_string()) - } - - pub async fn update_auth_plan(app: &AppHandle) -> Result<(), String> { - let auth = Self::get(app)?; - let Some(auth) = auth else { - return Err("User not authenticated".to_string()); - }; - - if let Some(plan) = &auth.plan - && plan.manual - { - return Ok(()); - } - - let mut auth = auth; - - match app - .authed_api_request("/api/desktop/plan", |client, url| client.get(url)) - .await - { - Ok(response) if response.status().is_success() => { - #[derive(Deserialize)] - struct PlanResponse { - upgraded: bool, - } - match response.json::().await { - Ok(plan_response) => { - auth.plan = Some(Plan { - upgraded: plan_response.upgraded, - last_checked: chrono::Utc::now().timestamp() as i32, - manual: auth.plan.as_ref().is_some_and(|p| p.manual), - }); - } - Err(e) => tracing::warn!("Failed to parse plan response: {e}"), - } - } - Ok(response) => tracing::warn!("Plan fetch returned {}", response.status()), - Err(e) => tracing::warn!("Failed to fetch plan: {e}"), - } - - match api::fetch_organizations(app).await { - Ok(orgs) => { - auth.organizations = orgs; - auth.organizations_updated_at = Some(chrono::Utc::now().timestamp() as i32); - } - Err(e) => { - tracing::warn!("Failed to fetch organizations: {e}"); - if auth.organizations.is_empty() { - auth.organizations_updated_at = Some(chrono::Utc::now().timestamp() as i32); - } - } - } - - Self::set(app, Some(auth))?; - - Ok(()) - } - - pub fn is_upgraded(&self) -> bool { - match &self.plan { - Some(plan) => plan.upgraded || plan.manual, - None => false, - } - } - - pub fn set(app: &AppHandle, value: Option) -> Result<(), String> { - let Ok(store) = app.store("store") else { - return Err("Store not found".to_string()); - }; - - store.set("auth", json!(value)); - store.save().map_err(|e| e.to_string()) - } -} diff --git a/apps/desktop/src-tauri/src/automation.rs b/apps/desktop/src-tauri/src/automation.rs deleted file mode 100644 index dc58a5d92e5..00000000000 --- a/apps/desktop/src-tauri/src/automation.rs +++ /dev/null @@ -1,1022 +0,0 @@ -use cap_automation::{ - AutomationExportCompression, AutomationHost, AutomationRecordingMode, AutomationsStore, - Capability, CaptureTargetKind, ClipboardSource, ExportDestination, ExportFormat, ExportProfile, - Trigger, TriggerContext, sanitize_filename_component, -}; -use cap_recording::sources::screen_capture::ScreenCaptureTarget; -#[cfg(not(target_os = "linux"))] -use clipboard_rs::Clipboard; -use clipboard_rs::common::RustImage; -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}; - -use crate::ClipboardContext; -use crate::general_settings::PostStudioRecordingBehaviour; - -const WEBHOOK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); -const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300); - -#[derive(Debug, PartialEq, Eq)] -enum ClipboardImageSource { - File(PathBuf), - ScreenshotProject(PathBuf), -} - -fn resolve_clipboard_image_source( - ctx: &TriggerContext, - source: ClipboardSource, -) -> Result { - match source { - ClipboardSource::Raw => ctx - .image_path - .as_ref() - .or(ctx.output_path.as_ref()) - .cloned() - .map(ClipboardImageSource::File) - .ok_or_else(|| "No image or output path available for clipboard copy".to_string()), - ClipboardSource::Rendered => { - if let Some(path) = ctx.output_path.clone() { - return Ok(ClipboardImageSource::File(path)); - } - if ctx.image_path.is_some() - && let Some(path) = ctx.project_path.clone() - { - return Ok(ClipboardImageSource::ScreenshotProject(path)); - } - ctx.image_path - .clone() - .map(ClipboardImageSource::File) - .ok_or_else(|| "No rendered output available for clipboard copy".to_string()) - } - } -} - -pub struct DesktopAutomationHost { - app: AppHandle, - clipboard: Arc>, -} - -impl DesktopAutomationHost { - pub fn new(app: AppHandle, clipboard: Arc>) -> Self { - Self { app, clipboard } - } -} - -impl AutomationHost for DesktopAutomationHost { - fn capabilities(&self) -> &[Capability] { - &[ - Capability::CopyToClipboard, - Capability::SaveToLocation, - Capability::Export, - Capability::Upload, - Capability::RevealInFileManager, - Capability::OpenFile, - Capability::RunCommand, - Capability::Webhook, - Capability::Notify, - Capability::OpenEditor, - Capability::ApplyPreset, - Capability::DeleteLocalFiles, - #[cfg(any(target_os = "macos", target_os = "windows"))] - Capability::RecognizeText, - ] - } - - async fn copy_to_clipboard( - &self, - ctx: &TriggerContext, - source: &ClipboardSource, - ) -> Result<(), String> { - let source = resolve_clipboard_image_source(ctx, *source)?; - let img_data = match source { - ClipboardImageSource::File(path) => { - let path = path.to_string_lossy().to_string(); - info!(%path, "Automation: copying file to clipboard"); - clipboard_rs::RustImageData::from_path(&path) - .map_err(|e| format!("Failed to load image for clipboard: {e}"))? - } - ClipboardImageSource::ScreenshotProject(path) => { - info!(project = %path.display(), "Automation: rendering screenshot for clipboard"); - let rendered = crate::screenshot_editor::render_screenshot_project_for_export( - self.app.clone(), - path, - ) - .await?; - clipboard_rs::RustImageData::from_bytes(&rendered.image_bytes) - .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}"))? - } - }; - - self.clipboard - .write() - .await - .set_image(img_data) - .map_err(|e| format!("Failed to set clipboard image: {e}"))?; - - Ok(()) - } - - async fn save_to_location( - &self, - ctx: &TriggerContext, - dir: &str, - filename_template: Option<&str>, - ) -> Result<(), String> { - let src = ctx - .image_path - .as_ref() - .or(ctx.output_path.as_ref()) - .ok_or("No file path available for save")?; - - let filename = if let Some(tmpl) = filename_template { - apply_filename_template(tmpl, ctx) - } else { - src.file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "capture.png".to_string()) - }; - - let dst = PathBuf::from(dir).join(&filename); - info!(src = %src.display(), dst = %dst.display(), "Automation: saving to location"); - - if let Some(parent) = dst.parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|e| format!("Failed to create directory: {e}"))?; - } - - tokio::fs::copy(src, &dst) - .await - .map_err(|e| format!("Failed to copy file: {e}"))?; - - Ok(()) - } - - async fn export( - &self, - ctx: &TriggerContext, - profile: &ExportProfile, - destination: &ExportDestination, - ) -> Result<(), String> { - let project_path = ctx - .project_path - .as_ref() - .ok_or("No project path available for export")?; - - info!( - project = %project_path.display(), - format = ?profile.format, - "Automation: exporting" - ); - - let settings = build_desktop_export_settings(profile); - - let output_path = match destination { - ExportDestination::ProjectFolder => None, - ExportDestination::CustomPath { dir } => { - let ext = match profile.format { - ExportFormat::Mp4 => "mp4", - ExportFormat::Gif => "gif", - ExportFormat::Mov => "mov", - }; - let name = project_path - .file_stem() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_else(|| "export".to_string()); - Some(PathBuf::from(dir).join(format!("{name}.{ext}"))) - } - }; - - let mut builder = cap_export::ExporterBase::builder(project_path.clone()); - - if let Some(ref out) = output_path { - builder = builder.with_output_path(out.clone()); - } - - let base = builder.build().await.map_err(|e| format!("{e}"))?; - - let result_path = match settings { - crate::export::ExportSettings::Mp4(s) => s.export(base, |_| true).await, - crate::export::ExportSettings::Gif(s) => s.export(base, |_| true).await, - crate::export::ExportSettings::Mov(s) => s.export(base, |_| true).await, - } - .map_err(|e| format!("Export failed: {e}"))?; - - info!(output = %result_path.display(), "Automation: export complete"); - Ok(()) - } - - async fn upload( - &self, - ctx: &TriggerContext, - organization_id: Option<&str>, - copy_link: bool, - open_in_browser: bool, - ) -> Result<(), String> { - let link = if let Some(image_path) = ctx.image_path.as_ref() { - info!(path = %image_path.display(), "Automation: uploading screenshot"); - let meta = ctx - .project_path - .as_ref() - .or(Some(image_path)) - .and_then(|path| crate::load_screenshot_project_meta(path).ok()); - let existing_video_id = meta - .as_ref() - .and_then(|(_, meta)| meta.sharing.as_ref().map(|sharing| sharing.id.clone())); - let uploaded = crate::upload::upload_screenshot_file( - &self.app, - image_path.clone(), - existing_video_id, - organization_id.map(str::to_string), - ) - .await - .map_err(|e| format!("Upload failed: {e}"))?; - - if let Some((project_path, meta)) = meta { - let _ = crate::save_screenshot_sharing(&project_path, meta, &uploaded, None); - } - - uploaded.link - } else if let Some(existing) = ctx.share_link.as_ref() { - info!(link = %existing, "Automation: recording already uploaded, reusing existing link"); - 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 result = crate::upload_exported_video( - self.app.clone(), - project_path.clone(), - crate::UploadMode::Initial { - pre_created_video: None, - }, - channel, - organization_id.map(|s| s.to_string()), - ) - .await?; - - match result { - crate::UploadResult::Success(link) => link, - crate::UploadResult::NotAuthenticated => { - return Err("Not authenticated for upload".to_string()); - } - crate::UploadResult::UpgradeRequired => { - return Err("Upgrade required for upload".to_string()); - } - crate::UploadResult::PlanCheckFailed => { - return Err("Plan check failed for upload".to_string()); - } - } - } else { - return Err("No image or project path available for upload".to_string()); - }; - - if copy_link { - self.clipboard - .write() - .await - .set_text(link.clone()) - .map_err(|e| format!("Failed to copy link: {e}"))?; - } - - if open_in_browser { - let _ = crate::open_external_link(self.app.clone(), link.clone()); - } - - Ok(()) - } - - async fn reveal_in_file_manager(&self, ctx: &TriggerContext) -> Result<(), String> { - let path = ctx - .image_path - .as_ref() - .or(ctx.output_path.as_ref()) - .or(ctx.project_path.as_ref()) - .ok_or("No path available to reveal")?; - - reveal_path(path) - } - - async fn open_file(&self, ctx: &TriggerContext) -> Result<(), String> { - use tauri_plugin_opener::OpenerExt; - - let path = ctx - .image_path - .as_ref() - .or(ctx.output_path.as_ref()) - .ok_or("No file path available to open")?; - - 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}"))?; - - Ok(()) - } - - async fn run_command( - &self, - ctx: &TriggerContext, - program: &str, - args: &[String], - cwd: Option<&str>, - env: &HashMap, - use_shell: bool, - ) -> Result<(), String> { - info!(program, "Automation: running command"); - - let mut cmd = if use_shell { - let shell_line = cap_automation::shell_command_line(program, args); - - #[cfg(target_os = "windows")] - let mut c = tokio::process::Command::new("cmd"); - #[cfg(target_os = "windows")] - c.args(["/C", &shell_line]); - - #[cfg(not(target_os = "windows"))] - let mut c = tokio::process::Command::new("sh"); - #[cfg(not(target_os = "windows"))] - c.args(["-c", &shell_line]); - - c - } else { - let mut c = tokio::process::Command::new(program); - c.args(args); - c - }; - - if let Some(dir) = cwd { - cmd.current_dir(dir); - } - - for (k, v) in env { - cmd.env(k, v); - } - - if let Some(ref p) = ctx.project_path { - cmd.env("CAP_PROJECT_PATH", p); - } - if let Some(ref p) = ctx.image_path { - cmd.env("CAP_IMAGE_PATH", p); - } - if let Some(ref p) = ctx.output_path { - cmd.env("CAP_OUTPUT_PATH", p); - } - if let Some(ref l) = ctx.share_link { - cmd.env("CAP_SHARE_LINK", l); - } - - // Kill the child if the timeout drops the future, so a hung command can't outlive the run. - cmd.kill_on_drop(true); - let output = tokio::time::timeout(COMMAND_TIMEOUT, cmd.output()) - .await - .map_err(|_| format!("Command timed out after {}s", COMMAND_TIMEOUT.as_secs()))? - .map_err(|e| format!("Failed to run command: {e}"))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("Command exited with {}: {}", output.status, stderr)); - } - - Ok(()) - } - - async fn webhook( - &self, - ctx: &TriggerContext, - url: &str, - method: &str, - headers: &HashMap, - body_template: Option<&str>, - ) -> Result<(), String> { - info!(url, method, "Automation: sending webhook"); - - let client = reqwest::Client::builder() - .timeout(WEBHOOK_TIMEOUT) - .build() - .map_err(|e| format!("Failed to build HTTP client: {e}"))?; - let method = method - .parse::() - .map_err(|e| format!("Invalid HTTP method: {e}"))?; - - let body = if let Some(tmpl) = body_template { - apply_body_template(tmpl, ctx) - } else { - serde_json::to_string(&serde_json::json!({ - "project_path": ctx.project_path, - "image_path": ctx.image_path, - "output_path": ctx.output_path, - "share_link": ctx.share_link, - })) - .map_err(|e| format!("Failed to serialize webhook body: {e}"))? - }; - - let mut req = client.request(method, url).body(body); - for (k, v) in headers { - req = req.header(k, v); - } - - let resp = req - .send() - .await - .map_err(|e| format!("Webhook request failed: {e}"))?; - - if !resp.status().is_success() { - return Err(format!("Webhook returned status {}", resp.status())); - } - - Ok(()) - } - - async fn recognize_text_to_clipboard(&self, ctx: &TriggerContext) -> Result<(), String> { - let path = ctx - .image_path - .as_ref() - .or(ctx.output_path.as_ref()) - .ok_or("No image path available for OCR")?; - - info!(path = %path.display(), "Automation: recognizing text"); - - let text = crate::screenshot_editor::recognize_text_from_image_path(path).await?; - - if text.trim().is_empty() { - return Err("No text recognized in image".to_string()); - } - - self.clipboard - .write() - .await - .set_text(text) - .map_err(|e| format!("Failed to set clipboard text: {e}"))?; - - Ok(()) - } - - async fn notify( - &self, - ctx: &TriggerContext, - title_template: &str, - body_template: &str, - ) -> Result<(), String> { - #[cfg(not(target_os = "linux"))] - 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); - - if !enabled { - return Ok(()); - } - - let title = apply_body_template(&apply_filename_template(title_template, ctx), ctx); - let body = apply_body_template(&apply_filename_template(body_template, ctx), ctx); - - #[cfg(target_os = "linux")] - crate::notifications::show_linux_notification(&title, &body).await?; - - #[cfg(not(target_os = "linux"))] - self.app - .notification() - .builder() - .title(title) - .body(body) - .show() - .map_err(|e| format!("Failed to send notification: {e}"))?; - - Ok(()) - } - - async fn open_editor(&self, ctx: &TriggerContext) -> Result<(), String> { - if let Some(image_path) = ctx.image_path.as_ref() { - let _ = crate::windows::ShowCapWindow::ScreenshotEditor { - path: image_path.clone(), - } - .show(&self.app) - .await; - return Ok(()); - } - - let path = ctx - .project_path - .as_ref() - .ok_or("No project path for editor")?; - - let _ = crate::windows::ShowCapWindow::Editor { - project_path: path.clone(), - } - .show(&self.app) - .await; - - Ok(()) - } - - async fn apply_preset(&self, ctx: &TriggerContext, name: &str) -> Result<(), String> { - let project_path = ctx - .project_path - .as_ref() - .ok_or("No project path for preset")?; - - let preset = crate::presets::PresetsStore::get_by_name(&self.app, name)? - .ok_or_else(|| format!("Preset '{name}' not found"))?; - - info!(preset = name, project = %project_path.display(), "Automation: applying preset"); - - let existing_timeline = cap_project::ProjectConfiguration::load(project_path) - .ok() - .and_then(|c| c.timeline); - - let mut config = preset.config; - if config.timeline.is_none() { - config.timeline = existing_timeline; - } - - config - .write(project_path) - .map_err(|e| format!("Failed to write project config: {e}"))?; - - Ok(()) - } - - async fn delete_local_files(&self, ctx: &TriggerContext) -> Result<(), String> { - let path = ctx - .project_path - .as_ref() - .ok_or("No project path for deletion")?; - - info!(path = %path.display(), "Automation: deleting local files"); - tokio::fs::remove_dir_all(path) - .await - .map_err(|e| format!("Failed to delete: {e}"))?; - - Ok(()) - } -} - -fn reveal_path(path: &Path) -> Result<(), String> { - let path_str = path.to_str().ok_or("Invalid path")?; - - #[cfg(target_os = "macos")] - { - std::process::Command::new("open") - .arg("-R") - .arg(path_str) - .spawn() - .map_err(|e| format!("Failed to reveal: {e}"))?; - } - - #[cfg(target_os = "windows")] - { - std::process::Command::new("explorer") - .args(["/select,", path_str]) - .spawn() - .map_err(|e| format!("Failed to reveal: {e}"))?; - } - - #[cfg(target_os = "linux")] - { - let parent = path.parent().and_then(|p| p.to_str()).unwrap_or(path_str); - std::process::Command::new("xdg-open") - .arg(parent) - .spawn() - .map_err(|e| format!("Failed to reveal: {e}"))?; - } - - Ok(()) -} - -fn build_desktop_export_settings(profile: &ExportProfile) -> crate::export::ExportSettings { - let compression = profile - .compression - .map(|c| match c { - AutomationExportCompression::Maximum => cap_export::mp4::ExportCompression::Maximum, - AutomationExportCompression::Social => cap_export::mp4::ExportCompression::Social, - AutomationExportCompression::Web => cap_export::mp4::ExportCompression::Web, - AutomationExportCompression::Potato => cap_export::mp4::ExportCompression::Potato, - }) - .unwrap_or(cap_export::mp4::ExportCompression::Web); - - match profile.format { - ExportFormat::Mp4 => { - crate::export::ExportSettings::Mp4(cap_export::mp4::Mp4ExportSettings { - fps: profile.fps, - resolution_base: profile.resolution_base, - compression, - custom_bpp: None, - force_ffmpeg_decoder: false, - optimize_filesize: false, - }) - } - ExportFormat::Gif => { - crate::export::ExportSettings::Gif(cap_export::gif::GifExportSettings { - fps: profile.fps, - resolution_base: profile.resolution_base, - quality: None, - }) - } - ExportFormat::Mov => { - crate::export::ExportSettings::Mov(cap_export::mov::MovExportSettings { - fps: profile.fps, - resolution_base: profile.resolution_base, - cursor_only: false, - }) - } - } -} - -fn apply_filename_template(template: &str, ctx: &TriggerContext) -> String { - let now = chrono::Local::now(); - let mut result = template.to_string(); - result = result.replace("{date}", &now.format("%Y-%m-%d").to_string()); - result = result.replace("{time}", &now.format("%H-%M-%S").to_string()); - result = result.replace("{datetime}", &now.format("%Y-%m-%d_%H-%M-%S").to_string()); - if let Some(ref title) = ctx.window_title { - result = result.replace("{window}", &sanitize_filename_component(title)); - } - result -} - -fn apply_body_template(template: &str, ctx: &TriggerContext) -> String { - let mut result = template.to_string(); - if let Some(ref p) = ctx.project_path { - result = result.replace("{project_path}", &p.to_string_lossy()); - } - if let Some(ref p) = ctx.image_path { - result = result.replace("{image_path}", &p.to_string_lossy()); - } - if let Some(ref p) = ctx.output_path { - result = result.replace("{output_path}", &p.to_string_lossy()); - } - if let Some(ref l) = ctx.share_link { - result = result.replace("{share_link}", l); - } - result -} - -pub fn capture_target_kind(target: &ScreenCaptureTarget) -> Option { - match target { - ScreenCaptureTarget::Window { .. } => Some(CaptureTargetKind::Window), - ScreenCaptureTarget::Display { .. } => Some(CaptureTargetKind::Display), - ScreenCaptureTarget::Area { .. } => Some(CaptureTargetKind::Area), - ScreenCaptureTarget::CameraOnly => None, - } -} - -fn build_host(app: &AppHandle) -> Option { - let clipboard = app - .try_state::>>() - .map(|s| s.inner().clone())?; - Some(DesktopAutomationHost::new(app.clone(), clipboard)) -} - -pub async fn run_trigger(app: &AppHandle, trigger: Trigger, ctx: TriggerContext) { - let store = match get_store(app) { - Ok(Some(store)) => store, - Ok(None) => return, - Err(e) => { - error!("Failed to load automations store: {e}"); - return; - } - }; - - if store.rules.is_empty() { - return; - } - - let Some(host) = build_host(app) else { - warn!("Automation host unavailable (clipboard state missing)"); - return; - }; - - let results = cap_automation::run(&host, &store, &trigger, &ctx).await; - for result in &results { - for action in &result.action_results { - if let Some(error) = &action.error { - warn!( - rule_id = %result.rule_id, - error = %error, - "Automation action did not complete" - ); - } - } - } -} - -pub fn should_open_screenshot_editor(app: &AppHandle, target: &ScreenCaptureTarget) -> bool { - let store = match get_store(app) { - Ok(Some(store)) => store, - _ => return true, - }; - - if store.rules.is_empty() { - return true; - } - - let mut ctx = TriggerContext::new(); - if let Some(kind) = capture_target_kind(target) { - ctx = ctx.with_capture_target(kind); - } - if let Some(title) = target.title() { - ctx = ctx.with_window_title(title); - } - - !cap_automation::has_skip_editor(&store, &Trigger::ScreenshotTaken, &ctx) -} - -// `None` means a matching `SkipEditor` rule asked for a headless flow, so the caller must suppress -// both the editor and the recordings overlay. Automation rules win over the user's default behaviour. -pub fn studio_recording_editor_behaviour( - app: &AppHandle, - project_path: &Path, - duration_secs: f64, - default: PostStudioRecordingBehaviour, -) -> Option { - let store = match get_store(app) { - Ok(Some(store)) if !store.rules.is_empty() => store, - _ => return Some(default), - }; - - let mut ctx = TriggerContext::new() - .with_project_path(project_path.to_path_buf()) - .with_recording_mode(AutomationRecordingMode::Studio); - if duration_secs > 0.0 { - ctx = ctx.with_duration(duration_secs); - } - - if cap_automation::has_skip_editor(&store, &Trigger::StudioRecordingFinished, &ctx) { - None - } else if cap_automation::has_open_editor(&store, &Trigger::StudioRecordingFinished, &ctx) { - Some(PostStudioRecordingBehaviour::OpenEditor) - } else { - Some(default) - } -} - -pub fn run_screenshot_automations( - app: AppHandle, - image_path: PathBuf, - target: &ScreenCaptureTarget, -) { - let project_path = image_path.parent().map(Path::to_path_buf); - let capture_target = capture_target_kind(target); - let window_title = target.title(); - - tokio::spawn(async move { - let mut ctx = TriggerContext::new().with_image_path(image_path); - if let Some(project_path) = project_path { - ctx = ctx.with_project_path(project_path); - } - if let Some(kind) = capture_target { - ctx = ctx.with_capture_target(kind); - } - if let Some(title) = window_title { - ctx = ctx.with_window_title(title); - } - - run_trigger(&app, Trigger::ScreenshotTaken, ctx).await; - }); -} - -pub fn run_studio_recording_automations(app: AppHandle, project_path: PathBuf, duration_secs: f64) { - tokio::spawn(async move { - let mut ctx = TriggerContext::new() - .with_project_path(project_path) - .with_recording_mode(AutomationRecordingMode::Studio); - if duration_secs > 0.0 { - ctx = ctx.with_duration(duration_secs); - } - run_trigger(&app, Trigger::StudioRecordingFinished, ctx).await; - }); -} - -pub fn run_instant_recording_automations( - app: AppHandle, - project_path: PathBuf, - share_link: Option, - share_id: Option, -) { - tokio::spawn(async move { - let mut ctx = TriggerContext::new() - .with_project_path(project_path) - .with_recording_mode(AutomationRecordingMode::Instant); - if let Some(link) = share_link { - ctx = ctx.with_share_link(link); - } - if let Some(id) = share_id { - ctx = ctx.with_share_id(id); - } - run_trigger(&app, Trigger::InstantRecordingFinished, ctx).await; - }); -} - -pub fn run_upload_completed_automations( - app: AppHandle, - project_path: PathBuf, - share_link: Option, - share_id: Option, -) { - tokio::spawn(async move { - let mut ctx = TriggerContext::new().with_project_path(project_path); - if let Some(link) = share_link { - ctx = ctx.with_share_link(link); - } - if let Some(id) = share_id { - ctx = ctx.with_share_id(id); - } - run_trigger(&app, Trigger::UploadCompleted, ctx).await; - }); -} - -pub fn run_video_imported_automations(app: AppHandle, project_path: PathBuf) { - tokio::spawn(async move { - let ctx = TriggerContext::new().with_project_path(project_path); - run_trigger(&app, Trigger::VideoImported, ctx).await; - }); -} - -pub fn run_recording_started_automations(app: AppHandle) { - tokio::spawn(async move { - run_trigger(&app, Trigger::RecordingStarted, TriggerContext::new()).await; - }); -} - -pub fn run_recording_deleted_automations(app: AppHandle, project_path: PathBuf) { - tokio::spawn(async move { - let ctx = TriggerContext::new().with_project_path(project_path); - run_trigger(&app, Trigger::RecordingDeleted, ctx).await; - }); -} - -pub fn get_store(app: &AppHandle) -> Result, String> { - match app.store("store").map(|s| s.get("automations")) { - Ok(Some(store)) => match serde_json::from_value(store) { - Ok(settings) => Ok(Some(settings)), - Err(e) => { - error!("Failed to deserialize automations store: {e}"); - Ok(None) - } - }, - _ => Ok(None), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn get_automations(app: AppHandle) -> Result { - Ok(get_store(&app)?.unwrap_or_default()) -} - -#[tauri::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())?; - tauri_store.set("automations", json!(store)); - tauri_store.save().map_err(|e| e.to_string()) -} - -#[derive(serde::Serialize, specta::Type)] -#[serde(rename_all = "camelCase")] -pub struct AutomationActionCheck { - pub action_type: String, - pub capability: String, - pub supported: bool, -} - -#[derive(serde::Serialize, specta::Type)] -#[serde(rename_all = "camelCase")] -pub struct AutomationTestReport { - pub rule_id: String, - pub rule_name: String, - pub action_checks: Vec, -} - -#[tauri::command] -#[specta::specta] -pub async fn test_automation( - app: AppHandle, - rule_id: String, -) -> Result { - let store = get_store(&app)?.unwrap_or_default(); - let rule = store - .rules - .iter() - .find(|r| r.id == rule_id) - .ok_or_else(|| format!("Rule '{rule_id}' not found"))?; - - let clipboard = app - .try_state::>>() - .map(|s| s.inner().clone()); - - let supported: Vec = if let Some(clipboard) = clipboard { - let host = DesktopAutomationHost::new(app.clone(), clipboard); - host.capabilities().to_vec() - } else { - Vec::new() - }; - - let action_checks = rule - .actions - .iter() - .map(|action| { - let cap = action.required_capability(); - AutomationActionCheck { - action_type: format!("{action:?}") - .split_whitespace() - .next() - .unwrap_or("Unknown") - .to_string(), - capability: cap.map_or_else(|| "None".to_string(), |c| format!("{c:?}")), - supported: cap.is_none_or(|c| supported.contains(&c)), - } - }) - .collect(); - - Ok(AutomationTestReport { - rule_id: rule.id.clone(), - rule_name: rule.name.clone(), - action_checks, - }) -} - -#[tauri::command] -#[specta::specta] -pub async fn automation_should_open_screenshot_editor( - app: AppHandle, - target: ScreenCaptureTarget, -) -> bool { - should_open_screenshot_editor(&app, &target) -} - -#[tauri::command] -#[specta::specta] -pub async fn list_automation_capabilities() -> Vec { - vec![ - "CopyToClipboard".to_string(), - "SaveToLocation".to_string(), - "Export".to_string(), - "Upload".to_string(), - "RevealInFileManager".to_string(), - "OpenFile".to_string(), - "RunCommand".to_string(), - "Webhook".to_string(), - "RecognizeText".to_string(), - "Notify".to_string(), - "OpenEditor".to_string(), - "ApplyPreset".to_string(), - "DeleteLocalFiles".to_string(), - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rendered_screenshot_uses_project_instead_of_raw_capture() { - let ctx = TriggerContext::new() - .with_project_path(PathBuf::from("capture.cap")) - .with_image_path(PathBuf::from("capture.cap/original.png")); - - assert_eq!( - resolve_clipboard_image_source(&ctx, ClipboardSource::Rendered), - Ok(ClipboardImageSource::ScreenshotProject(PathBuf::from( - "capture.cap" - ))) - ); - } - - #[test] - fn rendered_output_takes_priority_over_screenshot_project() { - let ctx = TriggerContext::new() - .with_project_path(PathBuf::from("capture.cap")) - .with_image_path(PathBuf::from("capture.cap/original.png")) - .with_output_path(PathBuf::from("capture-rendered.png")); - - assert_eq!( - resolve_clipboard_image_source(&ctx, ClipboardSource::Rendered), - Ok(ClipboardImageSource::File(PathBuf::from( - "capture-rendered.png" - ))) - ); - } - - #[test] - fn raw_screenshot_keeps_original_capture() { - let ctx = TriggerContext::new() - .with_project_path(PathBuf::from("capture.cap")) - .with_image_path(PathBuf::from("capture.cap/original.png")); - - assert_eq!( - resolve_clipboard_image_source(&ctx, ClipboardSource::Raw), - Ok(ClipboardImageSource::File(PathBuf::from( - "capture.cap/original.png" - ))) - ); - } -} diff --git a/apps/desktop/src-tauri/src/camera.rs b/apps/desktop/src-tauri/src/camera.rs deleted file mode 100644 index 350bce064a6..00000000000 --- a/apps/desktop/src-tauri/src/camera.rs +++ /dev/null @@ -1,2126 +0,0 @@ -use anyhow::{Context, anyhow}; -#[cfg(not(target_os = "macos"))] -use cap_recording::FFmpegVideoFrame; -#[cfg(target_os = "macos")] -use cap_recording::NativeCameraFrame; -use cap_recording::feeds::{self, camera::CameraFeed}; -#[cfg(target_os = "macos")] -use cap_utils::macos_qos::{MacOsQosClass, set_current_thread_qos}; - -#[cfg(target_os = "macos")] -use crate::camera_native::{NativeFrameConverter, classify_frame}; -use ffmpeg::{ - format::{self, Pixel}, - frame, - software::scaling, -}; -use kameo::actor::ActorRef; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::{ - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, - thread, -}; -use tauri::{LogicalPosition, LogicalSize, Manager, PhysicalSize, WebviewWindow}; -use tokio::{ - runtime::Runtime, - sync::{broadcast, oneshot}, - task::LocalSet, - time::{Duration, Instant}, -}; -use tracing::{error, info, trace, warn}; -use wgpu::{CompositeAlphaMode, SurfaceTexture}; - -static TOOLBAR_HEIGHT: f32 = 56.0; - -const DEFAULT_SURFACE_SCALE: f64 = 2.0; -// Quantizing the target texture width stops drag-resizing from rebuilding the -// scaler, preview texture, and bind group on every 1px size change. -const CAMERA_PREVIEW_TEXTURE_WIDTH_BUCKET: u32 = 64; -const CAMERA_PREVIEW_MIN_TEXTURE_WIDTH: u32 = 320; -const CAMERA_PREVIEW_MAX_TEXTURE_WIDTH: u32 = 960; -const CAMERA_PREVIEW_MAX_TEXTURE_HEIGHT: u32 = 540; -const CAMERA_PREVIEW_BLUR_MAX_TEXTURE_WIDTH: u32 = 640; -const CAMERA_PREVIEW_BLUR_MAX_TEXTURE_HEIGHT: u32 = 360; -const CAMERA_PREVIEW_TARGET_FRAME_INTERVAL: Duration = Duration::from_micros(16_666); -const CAMERA_PREVIEW_FRAME_INTERVAL_SLACK: Duration = Duration::from_millis(1); -const CAMERA_PREVIEW_BLUR_INFERENCE_INTERVAL: Duration = Duration::from_millis(33); - -// ── Low-spec preview profile ──────────────────────────────────────────────── -// On low-RAM machines (e.g. an 8GB iMac) the full-quality preview is laggy, so -// we opt those machines into a cheaper preview: smaller textures, 30fps pacing, -// and (most importantly) NO background-blur init at all. This is gated by a -// single startup RAM check (see `init_preview_profile`); on every machine with -// more than the threshold, `is_low_spec_preview()` returns `false` and the -// preview path uses the high-spec constants above bit-for-bit, so there is zero -// behavioural change for >8GB machines. This affects ONLY the preview — the -// recording pipeline is entirely separate and untouched. -const CAMERA_PREVIEW_LOW_SPEC_MAX_TEXTURE_WIDTH: u32 = 640; -const CAMERA_PREVIEW_LOW_SPEC_MAX_TEXTURE_HEIGHT: u32 = 360; -const CAMERA_PREVIEW_LOW_SPEC_TARGET_FRAME_INTERVAL: Duration = Duration::from_micros(33_333); - -/// Machines with total RAM at or below this are treated as low-spec for preview. -pub const LOW_SPEC_PREVIEW_RAM_THRESHOLD_BYTES: u64 = 8 * 1024 * 1024 * 1024; - -static LOW_SPEC_PREVIEW: std::sync::OnceLock = std::sync::OnceLock::new(); - -/// Detect the preview profile once at startup from the machine's total RAM and -/// log the result. Subsequent calls are no-ops (the first value wins), so this -/// is safe to call from a single startup site. Reading via -/// `is_low_spec_preview()` never touches sysinfo, so there are no per-frame -/// system queries. -pub fn init_preview_profile(total_ram_bytes: u64) -> bool { - let low_spec = total_ram_bytes <= LOW_SPEC_PREVIEW_RAM_THRESHOLD_BYTES; - // Only the first set takes effect; ignore the (impossible in practice) - // racing second caller. - let _ = LOW_SPEC_PREVIEW.set(low_spec); - let active = *LOW_SPEC_PREVIEW.get().unwrap_or(&low_spec); - info!( - total_ram_mb = total_ram_bytes / 1_048_576, - low_spec_preview = active, - "Camera preview profile detected" - ); - active -} - -/// Whether the low-spec preview profile is active. Defaults to `false` (the -/// high-spec / current behaviour) if detection never ran, so any unexpected -/// ordering can only ever fall back to the existing >8GB behaviour. -#[inline] -pub fn is_low_spec_preview() -> bool { - *LOW_SPEC_PREVIEW.get().unwrap_or(&false) -} - -pub const MIN_CAMERA_SIZE: f32 = 150.0; -pub const MAX_CAMERA_SIZE: f32 = 600.0; -pub const DEFAULT_CAMERA_SIZE: f32 = 230.0; -pub const WIDE_CAMERA_ASPECT_RATIO: f32 = 16.0 / 9.0; - -// On macOS the preview consumes retained CMSampleBuffers and renders them via -// a zero-copy IOSurface->Metal->wgpu import; everywhere else it consumes the -// CPU-converted ffmpeg frames. -#[cfg(target_os = "macos")] -type PreviewCameraFrame = NativeCameraFrame; -#[cfg(not(target_os = "macos"))] -type PreviewCameraFrame = FFmpegVideoFrame; - -#[derive(Clone)] -pub enum CameraPreviewSender { - #[cfg(not(target_os = "macos"))] - Ffmpeg(flume::Sender), - #[cfg(target_os = "macos")] - Native(flume::Sender), -} - -impl CameraPreviewSender { - fn from_tx(tx: flume::Sender) -> Self { - #[cfg(target_os = "macos")] - { - Self::Native(tx) - } - #[cfg(not(target_os = "macos"))] - { - Self::Ffmpeg(tx) - } - } - - pub async fn attach(&self, feed: &ActorRef) -> Result<(), String> { - match self { - #[cfg(not(target_os = "macos"))] - Self::Ffmpeg(tx) => feed - .ask(feeds::camera::AddSender(tx.clone())) - .await - .map_err(|err| err.to_string()), - #[cfg(target_os = "macos")] - Self::Native(tx) => feed - .ask(feeds::camera::AddNativeSender(tx.clone())) - .await - .map_err(|err| err.to_string()), - } - } - - pub async fn detach(&self, feed: &ActorRef) -> Result<(), String> { - match self { - #[cfg(not(target_os = "macos"))] - Self::Ffmpeg(tx) => feed - .ask(feeds::camera::RemoveSender(tx.clone())) - .await - .map_err(|err| err.to_string()), - #[cfg(target_os = "macos")] - Self::Native(tx) => feed - .ask(feeds::camera::RemoveNativeSender(tx.clone())) - .await - .map_err(|err| err.to_string()), - } - } -} - -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Type)] -#[serde(rename_all = "lowercase")] -pub enum CameraPreviewShape { - #[default] - Round, - Square, - Full, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)] -pub struct CameraPreviewState { - pub size: f32, - pub shape: CameraPreviewShape, - pub mirrored: bool, - #[serde(default)] - pub background_blur: cap_project::BackgroundBlurMode, -} - -impl Default for CameraPreviewState { - fn default() -> Self { - Self { - size: DEFAULT_CAMERA_SIZE, - shape: CameraPreviewShape::default(), - mirrored: false, - background_blur: cap_project::BackgroundBlurMode::Off, - } - } -} - -fn clamp_size(size: f32) -> f32 { - size.clamp(MIN_CAMERA_SIZE, MAX_CAMERA_SIZE) -} - -fn preferred_alpha_mode(alpha_modes: &[CompositeAlphaMode]) -> CompositeAlphaMode { - [ - CompositeAlphaMode::PreMultiplied, - CompositeAlphaMode::PostMultiplied, - CompositeAlphaMode::Opaque, - CompositeAlphaMode::Auto, - CompositeAlphaMode::Inherit, - ] - .into_iter() - .find(|mode| alpha_modes.contains(mode)) - .or_else(|| alpha_modes.first().copied()) - .unwrap_or(CompositeAlphaMode::Opaque) -} - -fn camera_preview_frame_due(last_render_at: Option, now: Instant) -> bool { - // High-spec keeps the original 60fps target; low-spec paces at 30fps. - let target_interval = if is_low_spec_preview() { - CAMERA_PREVIEW_LOW_SPEC_TARGET_FRAME_INTERVAL - } else { - CAMERA_PREVIEW_TARGET_FRAME_INTERVAL - }; - last_render_at.is_none_or(|last| { - now.saturating_duration_since(last) + CAMERA_PREVIEW_FRAME_INTERVAL_SLACK >= target_interval - }) -} - -fn camera_preview_texture_dimensions( - source_width: u32, - source_height: u32, - region_width: u32, - region_height: u32, - blur_enabled: bool, -) -> (u32, u32) { - let source_width = source_width.max(1); - let source_height = source_height.max(1); - let region_width = region_width.max(1); - let region_height = region_height.max(1); - let (max_width, max_height) = if is_low_spec_preview() { - // Low-spec caps preview to 640x360 regardless of blur (blur is skipped - // entirely on low-spec, so `blur_enabled` is effectively always false - // here, but we clamp unconditionally to stay safe if that ever changes). - ( - CAMERA_PREVIEW_LOW_SPEC_MAX_TEXTURE_WIDTH, - CAMERA_PREVIEW_LOW_SPEC_MAX_TEXTURE_HEIGHT, - ) - } else if blur_enabled { - ( - CAMERA_PREVIEW_BLUR_MAX_TEXTURE_WIDTH, - CAMERA_PREVIEW_BLUR_MAX_TEXTURE_HEIGHT, - ) - } else { - ( - CAMERA_PREVIEW_MAX_TEXTURE_WIDTH, - CAMERA_PREVIEW_MAX_TEXTURE_HEIGHT, - ) - }; - let source_aspect = source_width as f64 / source_height as f64; - let cover_width = (region_width as f64).max(region_height as f64 * source_aspect); - let requested_width = (cover_width.ceil() as u32) - .max(CAMERA_PREVIEW_MIN_TEXTURE_WIDTH) - .div_ceil(CAMERA_PREVIEW_TEXTURE_WIDTH_BUCKET) - .saturating_mul(CAMERA_PREVIEW_TEXTURE_WIDTH_BUCKET) - .min(max_width); - let scale = (requested_width as f64 / source_width as f64) - .min(max_height as f64 / source_height as f64) - .min(1.0); - let target_width = ((source_width as f64 * scale).round() as u32).max(1); - let target_height = ((source_height as f64 * scale).round() as u32).max(1); - (target_width, target_height) -} - -pub struct CameraPreviewManager { - store: Result>, String>, - store_save_generation: Arc, - preview: Option, - preview_session_id: Arc, - wgpu_instance: wgpu::Instance, -} - -impl CameraPreviewManager { - pub fn new(app: &tauri::AppHandle) -> Self { - Self { - store: tauri_plugin_store::StoreBuilder::new(app, "cameraPreview") - .build() - .map_err(|err| format!("Error initializing camera preview store: {err}")), - store_save_generation: Arc::new(AtomicU64::new(0)), - preview: None, - preview_session_id: Arc::new(AtomicU64::new(0)), - wgpu_instance: cap_rendering::create_wgpu_instance_sync(), - } - } - - pub fn session_id_handle(&self) -> Arc { - self.preview_session_id.clone() - } - - pub fn get_state(&self) -> anyhow::Result { - let mut state: CameraPreviewState = self - .store - .as_ref() - .map_err(|err| anyhow!("{err}"))? - .get("state") - .and_then(|v| serde_json::from_value(v).ok()) - .unwrap_or_default(); - - state.size = clamp_size(state.size); - Ok(state) - } - - pub fn set_state(&self, mut state: CameraPreviewState) -> anyhow::Result<()> { - state.size = clamp_size(state.size); - - let store = self.store.as_ref().map_err(|err| anyhow!("{err}"))?; - store.set("state", serde_json::to_value(&state)?); - - // set_state fires per mousemove during a size drag; debounce the disk - // write so only the post-drag state is persisted. - 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 { - tokio::time::sleep(Duration::from_millis(150)).await; - if save_generation.load(Ordering::Acquire) == generation - && let Err(err) = store.save() - { - error!("Error saving camera preview store: {err}"); - } - }); - - if let Some(preview) = &self.preview { - preview - .reconfigure - .send(ReconfigureEvent::State(state)) - .map_err(|err| error!("Error asking camera preview to reconfigure: {err}")) - .ok(); - } - - Ok(()) - } - - pub fn is_initialized(&self) -> bool { - self.preview.is_some() - } - - pub fn is_paused(&self) -> bool { - self.preview - .as_ref() - .is_some_and(|preview| preview.is_paused) - } - - pub fn sender(&self) -> Option { - self.preview - .as_ref() - .map(|p| CameraPreviewSender::from_tx(p.camera_tx.clone())) - } - - pub fn notify_window_resized(&self, width: u32, height: u32) { - if let Some(preview) = &self.preview { - preview - .reconfigure - .send(ReconfigureEvent::WindowResized { width, height }) - .map_err(|err| error!("Error notifying camera preview of resize: {err}")) - .ok(); - } - } - - // Pauses the camera preview, hiding the window but keeping GPU resources alive. - // Called when the user deselects the camera. This is part of the persistent - // renderer architecture that prevents crashes from repeated resource creation. - pub fn pause(&mut self) { - if let Some(preview) = &mut self.preview - && !preview.is_paused - { - preview.is_paused = true; - preview - .reconfigure - .send(ReconfigureEvent::Pause) - .map_err(|err| error!("Error sending camera preview pause event: {err}")) - .ok(); - } - } - - pub fn begin_shutdown(&mut self) -> Option> { - let preview = self.preview.take()?; - info!( - "Camera preview shutdown requested (session {}).", - preview.session_id - ); - preview - .reconfigure - .send(ReconfigureEvent::Shutdown) - .map_err(|err| error!("Error sending camera preview shutdown event: {err}")) - .ok(); - Some(preview.shutdown_complete) - } - - pub async fn init_window( - &mut self, - window: WebviewWindow, - actor: ActorRef, - ) -> anyhow::Result<()> { - let reveal_generation = crate::clean_capture::generation(window.app_handle()); - if let Some(preview) = &mut self.preview { - CameraPreviewSender::from_tx(preview.camera_tx.clone()) - .attach(&actor) - .await - .map_err(|err| anyhow!("Error re-attaching camera feed consumer: {err}"))?; - - if preview.is_paused { - preview.is_paused = false; - preview - .reconfigure - .send(ReconfigureEvent::Resume) - .map_err(|err| error!("Error sending camera preview resume event: {err}")) - .ok(); - } - - window - .run_on_main_thread({ - let window = window.clone(); - move || { - let _ = crate::clean_capture::reveal_now(&window, reveal_generation); - } - }) - .ok(); - - return Ok(()); - } - - let session_id = self.preview_session_id.fetch_add(1, Ordering::AcqRel) + 1; - - let (camera_tx, camera_rx) = flume::bounded::(1); - - CameraPreviewSender::from_tx(camera_tx.clone()) - .attach(&actor) - .await - .map_err(|err| anyhow!("Error attaching camera feed consumer: {err}"))?; - - let default_state = self - .get_state() - .map_err(|err| error!("Error getting camera preview state: {err}")) - .unwrap_or_default(); - - // Capacity must absorb bursts of control events (State spam during a - // size drag interleaved with Pause/Resume); a lagged capacity-1 channel - // could drop a Resume and leave the preview stuck hidden. - let (reconfigure, reconfigure_rx) = broadcast::channel(8); - let mut renderer = InitializedCameraPreview::init_wgpu( - window.clone(), - &default_state, - self.wgpu_instance.clone(), - ) - .await?; - window - .run_on_main_thread({ - let window = window.clone(); - move || { - let _ = crate::clean_capture::reveal_now(&window, reveal_generation); - } - }) - .ok(); - - let rt = Runtime::new().context("Failed to create camera preview runtime")?; - let (shutdown_complete_tx, shutdown_complete_rx) = oneshot::channel(); - - self.preview = Some(InitializedCameraPreview { - reconfigure, - session_id, - shutdown_complete: shutdown_complete_rx, - camera_tx, - is_paused: false, - }); - - thread::spawn(move || { - #[cfg(target_os = "macos")] - { - let result = set_current_thread_qos(MacOsQosClass::UserInteractive); - if result != 0 { - warn!(result, "pthread_set_qos_class_self_np failed"); - } - } - LocalSet::new().block_on( - &rt, - renderer.run(window.clone(), default_state, reconfigure_rx, camera_rx), - ); - - let (drop_tx, drop_rx) = oneshot::channel(); - let renderer = Box::new(renderer); - let _ = window.run_on_main_thread(move || { - drop(renderer); - let _ = drop_tx.send(()); - }); - - wait_for_shutdown_signal(&rt, drop_rx, Duration::from_millis(250)); - - shutdown_complete_tx.send(()).ok(); - info!("DONE"); - }); - - Ok(()) - } -} - -fn wait_for_shutdown_signal(runtime: &Runtime, receiver: oneshot::Receiver<()>, timeout: Duration) { - runtime.block_on(async move { - let _ = tokio::time::timeout(timeout, receiver).await; - }); -} - -// Internal events for the persistent camera renderer architecture. -// -// The camera preview uses a persistent WGPU renderer that stays alive across -// camera deselect/reselect cycles. This avoids repeated GPU resource creation -// which, combined with macOS panel.order_front_regardless() calls, was causing -// crashes after ~4-5 toggle cycles. -// -// Pause/Resume hide/show the window while keeping GPU resources alive. -// See the comment in windows.rs ShowCapWindow::Camera for the critical detail -// about avoiding order_front_regardless(). -#[derive(Clone)] -enum ReconfigureEvent { - State(CameraPreviewState), - WindowResized { width: u32, height: u32 }, - Pause, - Resume, - Shutdown, -} - -struct InitializedCameraPreview { - reconfigure: broadcast::Sender, - session_id: u64, - shutdown_complete: oneshot::Receiver<()>, - camera_tx: flume::Sender, - is_paused: bool, -} - -impl InitializedCameraPreview { - async fn init_wgpu( - window: WebviewWindow, - default_state: &CameraPreviewState, - instance: wgpu::Instance, - ) -> anyhow::Result { - 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 (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 (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 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 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::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 pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[&bind_group_layout, &uniform_bind_group_layout], - push_constant_ranges: &[], - }); - - 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 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, - }; - - 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 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); - - 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); - - 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(); - } - - Ok(renderer) - } -} - -struct Renderer { - surface: Option>, - surface_config: wgpu::SurfaceConfiguration, - surface_scale: f64, - render_pipeline: wgpu::RenderPipeline, - device: wgpu::Device, - queue: wgpu::Queue, - sampler: wgpu::Sampler, - bind_group_layout: wgpu::BindGroupLayout, - state_uniform_buffer: wgpu::Buffer, - window_uniform_buffer: wgpu::Buffer, - camera_uniform_buffer: wgpu::Buffer, - uniform_bind_group: wgpu::BindGroup, - texture: Cached<(u32, u32), PreparedTexture>, - aspect_ratio: Cached, - blur_processor: Option, - blur_processor_init_attempted: bool, - blur_source_texture: Option, - #[cfg(target_os = "macos")] - native_converter: Option, - #[cfg(target_os = "macos")] - native_converter_init_attempted: bool, -} - -impl Renderer { - async fn run( - &mut self, - window: WebviewWindow, - default_state: CameraPreviewState, - mut reconfigure: broadcast::Receiver, - camera_rx: flume::Receiver, - ) { - let mut resampler_frame = Cached::default(); - let Ok(mut scaler) = scaling::Context::get( - Pixel::RGBA, - 1, - 1, - Pixel::RGBA, - 1, - 1, - scaling::Flags::empty(), - ) - .map_err(|err| error!("Error initializing ffmpeg scaler: {err:?}")) else { - return; - }; - let mut source_dimensions = Cached::default(); - let mut last_render_at = None; - - // The camera's pixel buffer pool only recycles an IOSurface once its - // CVPixelBuffer is released; holding the last couple of sample buffers - // keeps the camera from writing into a surface the GPU is still - // sampling. - #[cfg(target_os = "macos")] - let mut inflight_frames: std::collections::VecDeque = - std::collections::VecDeque::new(); - #[cfg(target_os = "macos")] - let mut native_fallback_logged = false; - - let start_time = Instant::now(); - let startup_timeout = Duration::from_secs(5); - let mut received_first_frame = false; - let mut is_paused = false; - - let mut state = default_state; - let mut needs_full_reconfigure = false; - - let pause_and_hide = || { - window - .run_on_main_thread({ - let window = window.clone(); - move || { - let _ = window.hide(); - } - }) - .ok(); - }; - - 'main_loop: loop { - if is_paused { - loop { - match reconfigure.recv().await { - Ok(ReconfigureEvent::Resume) => { - is_paused = false; - while camera_rx.try_recv().is_ok() {} - #[cfg(target_os = "macos")] - inflight_frames.clear(); - if let Some(texture) = self.acquire_surface_texture() { - let (buffer, stride) = - render_solid_frame([0x11, 0x11, 0x11, 0xFF], 5, 5); - PreparedTexture::init( - self.device.clone(), - self.queue.clone(), - &self.sampler, - &self.bind_group_layout, - self.uniform_bind_group.clone(), - self.render_pipeline.clone(), - 5, - 5, - ) - .render(&texture, &buffer, stride); - texture.present(); - } - break; - } - Ok(ReconfigureEvent::Shutdown) => { - self.cleanup_for_shutdown(&window).await; - return; - } - Ok(ReconfigureEvent::State(new_state)) => { - state = new_state; - needs_full_reconfigure = true; - } - Ok(ReconfigureEvent::WindowResized { width, height }) => { - self.reconfigure_gpu_surface(width, height); - } - Ok(ReconfigureEvent::Pause) => {} - Err(broadcast::error::RecvError::Lagged(_)) => { - continue; - } - Err(broadcast::error::RecvError::Closed) => { - self.cleanup_for_shutdown(&window).await; - return; - } - } - } - } - - if needs_full_reconfigure { - needs_full_reconfigure = false; - self.update_state_uniforms(&state); - if state.background_blur == cap_project::BackgroundBlurMode::Off { - self.release_blur_resources(); - } - source_dimensions = Cached::default(); - last_render_at = None; - let aspect_ratio = self.aspect_ratio.get_latest_key().copied().unwrap_or( - if state.shape == CameraPreviewShape::Full { - WIDE_CAMERA_ASPECT_RATIO - } else { - 1.0 - }, - ); - self.aspect_ratio = Cached::default(); - if let Ok((width, height, scale)) = - resize_window(&window, &state, aspect_ratio, false) - .await - .map_err(|err| { - error!("Error resizing camera preview window after resume: {err}") - }) - { - self.surface_scale = scale; - self.reconfigure_gpu_surface(width, height); - } - } - - let frame_timeout = if received_first_frame { - Duration::from_secs(5) - } else if start_time.elapsed() < startup_timeout { - startup_timeout.saturating_sub(start_time.elapsed()) - } else { - Duration::from_secs(1) - }; - - let event = loop { - if frame_timeout.is_zero() { - warn!("Camera preview timed out waiting for first frame, waiting for recovery"); - tokio::time::sleep(Duration::from_secs(1)).await; - continue 'main_loop; - } - - tokio::select! { - frame = camera_rx.recv_async() => { - match frame { - Ok(f) => break Ok(f), - Err(_) => { - is_paused = true; - pause_and_hide(); - continue 'main_loop; - } - } - }, - result = reconfigure.recv() => { - match result { - Ok(result) => break Err(result), - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => { - break Err(ReconfigureEvent::Shutdown); - } - } - }, - _ = tokio::time::sleep(frame_timeout) => { - if received_first_frame { - warn!("Camera preview: frames stalled, waiting for feed refresh"); - continue; - } - warn!("Camera preview: no frames received within startup timeout, waiting for recovery"); - continue 'main_loop; - } - } - }; - - match event { - Ok(mut frame) => { - while let Ok(newer) = camera_rx.try_recv() { - frame = newer; - } - - received_first_frame = true; - let now = Instant::now(); - if !camera_preview_frame_due(last_render_at, now) { - continue 'main_loop; - } - last_render_at = Some(now); - - #[cfg(target_os = "macos")] - let (source_width, source_height) = { - let Some(image_buf) = frame.sample_buf.image_buf() else { - continue 'main_loop; - }; - (image_buf.width() as u32, image_buf.height() as u32) - }; - #[cfg(not(target_os = "macos"))] - let (source_width, source_height) = (frame.inner.width(), frame.inner.height()); - - let aspect_ratio = source_width as f32 / source_height as f32; - if source_dimensions.update_key_and_should_init((source_width, source_height)) { - self.sync_ratio_uniform_and_resize_window_to_it( - &window, - &state, - aspect_ratio, - ) - .await; - } - - let surface_result = self.acquire_surface_texture(); - if let Some(surface) = surface_result { - let surface_scale = self.surface_scale.max(1.0); - let toolbar_px = (TOOLBAR_HEIGHT as f64 * surface_scale).round() as u32; - let region_width = self.surface_config.width.max(1); - let region_height = - self.surface_config.height.saturating_sub(toolbar_px).max(1); - let blur_mode = blur_mode_from_project(state.background_blur); - let (output_width, output_height) = camera_preview_texture_dimensions( - source_width, - source_height, - region_width, - region_height, - blur_mode.is_some(), - ); - - #[cfg(target_os = "macos")] - { - match self.render_native_frame( - &frame, - output_width, - output_height, - blur_mode, - &surface, - ) { - Ok(()) => { - surface.present(); - inflight_frames.push_back(frame); - while inflight_frames.len() > 2 { - inflight_frames.pop_front(); - } - } - Err(err) => { - if !native_fallback_logged { - native_fallback_logged = true; - warn!( - "Camera GPU-native preview unavailable ({err}); using CPU conversion" - ); - } - match cap_camera_ffmpeg::sample_buf_as_ffmpeg(&frame.sample_buf) - { - Ok(inner) => { - if self.render_cpu_frame( - &inner, - output_width, - output_height, - blur_mode, - &surface, - &mut scaler, - &mut resampler_frame, - ) { - surface.present(); - } - } - Err(err) => { - error!( - "Camera preview CPU fallback conversion failed: {err}" - ); - } - } - } - } - } - #[cfg(not(target_os = "macos"))] - { - if self.render_cpu_frame( - &frame.inner, - output_width, - output_height, - blur_mode, - &surface, - &mut scaler, - &mut resampler_frame, - ) { - surface.present(); - } - } - } - } - Err(ReconfigureEvent::State(new_state)) => { - trace!("CameraPreview/ReconfigureEvent.State({new_state:?})"); - - state = new_state; - - let aspect_ratio = self.aspect_ratio.get_latest_key().copied().unwrap_or( - if state.shape == CameraPreviewShape::Full { - WIDE_CAMERA_ASPECT_RATIO - } else { - 1.0 - }, - ); - - self.sync_ratio_uniform_and_resize_window_to_it(&window, &state, aspect_ratio) - .await; - self.update_state_uniforms(&state); - if state.background_blur == cap_project::BackgroundBlurMode::Off { - self.release_blur_resources(); - } - if let Ok((width, height, scale)) = - resize_window(&window, &state, aspect_ratio, false) - .await - .map_err(|err| error!("Error resizing camera preview window: {err}")) - { - self.surface_scale = scale; - self.reconfigure_gpu_surface(width, height); - } - } - Err(ReconfigureEvent::WindowResized { width, height }) => { - trace!("CameraPreview/ReconfigureEvent.WindowResized({width}x{height})"); - self.reconfigure_gpu_surface(width, height); - } - Err(ReconfigureEvent::Pause) => { - // When pausing, we hide the window to provide proper UX feedback. - // The WGPU Surface and Device are kept alive to avoid repeated GPU - // resource creation, which previously contributed to crashes. - // - // The corresponding show operation in windows.rs uses window.show() - // instead of panel.order_front_regardless() - see the comment there - // for why this is critical to avoid macOS crashes. - is_paused = true; - #[cfg(target_os = "macos")] - inflight_frames.clear(); - window - .run_on_main_thread({ - let window = window.clone(); - move || { - let _ = window.hide(); - } - }) - .ok(); - } - Err(ReconfigureEvent::Resume) => { - while camera_rx.try_recv().is_ok() {} - #[cfg(target_os = "macos")] - inflight_frames.clear(); - if let Some(texture) = self.acquire_surface_texture() { - let (buffer, stride) = render_solid_frame([0x11, 0x11, 0x11, 0xFF], 5, 5); - PreparedTexture::init( - self.device.clone(), - self.queue.clone(), - &self.sampler, - &self.bind_group_layout, - self.uniform_bind_group.clone(), - self.render_pipeline.clone(), - 5, - 5, - ) - .render(&texture, &buffer, stride); - texture.present(); - } - } - Err(ReconfigureEvent::Shutdown) => { - self.cleanup_for_shutdown(&window).await; - return; - } - } - } - } - - #[allow(clippy::too_many_arguments)] - fn render_cpu_frame( - &mut self, - inner: &frame::Video, - output_width: u32, - output_height: u32, - blur_mode: Option, - surface: &SurfaceTexture, - scaler: &mut scaling::Context, - resampler_frame: &mut Cached<(u32, u32), frame::Video>, - ) -> bool { - let source_width = inner.width(); - let source_height = inner.height(); - - let already_preview_rgba = inner.format() == Pixel::RGBA - && output_width == source_width - && output_height == source_height; - let (frame_data, frame_stride) = if already_preview_rgba { - (inner.data(0), inner.stride(0) as u32) - } else { - let resampler_frame = - resampler_frame.get_or_init((output_width, output_height), frame::Video::empty); - - scaler.cached( - inner.format(), - source_width, - source_height, - format::Pixel::RGBA, - output_width, - output_height, - ffmpeg::software::scaling::flag::Flags::FAST_BILINEAR, - ); - - if let Err(err) = scaler.run(inner, resampler_frame) { - error!("Error rescaling frame with ffmpeg: {err:?}"); - return false; - } - - (resampler_frame.data(0), resampler_frame.stride(0) as u32) - }; - - let blurred = if let Some(mode) = blur_mode { - self.run_background_blur(frame_data, frame_stride, output_width, output_height, mode) - } else { - false - }; - - let prepared = self.texture.get_or_init((output_width, output_height), || { - PreparedTexture::init( - self.device.clone(), - self.queue.clone(), - &self.sampler, - &self.bind_group_layout, - self.uniform_bind_group.clone(), - self.render_pipeline.clone(), - output_width, - output_height, - ) - }); - - if blurred { - let Some(blur_output) = self - .blur_processor - .as_mut() - .and_then(|p| p.process_returning_output()) - else { - prepared.render(surface, frame_data, frame_stride); - return true; - }; - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Blur Copy"), - }); - encoder.copy_texture_to_texture( - wgpu::TexelCopyTextureInfo { - texture: blur_output, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyTextureInfo { - texture: &prepared.texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::Extent3d { - width: output_width, - height: output_height, - depth_or_array_layers: 1, - }, - ); - self.queue.submit(std::iter::once(encoder.finish())); - prepared.render_no_upload(surface); - } else { - prepared.render(surface, frame_data, frame_stride); - } - true - } - - #[cfg(target_os = "macos")] - fn ensure_native_converter(&mut self) -> bool { - if self.native_converter.is_some() { - return true; - } - if self.native_converter_init_attempted { - return false; - } - self.native_converter_init_attempted = true; - self.native_converter = NativeFrameConverter::new(&self.device); - if self.native_converter.is_some() { - info!("Camera GPU-native frame converter initialized"); - true - } else { - warn!("Failed to initialize camera GPU-native frame converter"); - false - } - } - - #[cfg(target_os = "macos")] - fn render_native_frame( - &mut self, - frame: &NativeCameraFrame, - output_width: u32, - output_height: u32, - blur_mode: Option, - surface: &SurfaceTexture, - ) -> Result<(), String> { - let kind = classify_frame(frame).map_err(|err| err.to_string())?; - if !self.ensure_native_converter() { - return Err("GPU-native converter unavailable".to_string()); - } - - let use_blur = blur_mode.is_some() && self.ensure_blur_processor(); - - if use_blur { - self.ensure_blur_source_texture(output_width, output_height); - let src_tex = self.blur_source_texture.as_ref().expect("just ensured"); - let dst_view = src_tex.create_view(&wgpu::TextureViewDescriptor::default()); - self.native_converter - .as_ref() - .expect("ensured above") - .render_frame(&self.device, &self.queue, frame, &kind, &dst_view) - .map_err(|err| err.to_string())?; - - if let (Some(mode), Some(processor)) = (blur_mode, self.blur_processor.as_mut()) { - processor.process(&self.device, &self.queue, src_tex, mode); - } - - let prepared = self.texture.get_or_init((output_width, output_height), || { - PreparedTexture::init( - self.device.clone(), - self.queue.clone(), - &self.sampler, - &self.bind_group_layout, - self.uniform_bind_group.clone(), - self.render_pipeline.clone(), - output_width, - output_height, - ) - }); - - let blur_output = self - .blur_processor - .as_mut() - .and_then(|p| p.process_returning_output()) - .ok_or_else(|| "blur output missing".to_string())?; - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Blur Copy"), - }); - encoder.copy_texture_to_texture( - wgpu::TexelCopyTextureInfo { - texture: blur_output, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyTextureInfo { - texture: &prepared.texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::Extent3d { - width: output_width, - height: output_height, - depth_or_array_layers: 1, - }, - ); - self.queue.submit(std::iter::once(encoder.finish())); - prepared.render_no_upload(surface); - } else { - let prepared = self.texture.get_or_init((output_width, output_height), || { - PreparedTexture::init( - self.device.clone(), - self.queue.clone(), - &self.sampler, - &self.bind_group_layout, - self.uniform_bind_group.clone(), - self.render_pipeline.clone(), - output_width, - output_height, - ) - }); - let dst_view = prepared - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - self.native_converter - .as_ref() - .expect("ensured above") - .render_frame(&self.device, &self.queue, frame, &kind, &dst_view) - .map_err(|err| err.to_string())?; - prepared.render_no_upload(surface); - } - - Ok(()) - } - - fn run_background_blur( - &mut self, - frame_data: &[u8], - frame_stride: u32, - width: u32, - height: u32, - mode: cap_camera_effects::BlurMode, - ) -> bool { - if !self.ensure_blur_processor() { - return false; - } - - self.ensure_blur_source_texture(width, height); - - let (Some(src_tex), Some(processor)) = ( - self.blur_source_texture.as_ref(), - self.blur_processor.as_mut(), - ) else { - return false; - }; - - self.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: src_tex, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - frame_data, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(frame_stride), - rows_per_image: Some(height), - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - - processor.process(&self.device, &self.queue, src_tex, mode); - true - } - - // The blur processor owns an ONNX session plus several GPU textures, so it - // is dropped as soon as blur is switched off instead of living for the - // window's lifetime; turning blur back on re-runs the lazy init. - fn release_blur_resources(&mut self) { - if self.blur_processor.is_some() || self.blur_source_texture.is_some() { - self.blur_processor = None; - self.blur_source_texture = None; - info!("Released camera background blur resources"); - } - self.blur_processor_init_attempted = false; - } - - fn ensure_blur_processor(&mut self) -> bool { - if self.blur_processor.is_some() { - return true; - } - - // Low-spec: never spin up the ONNX/wgpu blur processor (the heaviest - // cost in the preview). Both callers treat `false` as "blur unavailable" - // and fall back to rendering the raw camera frame, so the preview stays - // functional, just unblurred. The UI blur toggle is unaffected; it just - // does not take visual effect on these machines. - if is_low_spec_preview() { - return false; - } - - if self.blur_processor_init_attempted { - return false; - } - - self.blur_processor_init_attempted = true; - match cap_camera_effects::BlurProcessor::new(&self.device, wgpu::TextureFormat::Rgba8Unorm) - { - Ok(processor) => { - let mut processor = processor; - processor.set_inference_interval(CAMERA_PREVIEW_BLUR_INFERENCE_INTERVAL); - info!("Camera background blur processor initialized"); - self.blur_processor = Some(processor); - true - } - Err(err) => { - warn!("Failed to initialize camera background blur: {err}"); - false - } - } - } - - fn ensure_blur_source_texture(&mut self, width: u32, height: u32) -> &wgpu::Texture { - if self - .blur_source_texture - .as_ref() - .is_none_or(|t| t.width() != width || t.height() != height) - { - self.blur_source_texture = Some(self.device.create_texture(&wgpu::TextureDescriptor { - label: Some("Blur Source"), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - })); - } - self.blur_source_texture.as_ref().unwrap() - } - - async fn cleanup_for_shutdown(&mut self, window: &WebviewWindow) { - info!("Camera preview shutdown requested. Cleaning up..."); - - let _ = self.device.poll(wgpu::PollType::Poll); - - drop(std::mem::take(&mut self.texture)); - self.aspect_ratio = Cached::default(); - - let surface = self.surface.take(); - let (drop_tx, drop_rx) = oneshot::channel(); - let _ = window.run_on_main_thread(move || { - drop(surface); - let _ = drop_tx.send(()); - }); - let _ = tokio::time::timeout(Duration::from_millis(250), drop_rx).await; - - self.device.destroy(); - - tokio::time::sleep(Duration::from_millis(50)).await; - } - - fn acquire_surface_texture(&mut self) -> Option { - let surface = self.surface.as_ref()?; - match surface.get_current_texture() { - Ok(texture) => Some(texture), - Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { - tracing::warn!("Camera preview surface lost/outdated; reconfiguring"); - surface.configure(&self.device, &self.surface_config); - match surface.get_current_texture() { - Ok(texture) => Some(texture), - Err(err) => { - tracing::error!( - "Camera preview surface still failing after reconfigure: {err:?}" - ); - None - } - } - } - Err(wgpu::SurfaceError::Timeout) => { - tracing::debug!("Camera preview surface acquire timed out; skipping frame"); - None - } - Err(err) => { - tracing::error!("Error getting camera renderer surface texture: {err:?}"); - None - } - } - } - - fn reconfigure_gpu_surface(&mut self, window_width: u32, window_height: u32) { - let scale = self.surface_scale.max(1.0); - let surface_width = if window_width > 0 { - ((window_width as f64 * scale).round() as u32).max(1) - } else { - 1 - }; - let surface_height = if window_height > 0 { - ((window_height as f64 * scale).round() as u32).max(1) - } else { - 1 - }; - - if self.surface_config.width != surface_width - || self.surface_config.height != surface_height - { - self.surface_config.width = surface_width; - self.surface_config.height = surface_height; - if let Some(surface) = &self.surface { - surface.configure(&self.device, &self.surface_config); - } - } - - let window_uniforms = WindowUniforms { - window_height: window_height as f32, - window_width: window_width as f32, - toolbar_percentage: (TOOLBAR_HEIGHT * scale as f32) / self.surface_config.height as f32, - _padding: 0.0, - }; - self.queue.write_buffer( - &self.window_uniform_buffer, - 0, - bytemuck::cast_slice(&[window_uniforms]), - ); - } - - fn update_state_uniforms(&self, state: &CameraPreviewState) { - let clamped_size = clamp_size(state.size); - let normalized_size = - (clamped_size - MIN_CAMERA_SIZE) / (MAX_CAMERA_SIZE - MIN_CAMERA_SIZE); - - let state_uniforms = StateUniforms { - shape: if state.background_blur == cap_project::BackgroundBlurMode::Remove { - 3.0 - } else { - match state.shape { - CameraPreviewShape::Round => 0.0, - CameraPreviewShape::Square => 1.0, - CameraPreviewShape::Full => 2.0, - } - }, - size: normalized_size, - mirrored: if state.mirrored { 1.0 } else { 0.0 }, - _padding: 0.0, - }; - self.queue.write_buffer( - &self.state_uniform_buffer, - 0, - bytemuck::cast_slice(&[state_uniforms]), - ); - } - - async fn sync_ratio_uniform_and_resize_window_to_it( - &mut self, - window: &WebviewWindow, - state: &CameraPreviewState, - aspect_ratio: f32, - ) { - if self.aspect_ratio.update_key_and_should_init(aspect_ratio) { - let camera_uniforms = CameraUniforms { - camera_aspect_ratio: aspect_ratio, - _padding: 0.0, - }; - self.queue.write_buffer( - &self.camera_uniform_buffer, - 0, - bytemuck::cast_slice(&[camera_uniforms]), - ); - - if let Ok((width, height, scale)) = resize_window(window, state, aspect_ratio, false) - .await - .map_err(|err| error!("Error resizing camera preview window: {err}")) - { - self.surface_scale = scale; - self.reconfigure_gpu_surface(width, height); - } - } - } -} - -async fn resize_window( - window: &WebviewWindow, - state: &CameraPreviewState, - aspect: f32, - should_move: bool, -) -> anyhow::Result<(u32, u32, f64)> { - trace!("CameraPreview/resize_window"); - - let base = clamp_size(state.size); - let aspect = if state.shape == CameraPreviewShape::Full { - aspect.max(WIDE_CAMERA_ASPECT_RATIO) - } else { - 1.0 - }; - let window_width = base * aspect; - let window_height = base + TOOLBAR_HEIGHT; - - let window_width = window_width as u32; - let window_height = window_height as u32; - - let (tx, rx) = oneshot::channel(); - window - .run_on_main_thread({ - let window = window.clone(); - move || { - let result: tauri::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_size(LogicalSize::new(window_width, window_height))?; - - let scale_factor = window - .scale_factor() - .ok() - .filter(|scale| *scale > 0.0) - .unwrap_or(DEFAULT_SURFACE_SCALE); - - Ok((window_width, window_height, scale_factor)) - })(); - - tx.send(result).ok(); - } - }) - .context("Failed to run window resize on main thread")?; - - rx.await - .context("Failed to receive window resize result")? - .map_err(anyhow::Error::new) -} - -#[cfg(test)] -mod tests { - use super::{ - CAMERA_PREVIEW_MAX_TEXTURE_HEIGHT, CAMERA_PREVIEW_MAX_TEXTURE_WIDTH, - camera_preview_texture_dimensions, preferred_alpha_mode, wait_for_shutdown_signal, - }; - use std::thread; - use tokio::{runtime::Runtime, sync::oneshot, time::Duration}; - use wgpu::CompositeAlphaMode; - - #[test] - fn texture_covers_square_region_without_upscaling() { - let (width, height) = camera_preview_texture_dimensions(1280, 720, 460, 460, false); - let cover_scale = (460.0 / width as f64).max(460.0 / height as f64); - assert!( - cover_scale <= 1.0, - "texture {width}x{height} would upscale to cover a 460x460 region (scale {cover_scale})" - ); - } - - #[test] - fn texture_dimensions_stay_within_caps() { - let (width, height) = camera_preview_texture_dimensions(1920, 1080, 4000, 2200, false); - assert!(width <= CAMERA_PREVIEW_MAX_TEXTURE_WIDTH); - assert!(height <= CAMERA_PREVIEW_MAX_TEXTURE_HEIGHT); - } - - #[test] - fn preferred_alpha_mode_avoids_unsupported_inherit_fallback() { - assert_eq!( - preferred_alpha_mode(&[CompositeAlphaMode::Opaque]), - CompositeAlphaMode::Opaque - ); - } - - #[test] - fn preferred_alpha_mode_keeps_transparent_modes_when_supported() { - assert_eq!( - preferred_alpha_mode(&[ - CompositeAlphaMode::Opaque, - CompositeAlphaMode::PreMultiplied, - ]), - CompositeAlphaMode::PreMultiplied - ); - assert_eq!( - preferred_alpha_mode(&[ - CompositeAlphaMode::Opaque, - CompositeAlphaMode::PostMultiplied, - ]), - CompositeAlphaMode::PostMultiplied - ); - } - - #[test] - fn preferred_alpha_mode_uses_other_supported_fallbacks() { - assert_eq!( - preferred_alpha_mode(&[CompositeAlphaMode::Auto]), - CompositeAlphaMode::Auto - ); - assert_eq!( - preferred_alpha_mode(&[CompositeAlphaMode::Inherit]), - CompositeAlphaMode::Inherit - ); - assert_eq!(preferred_alpha_mode(&[]), CompositeAlphaMode::Opaque); - } - - #[test] - fn wait_for_shutdown_signal_can_run_from_plain_thread() { - let runtime = Runtime::new().unwrap(); - let (tx, rx) = oneshot::channel(); - - let handle = thread::spawn(move || { - wait_for_shutdown_signal(&runtime, rx, Duration::from_millis(100)); - }); - - tx.send(()).ok(); - handle.join().unwrap(); - } -} - -fn blur_mode_from_project( - mode: cap_project::BackgroundBlurMode, -) -> Option { - match mode { - cap_project::BackgroundBlurMode::Off => None, - cap_project::BackgroundBlurMode::Light => Some(cap_camera_effects::BlurMode::Light), - cap_project::BackgroundBlurMode::Heavy => Some(cap_camera_effects::BlurMode::Heavy), - cap_project::BackgroundBlurMode::Remove => { - cfg!(target_os = "macos").then_some(cap_camera_effects::BlurMode::Remove) - } - } -} - -fn render_solid_frame(color: [u8; 4], width: u32, height: u32) -> (Vec, u32) { - let pixel_count = (height * width) as usize; - let buffer: Vec = color - .iter() - .cycle() - .take(pixel_count * 4) - .copied() - .collect(); - - (buffer, 4 * width) -} - -pub struct PreparedTexture { - texture: wgpu::Texture, - bind_group: wgpu::BindGroup, - uniform_bind_group: wgpu::BindGroup, - render_pipeline: wgpu::RenderPipeline, - device: wgpu::Device, - queue: wgpu::Queue, - width: u32, - height: u32, -} - -impl PreparedTexture { - #[allow(clippy::too_many_arguments)] - pub fn init( - device: wgpu::Device, - queue: wgpu::Queue, - sampler: &wgpu::Sampler, - bind_group_layout: &wgpu::BindGroupLayout, - uniform_bind_group: wgpu::BindGroup, - render_pipeline: wgpu::RenderPipeline, - width: u32, - height: u32, - ) -> Self { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Texture"), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Texture Bind Group"), - layout: bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(sampler), - }, - ], - }); - - Self { - texture, - bind_group, - uniform_bind_group, - render_pipeline, - device, - queue, - width, - height, - } - } - - pub fn render_no_upload(&self, surface: &SurfaceTexture) { - let surface_view = surface - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &surface_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_bind_group(0, &self.bind_group, &[]); - render_pass.set_bind_group(1, &self.uniform_bind_group, &[]); - render_pass.draw(0..6, 0..1); - } - - self.queue.submit(Some(encoder.finish())); - } - - pub fn render(&self, surface: &SurfaceTexture, buffer: &[u8], stride: u32) { - let surface_view = surface - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - self.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &self.texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - buffer, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(stride), - rows_per_image: Some(self.height), - }, - wgpu::Extent3d { - width: self.width, - height: self.height, - depth_or_array_layers: 1, - }, - ); - - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &surface_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_bind_group(0, &self.bind_group, &[]); - render_pass.set_bind_group(1, &self.uniform_bind_group, &[]); - render_pass.draw(0..6, 0..1); - } - - self.queue.submit(Some(encoder.finish())); - } -} - -struct Cached { - value: Option<(K, V)>, -} - -impl Default for Cached { - fn default() -> Self { - Self { value: None } - } -} - -impl Cached { - pub fn get_or_init(&mut self, key: K, init: impl FnOnce() -> V) -> &mut V { - if self.value.as_ref().is_none_or(|(k, _)| *k != key) { - self.value = Some((key, init())); - } - - &mut self.value.as_mut().expect("checked above").1 - } - - pub fn get_latest_key(&self) -> Option<&K> { - self.value.as_ref().map(|(k, _)| k) - } -} - -impl Cached { - pub fn update_key_and_should_init(&mut self, key: K) -> bool { - if self.value.as_ref().is_none_or(|(k, _)| *k != key) { - self.value = Some((key, ())); - true - } else { - false - } - } -} - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct StateUniforms { - shape: f32, - size: f32, - mirrored: f32, - _padding: f32, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct WindowUniforms { - window_height: f32, - window_width: f32, - toolbar_percentage: f32, - _padding: f32, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct CameraUniforms { - camera_aspect_ratio: f32, - _padding: f32, -} diff --git a/apps/desktop/src-tauri/src/camera.wgsl b/apps/desktop/src-tauri/src/camera.wgsl deleted file mode 100644 index 7d5354bc9cf..00000000000 --- a/apps/desktop/src-tauri/src/camera.wgsl +++ /dev/null @@ -1,179 +0,0 @@ -struct StateUniforms { - shape: f32, - size: f32, - mirrored: f32, - _padding: f32, -} - -struct WindowUniforms { - window_height: f32, - window_width: f32, - toolbar_percentage: f32, - _padding: f32, -} - -struct CameraUniforms { - camera_aspect_ratio: f32, - _padding: f32, -} - -@group(1) @binding(0) -var uniforms: StateUniforms; - -@group(1) @binding(1) -var window_uniforms: WindowUniforms; - -@group(1) @binding(2) -var camera_uniforms: CameraUniforms; - -@group(0) @binding(0) -var t_camera: texture_2d; - -@group(0) @binding(1) -var s_camera: sampler; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv: vec2, -}; - -@vertex -fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { - var out: VertexOutput; - - // We use the vertex_index to determine the position of each vertex. - // This maps the 6 vertex indices to NDC coordinates for a fullscreen quad. - var indexes = array, 6>( - vec2(-1.0, -1.0),// First triangle (bottom-left) - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), // Second triangle (top-right) - vec2(1.0, -1.0), - vec2(1.0, 1.0), - ); - var uv = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0), - ); - - var pos: vec2 = indexes[idx]; - pos.y *= (1.0 - window_uniforms.toolbar_percentage); - pos.y -= window_uniforms.toolbar_percentage; - - out.position = vec4(pos, 0.0, 1.0); - out.uv = uv[idx]; - return out; -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { - // Calculate the effective rendering dimensions (accounting for toolbar) - let effective_height = window_uniforms.window_height * (1.0 - window_uniforms.toolbar_percentage); - let crop_width = window_uniforms.window_width; - let crop_height = effective_height; - let crop_aspect = crop_width / crop_height; - let camera_aspect = camera_uniforms.camera_aspect_ratio; - - // Calculate UV coordinates for proper "cover" behavior - var final_uv = in.uv; - - // Determine which dimension needs to be scaled to cover the crop region - if (camera_aspect > crop_aspect) { - // Camera is wider than crop region - scale horizontally - let scale = crop_aspect / camera_aspect; - let offset = (1.0 - scale) * 0.5; - final_uv.x = final_uv.x * scale + offset; - } else { - // Camera is taller than crop region - scale vertically - let scale = camera_aspect / crop_aspect; - let offset = (1.0 - scale) * 0.5; - final_uv.y = final_uv.y * scale + offset; - } - - // Apply mirroring if enabled - if (uniforms.mirrored == 1.0) { - final_uv.x = 1.0 - final_uv.x; - } - - // Shape constants: 0 = Round, 1 = Square, 2 = Full - // Size constants: 0 = Sm, 1 = Lg - let shape = uniforms.shape; - let size = uniforms.size; - - // Convert UV coordinates to center-based coordinates [-1, 1] - let center_uv = (in.uv - 0.5) * 2.0; - - var mask = 1.0; - - if (shape == 0.0) { - // Round shape with enhanced anti-aliasing - let distance = length(center_uv); - let radius = 1.0; - - // Multi-level anti-aliasing approach - let pixel_size = length(fwidth(center_uv)); - let aa_width = max(pixel_size, 0.003); // Reduced minimum AA width - - // Primary edge calculation - let edge_distance = distance - radius; - mask = 1.0 - smoothstep(-aa_width, aa_width, edge_distance); - - } else if (shape == 1.0) { - // Square shape with enhanced corner anti-aliasing - // Interpolate corner radius based on normalized size (0-1) - let corner_radius = mix(0.10, 0.14, size); - let abs_uv = abs(center_uv); - let corner_pos = abs_uv - (1.0 - corner_radius); - let corner_dist = length(max(corner_pos, vec2(0.0, 0.0))); - - // Enhanced anti-aliasing for corners - let pixel_size = length(fwidth(center_uv)); - let aa_width = max(pixel_size, 0.002); - - let edge_distance = corner_dist - corner_radius; - mask = 1.0 - smoothstep(-aa_width, aa_width, edge_distance); - } else if (shape == 2.0) { - // Full shape with aspect ratio-corrected rounded corners - let window_aspect = crop_aspect; - // Interpolate corner radius based on normalized size (0-1) - let corner_radius = mix(0.08, 0.12, size); - - let abs_uv = abs(center_uv); - let corner_pos = abs_uv - (1.0 - corner_radius); - - // Use aspect-ratio aware distance calculation for circular corners - var adjusted_corner_pos = max(corner_pos, vec2(0.0, 0.0)); - if (window_aspect > 1.0) { - // For wide windows, scale X component to make distance calculation circular - adjusted_corner_pos.x *= window_aspect; - } else { - // For tall windows, scale Y component to make distance calculation circular - adjusted_corner_pos.y /= window_aspect; - } - - let corner_dist = length(adjusted_corner_pos); - - // Enhanced anti-aliasing for corners - let pixel_size = length(fwidth(center_uv)); - let aa_width = max(pixel_size, 0.002); - - let edge_distance = corner_dist - corner_radius; - mask = 1.0 - smoothstep(-aa_width, aa_width, edge_distance); - } else { - // For any other shape, default to no masking (rectangular) - mask = 1.0; - } - - if (mask < 0.05) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let camera_color = textureSample(t_camera, s_camera, final_uv); - - let final_alpha = camera_color.a * select(1.0, mask, mask < 0.95); - return vec4(camera_color.rgb, final_alpha); -} diff --git a/apps/desktop/src-tauri/src/camera_legacy.rs b/apps/desktop/src-tauri/src/camera_legacy.rs deleted file mode 100644 index 4c4655f9538..00000000000 --- a/apps/desktop/src-tauri/src/camera_legacy.rs +++ /dev/null @@ -1,1084 +0,0 @@ -use std::sync::Arc; -use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; -use std::time::{Duration, Instant}; - -use cap_recording::FFmpegVideoFrame; -#[cfg(target_os = "macos")] -use cap_utils::macos_qos::{MacOsQosClass, set_current_thread_qos}; -use flume::Sender; -use tokio::sync::watch; -use tokio_util::sync::CancellationToken; - -use crate::camera::{CameraPreviewState, MAX_CAMERA_SIZE, MIN_CAMERA_SIZE, is_low_spec_preview}; -use crate::frame_ws::{WSFrame, WSFrameFormat, create_watch_frame_ws_with_instant_tracking}; - -const WS_READBACK_PENDING: u8 = 0; -const WS_READBACK_READY_OK: u8 = 1; -const WS_READBACK_READY_ERR: u8 = 2; - -const WS_READBACK_CONSUMED: u8 = 3; - -#[cfg(target_os = "linux")] -type OutputReceipt = Option; -#[cfg(not(target_os = "linux"))] -#[derive(Default)] -struct PreviewReceipt; -#[cfg(not(target_os = "linux"))] -type OutputReceipt = PreviewReceipt; - -struct ReadbackTicket { - status: Arc, - receipt: OutputReceipt, -} - -impl ReadbackTicket { - fn new(receipt: OutputReceipt) -> Self { - Self { - status: Arc::new(AtomicU8::new(WS_READBACK_PENDING)), - receipt, - } - } - - fn take_ready(&mut self) -> Result, String> { - match self.status.load(Ordering::Acquire) { - WS_READBACK_READY_OK => { - self.status.store(WS_READBACK_CONSUMED, Ordering::Release); - Ok(Some(std::mem::take(&mut self.receipt))) - } - WS_READBACK_READY_ERR => { - self.status.store(WS_READBACK_CONSUMED, Ordering::Release); - Err("Requested camera blur GPU readback failed".into()) - } - _ => Ok(None), - } - } -} - -enum WsReadbackState { - Idle, - InFlight(ReadbackTicket), -} - -struct WsReadback { - buffer: wgpu::Buffer, - state: WsReadbackState, -} - -struct BlurInput<'a> { - rgba: &'a [u8], - width: u32, - height: u32, - stride: u32, - mode: cap_camera_effects::BlurMode, - receipt: OutputReceipt, -} - -struct BlurredFrame { - data: Arc>, - #[cfg(target_os = "linux")] - receipt: Option, -} - -pub struct CameraPreviewWs { - pub sender: Sender, - pub port: u16, - pub shutdown: CancellationToken, - #[cfg(target_os = "linux")] - pub processing: crate::linux_instant_camera::ProcessingFactory, -} - -#[cfg(target_os = "linux")] -enum WorkerEvent { - Frame(F), - Command(C), - Closed, - Tick, -} - -#[cfg(target_os = "linux")] -fn next_worker_event( - frames: &flume::Receiver, - commands: &flume::Receiver, - timeout: Option, -) -> WorkerEvent { - let selector = flume::Selector::new() - .recv(frames, |result| { - result.map_or(WorkerEvent::Closed, WorkerEvent::Frame) - }) - .recv(commands, |result| { - result.map_or(WorkerEvent::Closed, WorkerEvent::Command) - }); - match timeout { - Some(timeout) => selector.wait_timeout(timeout).unwrap_or(WorkerEvent::Tick), - None => selector.wait(), - } -} - -fn processing_needed(preview: bool, recording: bool) -> bool { - preview || recording -} - -const WS_PREVIEW_SURFACE_SCALE: u32 = 2; -// Same drag-resize hysteresis as the native preview: only rebuild the scaler -// and reallocate buffers when the target width crosses a 64px bucket. -const WS_PREVIEW_WIDTH_BUCKET: u32 = 64; -const WS_PREVIEW_MIN_WIDTH: u32 = 320; -const WS_PREVIEW_MAX_WIDTH: u32 = 960; -const WS_PREVIEW_MAX_HEIGHT: u32 = 540; -const WS_PREVIEW_BLUR_MAX_WIDTH: u32 = 640; -const WS_PREVIEW_BLUR_MAX_HEIGHT: u32 = 360; -const WS_PREVIEW_TARGET_FRAME_INTERVAL: Duration = Duration::from_micros(16_666); -const WS_PREVIEW_FRAME_INTERVAL_SLACK: Duration = Duration::from_millis(1); -const WS_BLUR_INFERENCE_INTERVAL: Duration = - Duration::from_millis(if cfg!(target_os = "macos") { 33 } else { 150 }); - -// Low-spec preview profile (mirrors the native path in camera.rs). Only used -// when `is_low_spec_preview()` is true (machines <= 8GB RAM detected at -// startup); high-spec machines use the constants above bit-for-bit. -const WS_PREVIEW_LOW_SPEC_MAX_WIDTH: u32 = 640; -const WS_PREVIEW_LOW_SPEC_MAX_HEIGHT: u32 = 360; -const WS_PREVIEW_LOW_SPEC_TARGET_FRAME_INTERVAL: Duration = Duration::from_micros(33_333); -const FRAME_POOL_LOW_SPEC_MAX: usize = 2; - -fn preview_frame_due(last_preview_at: Option, now: Instant) -> bool { - // High-spec keeps the original 60fps target; low-spec paces at 30fps. - let target_interval = if is_low_spec_preview() { - WS_PREVIEW_LOW_SPEC_TARGET_FRAME_INTERVAL - } else { - WS_PREVIEW_TARGET_FRAME_INTERVAL - }; - last_preview_at.is_none_or(|last| { - now.saturating_duration_since(last) + WS_PREVIEW_FRAME_INTERVAL_SLACK >= target_interval - }) -} - -const FRAME_POOL_MAX: usize = 4; - -/// Frame-pool cap, lowered on low-spec to hold fewer ~2MB buffers in memory. -#[inline] -fn frame_pool_max() -> usize { - if is_low_spec_preview() { - FRAME_POOL_LOW_SPEC_MAX - } else { - FRAME_POOL_MAX - } -} - -// Reuses a previously-sent frame buffer once every WSFrame referencing it has -// been dropped (watch cell replaced + socket sends finished), eliminating the -// ~2MB allocation + page-fault churn per frame at steady state. -fn with_pooled_buffer( - pool: &mut Vec>>, - fill: impl FnOnce(&mut Vec), -) -> Arc> { - for buf in pool.iter_mut() { - if Arc::strong_count(buf) == 1 - && let Some(vec) = Arc::get_mut(buf) - { - vec.clear(); - fill(vec); - return buf.clone(); - } - } - - let mut vec = Vec::new(); - fill(&mut vec); - let buf = Arc::new(vec); - if pool.len() < frame_pool_max() { - pool.push(buf.clone()); - } - buf -} - -fn pack_plane_rows(dst: &mut Vec, src: &[u8], row_bytes: usize, height: u32, stride: u32) { - let stride = stride as usize; - let height = height as usize; - dst.reserve(row_bytes * height); - if stride == row_bytes { - dst.extend_from_slice(&src[..row_bytes * height]); - } else { - for row in 0..height { - let start = row * stride; - dst.extend_from_slice(&src[start..start + row_bytes]); - } - } -} - -fn pack_rgba_rows(dst: &mut Vec, frame: &ffmpeg::util::frame::Video) { - pack_plane_rows( - dst, - frame.data(0), - frame.width() as usize * 4, - frame.height(), - frame.stride(0) as u32, - ); -} - -fn pack_nv12_planes(dst: &mut Vec, frame: &ffmpeg::util::frame::Video) { - let width = frame.width() as usize; - pack_plane_rows( - dst, - frame.data(0), - width, - frame.height(), - frame.stride(0) as u32, - ); - pack_plane_rows( - dst, - frame.data(1), - width, - frame.height() / 2, - frame.stride(1) as u32, - ); -} - -fn prepare_ws_data( - frame: &ffmpeg::util::frame::Video, - format: WSFrameFormat, - frame_pool: &mut Vec>>, -) -> (Arc>, u32) { - match format { - WSFrameFormat::Rgba => ( - with_pooled_buffer(frame_pool, |vec| pack_rgba_rows(vec, frame)), - frame.width() * 4, - ), - WSFrameFormat::Nv12 { .. } => ( - with_pooled_buffer(frame_pool, |vec| pack_nv12_planes(vec, frame)), - frame.width(), - ), - } -} - -fn scaled_preview_dimensions(width: u32, height: u32, state: &CameraPreviewState) -> (u32, u32) { - let blur_enabled = state.background_blur != cap_project::BackgroundBlurMode::Off; - let (max_width, max_height) = if is_low_spec_preview() { - // Low-spec caps preview to 640x360 (blur is skipped on low-spec, so the - // blur-vs-no-blur branch below does not apply). - ( - WS_PREVIEW_LOW_SPEC_MAX_WIDTH, - WS_PREVIEW_LOW_SPEC_MAX_HEIGHT, - ) - } else if blur_enabled { - (WS_PREVIEW_BLUR_MAX_WIDTH, WS_PREVIEW_BLUR_MAX_HEIGHT) - } else { - (WS_PREVIEW_MAX_WIDTH, WS_PREVIEW_MAX_HEIGHT) - }; - let visible_width = (state.size.clamp(MIN_CAMERA_SIZE, MAX_CAMERA_SIZE) as u32) - .saturating_mul(WS_PREVIEW_SURFACE_SCALE); - let requested_width = visible_width - .max(WS_PREVIEW_MIN_WIDTH) - .div_ceil(WS_PREVIEW_WIDTH_BUCKET) - .saturating_mul(WS_PREVIEW_WIDTH_BUCKET) - .min(max_width); - let width_scale = requested_width as f64 / width.max(1) as f64; - let height_scale = max_height as f64 / height.max(1) as f64; - let scale = width_scale.min(height_scale).min(1.0); - let target_width = ((width as f64 * scale).round() as u32).max(1); - let target_height = ((height as f64 * scale).round() as u32).max(1); - (target_width, target_height) -} - -pub async fn create_camera_preview_ws( - state_rx: watch::Receiver, -) -> CameraPreviewWs { - let (camera_tx, camera_rx) = flume::bounded::(1); - #[cfg(target_os = "linux")] - let (processing, commands) = crate::linux_instant_camera::processing_channel(); - let (frame_tx, frame_rx) = watch::channel::>>(None); - let subscriber_count = Arc::new(AtomicUsize::new(0)); - let instant_subscriber_count = Arc::new(AtomicUsize::new(0)); - let frame_tx_clone = frame_tx.clone(); - let thread_subscriber_count = subscriber_count.clone(); - let thread_instant_subscriber_count = instant_subscriber_count.clone(); - std::thread::spawn(move || { - use ffmpeg::format::Pixel; - - #[cfg(target_os = "macos")] - { - let result = set_current_thread_qos(MacOsQosClass::UserInteractive); - if result != 0 { - tracing::warn!(result, "pthread_set_qos_class_self_np failed"); - } - } - #[cfg(windows)] - { - use windows::Win32::System::Threading::{ - GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL, - }; - if let Err(err) = - unsafe { SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL) } - { - tracing::warn!("SetThreadPriority failed: {err}"); - } - } - - let mut converter: Option<(Pixel, Pixel, ffmpeg::software::scaling::Context)> = None; - let mut reusable_frame: Option = None; - let mut state_rx = state_rx; - - let mut blur_state = WsBlurState::new(); - let mut last_preview_at = None; - let mut frame_pool: Vec>> = Vec::new(); - let mut frame_counter: u32 = 0; - let mut idle = true; - #[cfg(target_os = "linux")] - let mut recording: Option = None; - #[cfg(target_os = "linux")] - let mut warm_until: Option = None; - loop { - #[cfg(target_os = "linux")] - if recording.as_ref().is_some_and(|work| work.cancelled()) { - recording = None; - warm_until = Some(Instant::now() + Duration::from_millis(500)); - } - #[cfg(target_os = "linux")] - if let Some(deadline) = warm_until { - if thread_subscriber_count.load(Ordering::Acquire) > 0 { - warm_until = None; - } else if Instant::now() >= deadline { - converter = None; - reusable_frame = None; - frame_pool.clear(); - blur_state.release(); - last_preview_at = None; - idle = true; - warm_until = None; - let _previous_frame = frame_tx_clone.send_replace(None); - } - } - let input = &camera_rx; - #[cfg(target_os = "linux")] - let input = recording.as_ref().map_or(input, |work| &work.frames); - #[cfg(target_os = "linux")] - let raw_frame = { - let timeout = if recording.is_some() { - Some(Duration::from_millis(50)) - } else { - warm_until.map(|deadline| deadline.saturating_duration_since(Instant::now())) - }; - let event = next_worker_event(input, &commands, timeout); - match event { - WorkerEvent::Frame(frame) => frame, - WorkerEvent::Command(work) => { - if work.cancelled() { - continue; - } - if recording.is_some() { - work.fail("Another camera processing lease is active"); - continue; - } - warm_until = None; - blur_state.begin_recording_epoch(); - last_preview_at = None; - recording = Some(*work); - continue; - } - WorkerEvent::Closed => break, - WorkerEvent::Tick => continue, - } - }; - #[cfg(not(target_os = "linux"))] - let raw_frame = match input.recv() { - Ok(frame) => frame, - Err(_) => break, - }; - let mut raw_frame = raw_frame; - while let Ok(newer) = input.try_recv() { - raw_frame = newer; - } - #[cfg(target_os = "linux")] - if let Some(work) = &recording { - match work.accepts(raw_frame.timestamp) { - Ok(true) => {} - Ok(false) => continue, - Err(error) => { - work.fail(error); - continue; - } - } - } - #[cfg(target_os = "linux")] - let capture_timestamp = raw_frame.timestamp; - let frame = raw_frame.inner; - let instant_preview_active = - thread_instant_subscriber_count.load(Ordering::Acquire) > 0; - let ws_active = thread_subscriber_count.load(Ordering::Acquire) > 0; - #[cfg(target_os = "linux")] - let recording_active = recording.is_some(); - #[cfg(not(target_os = "linux"))] - let recording_active = false; - let processing_active = processing_needed(ws_active, recording_active); - if !processing_active { - #[cfg(target_os = "linux")] - if warm_until.is_some() { - continue; - } - if !idle { - idle = true; - converter = None; - reusable_frame = None; - frame_pool.clear(); - blur_state.release(); - last_preview_at = None; - let _previous_frame = frame_tx_clone.send_replace(None); - } - continue; - } - idle = false; - let now = Instant::now(); - if !preview_frame_due(last_preview_at, now) { - continue; - } - last_preview_at = Some(now); - let state = state_rx.borrow_and_update().clone(); - #[cfg(target_os = "linux")] - let state = recording.as_ref().map_or(state, |work| work.state.clone()); - let blur_mode = state.background_blur; - let blur_enabled = blur_mode != cap_project::BackgroundBlurMode::Off; - let effects_mode = match blur_mode { - cap_project::BackgroundBlurMode::Off | cap_project::BackgroundBlurMode::Light => { - cap_camera_effects::BlurMode::Light - } - cap_project::BackgroundBlurMode::Heavy => cap_camera_effects::BlurMode::Heavy, - cap_project::BackgroundBlurMode::Remove => cap_camera_effects::BlurMode::Remove, - }; - let (mut target_width, mut target_height) = - scaled_preview_dimensions(frame.width(), frame.height(), &state); - let use_nv12 = cfg!(target_os = "macos") - && instant_preview_active - && !blur_enabled - && frame.format() == Pixel::NV12; - let (output_pixel, output_format) = if use_nv12 { - target_width = target_width.max(2) & !1; - target_height = target_height.max(2) & !1; - ( - Pixel::NV12, - WSFrameFormat::Nv12 { - full_range: frame.color_range() == ffmpeg::color::Range::JPEG, - }, - ) - } else { - (Pixel::RGBA, WSFrameFormat::Rgba) - }; - let needs_convert = frame.format() != output_pixel - || frame.width() != target_width - || frame.height() != target_height; - if !blur_enabled { - blur_state.release(); - } - let output_frame = if needs_convert { - let ctx = match &mut converter { - Some((input_format, cached_output_format, ctx)) - if *input_format == frame.format() - && *cached_output_format == output_pixel - && ctx.input().width == frame.width() - && ctx.input().height == frame.height() - && ctx.output().width == target_width - && ctx.output().height == target_height => - { - ctx - } - _ => { - let new_converter = ffmpeg::software::scaling::Context::get( - frame.format(), - frame.width(), - frame.height(), - output_pixel, - target_width, - target_height, - ffmpeg::software::scaling::flag::Flags::FAST_BILINEAR, - ); - let Ok(new_converter) = new_converter else { - #[cfg(target_os = "linux")] - if let Some(work) = &recording { - work.fail("Camera pixel conversion is unavailable"); - } - continue; - }; - reusable_frame = None; - &mut converter - .insert((frame.format(), output_pixel, new_converter)) - .2 - } - }; - let out_frame = reusable_frame.get_or_insert_with(|| { - ffmpeg::util::frame::Video::new( - output_pixel, - ctx.output().width, - ctx.output().height, - ) - }); - if ctx.run(&frame, out_frame).is_err() { - #[cfg(target_os = "linux")] - if let Some(work) = &recording { - work.fail("Camera pixel conversion failed"); - } - continue; - } - &*out_frame - } else { - &frame - }; - let width = output_frame.width(); - let height = output_frame.height(); - #[cfg(target_os = "linux")] - let receipt = recording - .as_ref() - .map(|work| work.receipt(capture_timestamp, (width, height))); - let blurred = if blur_enabled { - blur_state.process( - BlurInput { - rgba: output_frame.data(0), - width, - height, - stride: output_frame.stride(0) as u32, - mode: effects_mode, - #[cfg(target_os = "linux")] - receipt: receipt.clone(), - #[cfg(not(target_os = "linux"))] - receipt: PreviewReceipt, - }, - &mut frame_pool, - ) - } else { - Ok(None) - }; - #[cfg(target_os = "linux")] - if let Some(work) = &recording { - if !blur_enabled { - let (data, _) = prepare_ws_data(output_frame, output_format, &mut frame_pool); - work.publish(&data, receipt.unwrap()); - } else { - match &blurred { - Ok(Some(output)) => { - if let Some(receipt) = output.receipt.clone() { - work.publish(&output.data, receipt); - } - } - Ok(None) => {} - Err(error) => work.fail(error.clone()), - } - } - } - if ws_active { - if blur_mode == cap_project::BackgroundBlurMode::Remove - && matches!(blurred, Ok(None)) - { - continue; - } - let (data, stride) = blurred - .ok() - .flatten() - .map(|frame| (frame.data, width * 4)) - .unwrap_or_else(|| { - prepare_ws_data(output_frame, output_format, &mut frame_pool) - }); - frame_counter = frame_counter.wrapping_add(1); - let _previous_frame = frame_tx_clone.send_replace(Some(Arc::new(WSFrame { - data: data.into(), - width, - height, - stride, - frame_number: frame_counter, - target_time_ns: 0, - format: output_format, - created_at: Instant::now(), - }))); - } else { - frame_tx_clone.send_if_modified(|frame| frame.take().is_some()); - } - } - }); - let (camera_ws_port, _shutdown) = create_watch_frame_ws_with_instant_tracking( - frame_rx, - subscriber_count, - instant_subscriber_count, - ) - .await; - - CameraPreviewWs { - sender: camera_tx, - port: camera_ws_port, - shutdown: _shutdown, - #[cfg(target_os = "linux")] - processing, - } -} - -struct WsBlurState { - processor: Option, - init_attempted: bool, -} - -struct WsBlurResources { - device: wgpu::Device, - queue: wgpu::Queue, - processor: cap_camera_effects::BlurProcessor, - source_texture: Option<(u32, u32, wgpu::Texture)>, - readbacks: Option<(u32, u32, [WsReadback; 2])>, - current_idx: usize, - mode: Option, -} - -impl WsBlurState { - fn new() -> Self { - Self { - processor: None, - init_attempted: false, - } - } - - #[cfg(target_os = "linux")] - fn begin_recording_epoch(&mut self) { - if let Some(resources) = &mut self.processor { - resources.readbacks = None; - resources.current_idx = 0; - resources.processor.reset_mask_history(); - } else { - self.init_attempted = false; - } - } - - // Drops the dedicated wgpu device, ONNX session, and readback buffers as - // soon as blur is off; re-enabling re-runs the lazy init. - fn release(&mut self) { - if self.processor.is_some() { - self.processor = None; - tracing::info!("Released WebSocket camera blur resources"); - } - self.init_attempted = false; - } - - fn process( - &mut self, - input: BlurInput<'_>, - pool: &mut Vec>>, - ) -> Result, String> { - let BlurInput { - rgba: rgba_data, - width, - height, - stride, - mode, - receipt, - } = input; - // Idle preview keeps its compatibility fallback; recording requires this error to stay terminal. - if is_low_spec_preview() || cap_camera_effects::blur_disabled() { - return Err("Requested camera blur is disabled or unavailable on this device".into()); - } - - if !self.init_attempted { - self.init_attempted = true; - self.processor = Some(init_headless_blur()?); - } - - let res = self - .processor - .as_mut() - .ok_or("Requested camera blur initialization failed")?; - - if res.mode != Some(mode) { - res.readbacks = None; - res.current_idx = 0; - res.mode = Some(mode); - } - - #[cfg(target_os = "linux")] - if receipt.as_ref().is_some_and(|receipt| matches!(receipt.timestamp, - cap_timestamp::Timestamp::Instant(captured) if captured.elapsed() > Duration::from_secs(1))) { - return Ok(None); - } - - let src = match &res.source_texture { - Some((w, h, t)) if *w == width && *h == height => t, - _ => { - let tex = res.device.create_texture(&wgpu::TextureDescriptor { - label: Some("WS Blur Source"), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - res.source_texture = Some((width, height, tex)); - &res.source_texture.as_ref().unwrap().2 - } - }; - - let bytes_per_row_aligned = (width * 4 + 255) & !255; - let buf_size = (bytes_per_row_aligned * height) as u64; - - let readbacks_match = matches!( - &res.readbacks, - Some((w, h, _)) if *w == width && *h == height - ); - if !readbacks_match { - let make_buf = |label: &str| { - res.device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size: buf_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }) - }; - let readbacks = [ - WsReadback { - buffer: make_buf("WS Blur Readback 0"), - state: WsReadbackState::Idle, - }, - WsReadback { - buffer: make_buf("WS Blur Readback 1"), - state: WsReadbackState::Idle, - }, - ]; - res.readbacks = Some((width, height, readbacks)); - res.current_idx = 0; - } - - let _ = res.device.poll(wgpu::PollType::Poll); - - let current_idx = res.current_idx; - let prev_idx = 1 - current_idx; - - let prev_data = try_drain_readback( - &mut res.readbacks.as_mut().unwrap().2[prev_idx], - width, - height, - bytes_per_row_aligned, - pool, - ); - let curr_data = try_drain_readback( - &mut res.readbacks.as_mut().unwrap().2[current_idx], - width, - height, - bytes_per_row_aligned, - pool, - ); - #[cfg(target_os = "linux")] - let require_verified_output = receipt.is_some(); - #[cfg(not(target_os = "linux"))] - let require_verified_output = false; - let blurred_out = if require_verified_output { - prev_data?.or(curr_data?) - } else { - prev_data.ok().flatten().or(curr_data.ok().flatten()) - }; - - let issue_idx = if matches!( - res.readbacks.as_ref().unwrap().2[current_idx].state, - WsReadbackState::Idle - ) { - Some(current_idx) - } else if matches!( - res.readbacks.as_ref().unwrap().2[prev_idx].state, - WsReadbackState::Idle - ) { - Some(prev_idx) - } else { - None - }; - - if let Some(idx) = issue_idx { - res.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: src, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - rgba_data, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(stride), - rows_per_image: Some(height), - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - - let output = res.processor.process(&res.device, &res.queue, src, mode); - - let mut encoder = res - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("WS Blur Copy"), - }); - - encoder.copy_texture_to_buffer( - wgpu::TexelCopyTextureInfo { - texture: output, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyBufferInfo { - buffer: &res.readbacks.as_ref().unwrap().2[idx].buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(bytes_per_row_aligned), - rows_per_image: Some(height), - }, - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - - #[cfg(target_os = "linux")] - let receipt = receipt.map(|mut receipt| { - receipt.blur = res.processor.output_status(); - receipt - }); - res.queue.submit(std::iter::once(encoder.finish())); - let ticket = ReadbackTicket::new(receipt); - let status_cb = ticket.status.clone(); - res.readbacks.as_ref().unwrap().2[idx] - .buffer - .slice(..) - .map_async(wgpu::MapMode::Read, move |result| { - status_cb.store( - if result.is_ok() { - WS_READBACK_READY_OK - } else { - WS_READBACK_READY_ERR - }, - Ordering::Release, - ); - }); - res.readbacks.as_mut().unwrap().2[idx].state = WsReadbackState::InFlight(ticket); - res.current_idx = 1 - idx; - } - - Ok(blurred_out) - } -} - -fn try_drain_readback( - readback: &mut WsReadback, - width: u32, - height: u32, - bytes_per_row_aligned: u32, - pool: &mut Vec>>, -) -> Result, String> { - let WsReadbackState::InFlight(ticket) = &mut readback.state else { - return Ok(None); - }; - let receipt = match ticket.take_ready() { - Ok(Some(receipt)) => receipt, - Ok(None) => return Ok(None), - Err(error) => { - readback.buffer.unmap(); - readback.state = WsReadbackState::Idle; - return Err(error); - } - }; - let slice = readback.buffer.slice(..); - let data = slice.get_mapped_range(); - let row_bytes = (width * 4) as usize; - let out = with_pooled_buffer(pool, |vec| { - vec.reserve(row_bytes * height as usize); - for row in 0..height as usize { - let start = row * bytes_per_row_aligned as usize; - vec.extend_from_slice(&data[start..start + row_bytes]); - } - }); - drop(data); - readback.buffer.unmap(); - readback.state = WsReadbackState::Idle; - #[cfg(not(target_os = "linux"))] - let _ = receipt; - Ok(Some(BlurredFrame { - data: out, - #[cfg(target_os = "linux")] - receipt, - })) -} - -fn init_headless_blur() -> Result { - // Arm the sentinel's blur marker around the blur-dedicated wgpu - // adapter/device setup too, so a native death here is attributed to blur. - // Deliberately not `enter_gpu_init_phase`: that would cross-trigger WARP - // software-graphics recovery and cripple the editor for a blur-only crash. - crate::crash_sentinel::enter_blur_session(); - struct BlurSessionGuard; - impl Drop for BlurSessionGuard { - fn drop(&mut self) { - crate::crash_sentinel::exit_blur_session(); - } - } - let _guard = BlurSessionGuard; - - let instance = cap_rendering::create_wgpu_instance_sync(); - let force_software_adapter = cap_rendering::force_software_wgpu_adapter(); - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - force_fallback_adapter: force_software_adapter, - compatible_surface: None, - })) - .map_err(|error| format!("Camera blur adapter unavailable: {error}"))?; - - let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { - label: Some("WS Blur Device"), - required_features: wgpu::Features::empty(), - required_limits: - wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()), - memory_hints: Default::default(), - trace: wgpu::Trace::Off, - })) - .map_err(|error| format!("Camera blur device unavailable: {error}"))?; - - let mut processor = - cap_camera_effects::BlurProcessor::new(&device, wgpu::TextureFormat::Rgba8Unorm) - .map_err(|error| format!("Camera blur processor unavailable: {error}"))?; - processor.set_inference_interval(WS_BLUR_INFERENCE_INTERVAL); - - tracing::info!("WebSocket camera blur processor initialized (headless)"); - - Ok(WsBlurResources { - device, - queue, - processor, - source_texture: None, - readbacks: None, - current_idx: 0, - mode: None, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn packs_plane_rows_without_stride_padding() { - let src = [0, 1, 2, 3, 90, 91, 4, 5, 6, 7, 92, 93, 8, 9, 10, 11, 94, 95]; - let mut dst = vec![99]; - - pack_plane_rows(&mut dst, &src, 4, 3, 6); - - assert_eq!(dst, [99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - } - - #[test] - fn packs_contiguous_plane_as_one_slice() { - let src = [0, 1, 2, 3, 4, 5, 6, 7]; - let mut dst = Vec::new(); - - pack_plane_rows(&mut dst, &src, 4, 2, 4); - - assert_eq!(dst, src); - } - - #[cfg(target_os = "linux")] - fn receipt(generation: u64, captured: Instant) -> crate::linux_instant_camera::FrameReceipt { - crate::linux_instant_camera::FrameReceipt { - timestamp: cap_timestamp::Timestamp::Instant(captured), - generation, - processing: cap_recording::instant_recording::LinuxCameraProcessing { - mirrored: false, - blur: cap_recording::instant_recording::LinuxCameraBlur::Off, - }, - dimensions: (2, 1), - blur: None, - } - } - - #[cfg(target_os = "linux")] - #[tokio::test] - async fn asynchronous_readback_keeps_its_original_capture_and_generation() { - let first = Instant::now(); - let second = first + Duration::from_millis(10); - let mut a = ReadbackTicket::new(Some(receipt(7, first))); - let mut b = ReadbackTicket::new(Some(receipt(8, second))); - let a_ready = a.status.clone(); - let b_ready = b.status.clone(); - let (release, delayed) = tokio::sync::oneshot::channel(); - let (ready, notified) = tokio::sync::oneshot::channel(); - let worker = tokio::spawn(async move { - b_ready.store(WS_READBACK_READY_OK, Ordering::Release); - ready.send(()).unwrap(); - delayed.await.unwrap(); - a_ready.store(WS_READBACK_READY_OK, Ordering::Release); - }); - notified.await.unwrap(); - assert!(a.take_ready().unwrap().is_none()); - let newer = b.take_ready().unwrap().unwrap().unwrap(); - assert_eq!(newer.generation, 8); - assert!(matches!(newer.timestamp,cap_timestamp::Timestamp::Instant(at) if at==second)); - assert!(b.take_ready().unwrap().is_none()); - release.send(()).unwrap(); - worker.await.unwrap(); - let older = a.take_ready().unwrap().unwrap().unwrap(); - assert_eq!(older.generation, 7); - assert!(matches!(older.timestamp,cap_timestamp::Timestamp::Instant(at) if at==first)); - } - - #[cfg(target_os = "linux")] - #[tokio::test] - async fn cancelled_readback_callback_cannot_certify_a_new_lease() { - let old = ReadbackTicket::new(Some(receipt(1, Instant::now()))); - let callback = old.status.clone(); - drop(old); - let mut fresh = ReadbackTicket::new(Some(receipt(2, Instant::now()))); - tokio::spawn(async move { - callback.store(WS_READBACK_READY_OK, Ordering::Release); - }) - .await - .unwrap(); - assert!(fresh.take_ready().unwrap().is_none()); - fresh.status.store(WS_READBACK_READY_ERR, Ordering::Release); - assert!(fresh.take_ready().is_err()); - assert!(fresh.take_ready().unwrap().is_none()); - } - - #[cfg(target_os = "linux")] - #[tokio::test] - async fn hidden_websocket_does_not_disable_recording_demand_or_command_delivery() { - let (frame_tx, frames) = flume::bounded::(2); - let (command_tx, commands) = flume::bounded::(1); - let frame_input = frames.clone(); - let command_input = commands.clone(); - let waiting = tokio::task::spawn_blocking(move || { - next_worker_event(&frame_input, &command_input, None) - }); - command_tx.send_async(7).await.unwrap(); - assert!(matches!(waiting.await.unwrap(), WorkerEvent::Command(7))); - assert!(processing_needed(false, true)); - frame_tx.send_async(42).await.unwrap(); - assert!(matches!( - next_worker_event(&frames, &commands, Some(Duration::from_secs(1))), - WorkerEvent::Frame(42) - )); - assert!(!processing_needed(false, false)); - } - - #[cfg(target_os = "linux")] - #[tokio::test] - async fn active_worker_polls_cancellation_without_needing_a_camera_frame() { - let (_frame_tx, frames) = flume::bounded::(2); - let (_command_tx, commands) = flume::bounded::(1); - let waiting = tokio::task::spawn_blocking(move || { - next_worker_event(&frames, &commands, Some(Duration::from_millis(1))) - }); - assert!(matches!( - tokio::time::timeout(Duration::from_secs(1), waiting) - .await - .unwrap() - .unwrap(), - WorkerEvent::Tick - )); - } -} diff --git a/apps/desktop/src-tauri/src/camera_native.rs b/apps/desktop/src-tauri/src/camera_native.rs deleted file mode 100644 index 9d398845128..00000000000 --- a/apps/desktop/src-tauri/src/camera_native.rs +++ /dev/null @@ -1,356 +0,0 @@ -use cap_recording::NativeCameraFrame; -use cap_rendering::iosurface_texture::{ - IOSurfaceTextureCache, IOSurfaceTextureError, import_metal_texture_to_wgpu, -}; - -// Converts IOSurface-backed camera frames into an RGBA destination texture -// entirely on the GPU: the CVPixelBuffer planes are imported as Metal textures -// (zero CPU copies) and a fullscreen pass does YUV->RGB plus downscaling in -// one step. BT.601 matches what the swscale CPU path produced for untagged -// webcam streams. -const CONVERT_SHADER: &str = r#" -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv: vec2, -} - -@vertex -fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { - var positions = array, 3>( - vec2(-1.0, -1.0), - vec2(3.0, -1.0), - vec2(-1.0, 3.0), - ); - var uvs = array, 3>( - vec2(0.0, 1.0), - vec2(2.0, 1.0), - vec2(0.0, -1.0), - ); - var out: VertexOutput; - out.position = vec4(positions[vi], 0.0, 1.0); - out.uv = uvs[vi]; - return out; -} - -@group(0) @binding(0) var frame_sampler: sampler; -@group(0) @binding(1) var plane0_texture: texture_2d; -@group(0) @binding(2) var uv_texture: texture_2d; - -fn yuv_video_range(y: f32, u: f32, v: f32) -> vec3 { - let yy = y - 0.0625; - let uu = u - 0.5; - let vv = v - 0.5; - return clamp( - vec3( - 1.164 * yy + 1.596 * vv, - 1.164 * yy - 0.392 * uu - 0.813 * vv, - 1.164 * yy + 2.017 * uu, - ), - vec3(0.0), - vec3(1.0), - ); -} - -fn yuv_full_range(y: f32, u: f32, v: f32) -> vec3 { - let uu = u - 0.5; - let vv = v - 0.5; - return clamp( - vec3( - y + 1.402 * vv, - y - 0.344 * uu - 0.714 * vv, - y + 1.772 * uu, - ), - vec3(0.0), - vec3(1.0), - ); -} - -@fragment -fn fs_nv12_video(in: VertexOutput) -> @location(0) vec4 { - let y = textureSample(plane0_texture, frame_sampler, in.uv).r; - let uv = textureSample(uv_texture, frame_sampler, in.uv).rg; - return vec4(yuv_video_range(y, uv.r, uv.g), 1.0); -} - -@fragment -fn fs_nv12_full(in: VertexOutput) -> @location(0) vec4 { - let y = textureSample(plane0_texture, frame_sampler, in.uv).r; - let uv = textureSample(uv_texture, frame_sampler, in.uv).rg; - return vec4(yuv_full_range(y, uv.r, uv.g), 1.0); -} - -@fragment -fn fs_bgra(in: VertexOutput) -> @location(0) vec4 { - let color = textureSample(plane0_texture, frame_sampler, in.uv); - return vec4(color.rgb, 1.0); -} -"#; - -#[derive(Debug)] -pub enum NativeFrameError { - NoImageBuffer, - NoFormatDesc, - UnsupportedFormat(String), - Surface(IOSurfaceTextureError), -} - -impl std::fmt::Display for NativeFrameError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NoImageBuffer => write!(f, "sample buffer has no image buffer"), - Self::NoFormatDesc => write!(f, "sample buffer has no format description"), - Self::UnsupportedFormat(fourcc) => { - write!(f, "unsupported pixel format for GPU path: {fourcc}") - } - Self::Surface(err) => write!(f, "{err}"), - } - } -} - -impl From for NativeFrameError { - fn from(err: IOSurfaceTextureError) -> Self { - Self::Surface(err) - } -} - -pub enum NativeFrameKind { - Nv12 { full_range: bool }, - Bgra, -} - -pub fn classify_frame(frame: &NativeCameraFrame) -> Result { - let format_desc = frame - .sample_buf - .format_desc() - .ok_or(NativeFrameError::NoFormatDesc)?; - let mut fourcc = format_desc.media_sub_type().to_be_bytes(); - match cidre::four_cc_to_str(&mut fourcc) { - "420v" => Ok(NativeFrameKind::Nv12 { full_range: false }), - "420f" => Ok(NativeFrameKind::Nv12 { full_range: true }), - "BGRA" => Ok(NativeFrameKind::Bgra), - other => Err(NativeFrameError::UnsupportedFormat(other.to_string())), - } -} - -pub struct NativeFrameConverter { - cache: IOSurfaceTextureCache, - sampler: wgpu::Sampler, - nv12_layout: wgpu::BindGroupLayout, - single_layout: wgpu::BindGroupLayout, - nv12_video_pipeline: wgpu::RenderPipeline, - nv12_full_pipeline: wgpu::RenderPipeline, - bgra_pipeline: wgpu::RenderPipeline, -} - -impl NativeFrameConverter { - pub fn new(device: &wgpu::Device) -> Option { - let cache = IOSurfaceTextureCache::new()?; - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Camera Native Convert Shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(CONVERT_SHADER)), - }); - - let sampler_entry = wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }; - let texture_entry = |binding: u32| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }; - - let nv12_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Camera NV12 Layout"), - entries: &[sampler_entry, texture_entry(1), texture_entry(2)], - }); - let single_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Camera BGRA Layout"), - entries: &[sampler_entry, texture_entry(1)], - }); - - let make_pipeline = |layout: &wgpu::BindGroupLayout, entry_point: &str| { - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[layout], - push_constant_ranges: &[], - }); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Camera Native Convert Pipeline"), - 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(entry_point), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: Default::default(), - multiview: None, - cache: None, - }) - }; - - let nv12_video_pipeline = make_pipeline(&nv12_layout, "fs_nv12_video"); - let nv12_full_pipeline = make_pipeline(&nv12_layout, "fs_nv12_full"); - let bgra_pipeline = make_pipeline(&single_layout, "fs_bgra"); - - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); - - Some(Self { - cache, - sampler, - nv12_layout, - single_layout, - nv12_video_pipeline, - nv12_full_pipeline, - bgra_pipeline, - }) - } - - pub fn render_frame( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - frame: &NativeCameraFrame, - kind: &NativeFrameKind, - dst_view: &wgpu::TextureView, - ) -> Result<(), NativeFrameError> { - let image_buf = frame - .sample_buf - .image_buf() - .ok_or(NativeFrameError::NoImageBuffer)?; - let io_surface = image_buf.io_surf().ok_or(NativeFrameError::Surface( - IOSurfaceTextureError::NoIOSurface, - ))?; - let width = image_buf.width() as u32; - let height = image_buf.height() as u32; - - let (pipeline, bind_group) = match kind { - NativeFrameKind::Nv12 { full_range } => { - let y_metal = self.cache.create_y_texture(io_surface, width, height)?; - let uv_metal = self.cache.create_uv_texture(io_surface, width, height)?; - let y_texture = import_metal_texture_to_wgpu( - device, - &y_metal, - wgpu::TextureFormat::R8Unorm, - width, - height, - Some("Camera Y Plane"), - )?; - let uv_texture = import_metal_texture_to_wgpu( - device, - &uv_metal, - wgpu::TextureFormat::Rg8Unorm, - width / 2, - height / 2, - Some("Camera UV Plane"), - )?; - let y_view = y_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let uv_view = uv_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Camera NV12 Bind Group"), - layout: &self.nv12_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&self.sampler), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&y_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&uv_view), - }, - ], - }); - let pipeline = if *full_range { - &self.nv12_full_pipeline - } else { - &self.nv12_video_pipeline - }; - (pipeline, bind_group) - } - NativeFrameKind::Bgra => { - let bgra_metal = self.cache.create_bgra_texture(io_surface, width, height)?; - let bgra_texture = import_metal_texture_to_wgpu( - device, - &bgra_metal, - wgpu::TextureFormat::Bgra8Unorm, - width, - height, - Some("Camera BGRA"), - )?; - let view = bgra_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Camera BGRA Bind Group"), - layout: &self.single_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&self.sampler), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&view), - }, - ], - }); - (&self.bgra_pipeline, bind_group) - } - }; - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Camera Native Convert"), - }); - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Camera Native Convert Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: dst_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - pass.set_pipeline(pipeline); - pass.set_bind_group(0, &bind_group, &[]); - pass.draw(0..3, 0..1); - } - queue.submit(std::iter::once(encoder.finish())); - - Ok(()) - } -} diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-tauri/src/captions.rs deleted file mode 100644 index b64305aa478..00000000000 --- a/apps/desktop/src-tauri/src/captions.rs +++ /dev/null @@ -1,2910 +0,0 @@ -use anyhow::Result; -use cap_audio::{ - AudioData, TranscriptionAudioSource, TranscriptionAudioTake, append_transcription_audio, -}; -use cap_rendering::Video; -use ffmpeg::{ - ChannelLayout, codec as avcodec, - format::{self as avformat}, - software::resampling, -}; -use futures::StreamExt; -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -use parakeet_rs::{ParakeetTDT, TimestampMode, Transcriber}; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::collections::HashMap; -use std::fs::File; -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}; -use tracing::instrument; -use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; - -pub use cap_project::{ - CaptionSegment, CaptionSettings, CaptionWord, RecordingMeta, StudioRecordingMeta, -}; - -use crate::{general_settings::GeneralSettingsStore, http_client}; - -#[cfg(all(target_os = "macos", target_arch = "x86_64"))] -const PARAKEET_UNSUPPORTED_MESSAGE: &str = "Parakeet transcription is not available on Intel macOS"; - -#[derive(Debug, Serialize, Deserialize, Type, Clone)] -pub enum TranscriptionEngine { - Whisper, - Parakeet, -} - -#[derive(Debug, Serialize, Deserialize, Type, Clone)] -pub struct CaptionData { - pub segments: Vec, - pub settings: Option, -} - -impl Default for CaptionData { - fn default() -> Self { - Self { - segments: Vec::new(), - settings: Some(CaptionSettings::default()), - } - } -} - -lazy_static::lazy_static! { - static ref WHISPER_CONTEXT: Arc>> = Arc::new(Mutex::new(None)); - static ref MODEL_DOWNLOADS: Mutex> = Mutex::new(HashMap::new()); - static ref TRANSCRIPTION_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); -} - -struct CachedWhisperContext { - model_path: String, - context: Arc, -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -lazy_static::lazy_static! { - static ref PARAKEET_CONTEXT: Mutex> = Mutex::new(None); -} - -const WHISPER_SAMPLE_RATE: u32 = 16000; -const TARGET_CAPTION_WORDS_PER_SEGMENT: usize = 6; -const MAX_CAPTION_WORDS_PER_SEGMENT: usize = 8; -const MIN_FINAL_CAPTION_WORDS: usize = 3; -// Whisper/Parakeet sometimes stretch a trailing word's end across a following -// silence (e.g. a 16s "seconds."), which leaves the rendered caption stuck on -// screen and duplicates the word across timeline cuts once projected. Real -// spoken words never approach this, so cap each word's duration to keep timing -// tied to speech rather than silence. -const MAX_CAPTION_WORD_DURATION: f32 = 2.5; - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -struct CachedParakeetContext { - model_dir: String, - model: Arc>, -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -fn parakeet_model_dir_matches(cached_model_dir: &str, model_dir: &Path) -> bool { - cached_model_dir == model_dir.to_string_lossy() -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -async fn invalidate_parakeet_cache_for_dir(model_dir: &Path) { - let mut ctx = PARAKEET_CONTEXT.lock().await; - if ctx - .as_ref() - .is_some_and(|cached| parakeet_model_dir_matches(&cached.model_dir, model_dir)) - { - tracing::info!( - "Invalidating cached Parakeet context for {}", - model_dir.display() - ); - *ctx = None; - } -} - -#[cfg(all(target_os = "macos", target_arch = "x86_64"))] -async fn invalidate_parakeet_cache_for_dir(_model_dir: &Path) {} - -async fn invalidate_whisper_cache_for_path(model_path: &Path) { - let cached_context = { - let mut context = WHISPER_CONTEXT.lock().await; - if context - .as_ref() - .is_some_and(|cached| Path::new(&cached.model_path) == model_path) - { - tracing::info!("Releasing deleted Whisper model from cache"); - context.take() - } else { - None - } - }; - drop(cached_context); -} - -pub async fn release_ml_models() { - { - let mut ctx = WHISPER_CONTEXT.lock().await; - if ctx.is_some() { - tracing::info!("Releasing Whisper context to free memory"); - *ctx = None; - } - } - #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] - { - let mut ctx = PARAKEET_CONTEXT.lock().await; - if ctx.is_some() { - tracing::info!("Releasing Parakeet context to free memory"); - *ctx = None; - } - } -} - -fn normalize_relative_components(path: &Path) -> Result { - let mut normalized = PathBuf::new(); - - for component in path.components() { - match component { - Component::Normal(part) => normalized.push(part), - Component::CurDir => {} - Component::ParentDir => { - return Err("Path is outside the app data directory".to_string()); - } - Component::RootDir => { - return Err("Path is outside the app data directory".to_string()); - } - Component::Prefix(_) => { - return Err("Path is outside the app data directory".to_string()); - } - } - } - - Ok(normalized) -} - -fn resolve_path_with_base(base_dir: &Path, path: &str) -> Result { - if !base_dir.exists() { - std::fs::create_dir_all(base_dir) - .map_err(|e| format!("Failed to create app data directory: {e}"))?; - } - - let canonical_base = base_dir - .canonicalize() - .map_err(|e| format!("Failed to resolve app data directory: {e}"))?; - let requested = PathBuf::from(path); - let candidate = if requested.is_absolute() { - requested - } else { - canonical_base.join(normalize_relative_components(&requested)?) - }; - - let mut suffix = Vec::new(); - let mut current = candidate.as_path(); - - while !current.exists() { - let file_name = current - .file_name() - .ok_or_else(|| "Path is outside the app data directory".to_string())?; - suffix.push(file_name.to_os_string()); - current = current - .parent() - .ok_or_else(|| "Path is outside the app data directory".to_string())?; - } - - let mut resolved = current - .canonicalize() - .map_err(|e| format!("Failed to resolve path: {e}"))?; - - if !resolved.starts_with(&canonical_base) { - return Err("Path is outside the app data directory".to_string()); - } - - for component in suffix.into_iter().rev() { - resolved.push(component); - } - - Ok(resolved) -} - -fn validate_model_path(app: &AppHandle, path: &str) -> Result { - let app_data_dir = app - .path() - .app_local_data_dir() - .map_err(|_| "Failed to get app local data directory".to_string())?; - - resolve_path_with_base(&app_data_dir, path) -} - -#[tauri::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] -#[specta::specta] -#[instrument] -pub async fn save_model_file(path: String, data: Vec) -> Result<(), String> { - std::fs::write(&path, &data).map_err(|e| format!("Failed to write model file: {e}")) -} - -enum AudioExtractionSource { - ProjectDirectory { base_path: PathBuf }, - MediaFile(PathBuf), -} - -fn resolve_audio_extraction_source(video_path: &str) -> Result { - let path = PathBuf::from(video_path); - let metadata = - std::fs::metadata(&path).map_err(|e| format!("Failed to read video path metadata: {e}"))?; - - if metadata.is_dir() { - let meta_path = path.join("recording-meta.json"); - if !meta_path.is_file() { - return Err("Recording directory is missing recording-meta.json".to_string()); - } - - return Ok(AudioExtractionSource::ProjectDirectory { base_path: path }); - } - - if metadata.is_file() { - return Ok(AudioExtractionSource::MediaFile(path)); - } - - Err("Video path is neither a file nor a recording directory".to_string()) -} - -async fn extract_audio_from_video(video_path: &str, output_path: &PathBuf) -> Result<(), String> { - log::info!("=== EXTRACT AUDIO START ==="); - log::info!("Attempting to extract audio from: {video_path}"); - log::info!("Output path: {output_path:?}"); - - match resolve_audio_extraction_source(video_path)? { - AudioExtractionSource::ProjectDirectory { base_path } => { - log::info!("Detected recording project directory"); - - let recording_meta = RecordingMeta::load_for_project(&base_path) - .map_err(|e| format!("Failed to read recording metadata: {e}"))?; - let studio = recording_meta - .studio_meta() - .ok_or_else(|| "Only studio recordings can be transcribed".to_string())?; - studio.ensure_ordinary_media_access(&base_path)?; - - let mut final_samples = Vec::new(); - let mut any_audio = false; - match studio { - StudioRecordingMeta::SingleSegment { segment } => { - let display_duration = Video::new( - segment.display.path.to_path(&base_path), - segment.display.start_time.unwrap_or_default(), - ) - .map_err(|error| format!("Failed to read display video: {error}"))? - .duration; - let mut sources = Vec::new(); - if let Some(audio) = &segment.audio { - let path = audio.path.to_path(&base_path); - if path.exists() { - match AudioData::from_file(&path) { - Ok(decoded) => { - any_audio = true; - sources.push(TranscriptionAudioSource { - samples: decoded.samples().to_vec(), - channels: decoded.channels() as usize, - offset_secs: 0.0, - }); - } - Err(error) => { - log::warn!("Failed to process audio source {path:?}: {error}") - } - } - } - } - let take = TranscriptionAudioTake { - display_duration_secs: display_duration, - sources, - }; - append_transcription_audio(&mut final_samples, &take, AudioData::SAMPLE_RATE) - .map_err(|e| format!("Failed to assemble transcription audio: {e}"))?; - } - StudioRecordingMeta::MultipleSegments { inner } => { - for (segment_idx, segment) in inner.segments.iter().enumerate() { - let display_duration = Video::new( - segment.display.path.to_path(&base_path), - segment.display.start_time.unwrap_or_default(), - ) - .map_err(|error| { - format!( - "Failed to read display video for segment {segment_idx}: {error}" - ) - })? - .duration; - let offsets = segment.calculate_audio_offsets(); - let mut sources = Vec::new(); - let mut push_source = |path: &cap_project::AudioMeta, offset_secs: f64| { - let path = path.path.to_path(&base_path); - if !path.exists() { - return; - } - match AudioData::from_file(&path) { - Ok(decoded) => { - any_audio = true; - sources.push(TranscriptionAudioSource { - samples: decoded.samples().to_vec(), - channels: decoded.channels() as usize, - offset_secs, - }); - } - Err(error) => { - log::warn!("Failed to process audio source {path:?}: {error}") - } - } - }; - - if let Some(system_audio) = &segment.system_audio { - push_source(system_audio, f64::from(offsets.system_audio)); - } - if let Some(mic) = &segment.mic { - push_source(mic, f64::from(offsets.mic)); - } - - let take = TranscriptionAudioTake { - display_duration_secs: display_duration, - sources, - }; - append_transcription_audio( - &mut final_samples, - &take, - AudioData::SAMPLE_RATE, - ) - .map_err(|e| format!("Failed to assemble transcription audio: {e}"))?; - } - } - } - - if !any_audio { - return Err("No audio sources found in the recording metadata".to_string()); - } - - let mut mixed_samples = final_samples; - let channel_count = 1_usize; - - if mixed_samples.is_empty() { - log::error!("No audio samples after processing all sources"); - return Err("Failed to process any audio sources".to_string()); - } - - let gain = normalize_audio_for_transcription(&mut mixed_samples); - if (gain - 1.0).abs() > 0.01 { - log::info!("Applied transcription audio gain: {gain:.2}x"); - } - - log::info!("Final mixed audio: {} samples", mixed_samples.len()); - let mix_rms = (mixed_samples.iter().map(|&s| s * s).sum::() - / mixed_samples.len() as f32) - .sqrt(); - log::info!("Mixed audio RMS: {mix_rms:.4}"); - - if mix_rms < 0.001 { - log::warn!( - "WARNING: Mixed audio RMS is very low ({mix_rms:.6}) - audio may be nearly silent!" - ); - } - - let mut output = avformat::output(&output_path) - .map_err(|e| format!("Failed to create output file: {e}"))?; - - let codec = avcodec::encoder::find_by_name("pcm_s16le") - .ok_or_else(|| "PCM encoder not found".to_string())?; - - let mut encoder = avcodec::Context::new() - .encoder() - .audio() - .map_err(|e| format!("Failed to create encoder: {e}"))?; - - encoder.set_rate(WHISPER_SAMPLE_RATE as i32); - let channel_layout = ChannelLayout::MONO; - encoder.set_channel_layout(channel_layout); - encoder.set_format(avformat::Sample::I16(avformat::sample::Type::Packed)); - - let mut encoder = encoder - .open_as(codec) - .map_err(|e| format!("Failed to open encoder: {e}"))?; - - let mut stream = output - .add_stream(codec) - .map_err(|e| format!("Failed to add stream: {e}"))?; - stream.set_parameters(&encoder); - - output - .write_header() - .map_err(|e| format!("Failed to write header: {e}"))?; - - let mut resampler = resampling::Context::get( - avformat::Sample::F32(avformat::sample::Type::Packed), - channel_layout, - AudioData::SAMPLE_RATE, - avformat::Sample::I16(avformat::sample::Type::Packed), - channel_layout, - WHISPER_SAMPLE_RATE, - ) - .map_err(|e| format!("Failed to create resampler: {e}"))?; - - let frame_size = encoder.frame_size() as usize; - let frame_size = if frame_size == 0 { 1024 } else { frame_size }; - - log::info!( - "Using frame size: {}, total samples: {}, channel count: {}", - frame_size, - mixed_samples.len(), - channel_count - ); - - let mut frame = ffmpeg::frame::Audio::new( - avformat::Sample::I16(avformat::sample::Type::Packed), - frame_size, - ChannelLayout::MONO, - ); - frame.set_rate(WHISPER_SAMPLE_RATE); - - if !mixed_samples.is_empty() && frame_size * channel_count > 0 { - for (chunk_idx, chunk) in - mixed_samples.chunks(frame_size * channel_count).enumerate() - { - if chunk_idx % 100 == 0 { - log::info!("Processing chunk {}, size: {}", chunk_idx, chunk.len()); - } - - let mut input_frame = ffmpeg::frame::Audio::new( - avformat::Sample::F32(avformat::sample::Type::Packed), - chunk.len() / channel_count, - channel_layout, - ); - input_frame.set_rate(AudioData::SAMPLE_RATE); - - let bytes = unsafe { - std::slice::from_raw_parts( - chunk.as_ptr() as *const u8, - std::mem::size_of_val(chunk), - ) - }; - input_frame.data_mut(0)[0..bytes.len()].copy_from_slice(bytes); - - let mut output_frame = ffmpeg::frame::Audio::new( - avformat::Sample::I16(avformat::sample::Type::Packed), - frame_size, - ChannelLayout::MONO, - ); - output_frame.set_rate(WHISPER_SAMPLE_RATE); - - match resampler.run(&input_frame, &mut output_frame) { - Ok(_) => { - if chunk_idx % 100 == 0 { - log::info!( - "Successfully resampled chunk {}, output samples: {}", - chunk_idx, - output_frame.samples() - ); - } - } - Err(e) => { - log::error!("Failed to resample chunk {chunk_idx}: {e}"); - continue; - } - } - - if let Err(e) = encoder.send_frame(&output_frame) { - log::error!("Failed to send frame to encoder: {e}"); - continue; - } - - loop { - let mut packet = ffmpeg::Packet::empty(); - match encoder.receive_packet(&mut packet) { - Ok(_) => { - if let Err(e) = packet.write_interleaved(&mut output) { - log::error!("Failed to write packet: {e}"); - } - } - Err(_) => break, - } - } - } - } - - encoder - .send_eof() - .map_err(|e| format!("Failed to send EOF: {e}"))?; - - loop { - let mut packet = ffmpeg::Packet::empty(); - let received = encoder.receive_packet(&mut packet); - - if received.is_err() { - break; - } - - { - if let Err(e) = packet.write_interleaved(&mut output) { - return Err(format!("Failed to write final packet: {e}")); - } - } - } - - output - .write_trailer() - .map_err(|e| format!("Failed to write trailer: {e}"))?; - - log::info!("=== EXTRACT AUDIO END (from recording project) ==="); - Ok(()) - } - AudioExtractionSource::MediaFile(video_path) => { - let mut input = avformat::input(&video_path) - .map_err(|e| format!("Failed to open video file: {e}"))?; - - let stream = input - .streams() - .best(ffmpeg::media::Type::Audio) - .ok_or_else(|| "No audio stream found".to_string())?; - - let codec_params = stream.parameters(); - - let decoder_ctx = avcodec::Context::from_parameters(codec_params.clone()) - .map_err(|e| format!("Failed to create decoder context: {e}"))?; - - let mut decoder = decoder_ctx - .decoder() - .audio() - .map_err(|e| format!("Failed to create decoder: {e}"))?; - - let decoder_format = decoder.format(); - let decoder_channel_layout = decoder.channel_layout(); - let decoder_rate = decoder.rate(); - - let channel_layout = ChannelLayout::MONO; - - let mut encoder_ctx = avcodec::Context::new() - .encoder() - .audio() - .map_err(|e| format!("Failed to create encoder: {e}"))?; - - encoder_ctx.set_rate(WHISPER_SAMPLE_RATE as i32); - encoder_ctx.set_channel_layout(channel_layout); - encoder_ctx.set_format(avformat::Sample::I16(avformat::sample::Type::Packed)); - - let codec = avcodec::encoder::find_by_name("pcm_s16le") - .ok_or_else(|| "PCM encoder not found".to_string())?; - - let mut encoder = encoder_ctx - .open_as(codec) - .map_err(|e| format!("Failed to open encoder: {e}"))?; - - let mut output = avformat::output(&output_path) - .map_err(|e| format!("Failed to create output file: {e}"))?; - - let stream_params = { - let mut output_stream = output - .add_stream(codec) - .map_err(|e| format!("Failed to add stream: {e}"))?; - - output_stream.set_parameters(&encoder); - - (output_stream.index(), output_stream.id()) - }; - - output - .write_header() - .map_err(|e| format!("Failed to write header: {e}"))?; - - let mut resampler = resampling::Context::get( - decoder_format, - decoder_channel_layout, - decoder_rate, - avformat::Sample::I16(avformat::sample::Type::Packed), - channel_layout, - WHISPER_SAMPLE_RATE, - ) - .map_err(|e| format!("Failed to create resampler: {e}"))?; - - let mut decoded_frame = ffmpeg::frame::Audio::empty(); - let mut resampled_frame = ffmpeg::frame::Audio::new( - avformat::Sample::I16(avformat::sample::Type::Packed), - encoder.frame_size() as usize, - channel_layout, - ); - - let input_stream_index = stream.index(); - - let mut packet_queue = Vec::new(); - - { - for (stream_idx, packet) in input.packets() { - if stream_idx.index() == input_stream_index - && let Some(data) = packet.data() - { - let mut cloned_packet = ffmpeg::Packet::copy(data); - if let Some(pts) = packet.pts() { - cloned_packet.set_pts(Some(pts)); - } - if let Some(dts) = packet.dts() { - cloned_packet.set_dts(Some(dts)); - } - packet_queue.push(cloned_packet); - } - } - } - - for packet_res in packet_queue { - if let Err(e) = decoder.send_packet(&packet_res) { - log::warn!("Failed to send packet to decoder: {e}"); - continue; - } - - while decoder.receive_frame(&mut decoded_frame).is_ok() { - if let Err(e) = resampler.run(&decoded_frame, &mut resampled_frame) { - log::warn!("Failed to resample audio: {e}"); - continue; - } - - if let Err(e) = encoder.send_frame(&resampled_frame) { - log::warn!("Failed to send frame to encoder: {e}"); - continue; - } - - loop { - let mut packet = ffmpeg::Packet::empty(); - match encoder.receive_packet(&mut packet) { - Ok(_) => { - packet.set_stream(stream_params.0); - - if let Err(e) = packet.write_interleaved(&mut output) { - log::error!("Failed to write packet: {e}"); - } - } - Err(_) => break, - } - } - } - } - - decoder - .send_eof() - .map_err(|e| format!("Failed to send EOF to decoder: {e}"))?; - - while decoder.receive_frame(&mut decoded_frame).is_ok() { - resampler - .run(&decoded_frame, &mut resampled_frame) - .map_err(|e| format!("Failed to resample final audio: {e}"))?; - - encoder - .send_frame(&resampled_frame) - .map_err(|e| format!("Failed to send final frame: {e}"))?; - - loop { - let mut packet = ffmpeg::Packet::empty(); - let received = encoder.receive_packet(&mut packet); - - if received.is_err() { - break; - } - - packet - .write_interleaved(&mut output) - .map_err(|e| format!("Failed to write final packet: {e}"))?; - } - } - - output - .write_trailer() - .map_err(|e| format!("Failed to write trailer: {e}"))?; - - log::info!("=== EXTRACT AUDIO END (from video) ==="); - Ok(()) - } - } -} - -fn lock_transcription_worker_slot() -> std::sync::MutexGuard<'static, ()> { - TRANSCRIPTION_LOCK - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) -} - -fn get_whisper_context_blocking(model_path: &str) -> Result, String> { - cap_utils::local_captions::ensure_whisper_cpu_support()?; - let mut context_guard = WHISPER_CONTEXT.blocking_lock(); - - if let Some(ref existing) = *context_guard - && existing.model_path == model_path - { - log::info!("Reusing cached Whisper context"); - return Ok(existing.context.clone()); - } - - log::info!("Initializing Whisper context with model: {model_path}"); - let ctx = WhisperContext::new_with_params(model_path, WhisperContextParameters::default()) - .map_err(|e| format!("Failed to load Whisper model: {e}"))?; - - let ctx_arc = Arc::new(ctx); - *context_guard = Some(CachedWhisperContext { - model_path: model_path.to_string(), - context: ctx_arc.clone(), - }); - - Ok(ctx_arc) -} - -#[cfg(all(target_os = "macos", target_arch = "aarch64"))] -fn release_whisper_context_after_transcription() { - let mut ctx = WHISPER_CONTEXT.blocking_lock(); - *ctx = None; -} - -#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] -fn release_whisper_context_after_transcription() {} - -fn is_special_token(token_text: &str) -> bool { - let trimmed = token_text.trim(); - if trimmed.is_empty() { - return true; - } - - let is_special = trimmed.contains('[') - || trimmed.contains(']') - || trimmed.contains("_TT_") - || trimmed.contains("_BEG_") - || trimmed.contains("<|"); - - if is_special { - log::debug!("Filtering special token: {token_text:?}"); - } - - is_special -} - -fn caption_token_attaches_to_previous(text: &str) -> bool { - let Some(first_char) = text.trim().chars().next() else { - return false; - }; - - caption_char_attaches_to_previous(first_char) -} - -fn caption_char_attaches_to_previous(value: char) -> bool { - matches!( - value, - ',' | '.' - | '!' - | '?' - | ';' - | ':' - | '%' - | ')' - | ']' - | '}' - | '\'' - | '’' - | '、' - | '。' - | '!' - | '?' - | ';' - | ':' - | ',' - ) -} - -fn caption_boundary_word_is_weak(word: &CaptionWord) -> bool { - let normalized = word - .text - .trim() - .chars() - .filter(|c| c.is_alphanumeric()) - .collect::() - .to_lowercase(); - - normalized.len() <= 1 - || matches!( - normalized.as_str(), - "an" | "as" - | "at" - | "be" - | "by" - | "do" - | "he" - | "if" - | "in" - | "is" - | "it" - | "me" - | "my" - | "of" - | "on" - | "or" - | "so" - | "to" - | "up" - | "we" - ) -} - -fn normalize_caption_words(words: Vec) -> Vec { - let mut normalized: Vec = Vec::with_capacity(words.len()); - - for word in words { - let text = word.text.trim(); - if text.is_empty() { - continue; - } - - if caption_token_attaches_to_previous(text) - && let Some(previous) = normalized.last_mut() - { - previous.text.push_str(text); - previous.end = word.end; - } else { - normalized.push(CaptionWord { - text: text.to_string(), - start: word.start, - end: word.end, - }); - } - } - - for word in &mut normalized { - word.end = word.end.min(word.start + MAX_CAPTION_WORD_DURATION); - } - - normalized -} - -fn caption_text_from_words<'a>(words: impl IntoIterator) -> String { - let mut text = String::new(); - - for word in words { - let word_text = word.text.trim(); - if word_text.is_empty() { - continue; - } - - if !text.is_empty() && !caption_token_attaches_to_previous(word_text) { - text.push(' '); - } - text.push_str(word_text); - } - - text -} - -fn caption_word_chunks(words: &[CaptionWord]) -> Vec<&[CaptionWord]> { - let mut chunks = Vec::new(); - let mut start = 0; - - while start < words.len() { - let remaining = words.len() - start; - if remaining <= TARGET_CAPTION_WORDS_PER_SEGMENT { - chunks.push(&words[start..]); - break; - } - - let mut end = (start + TARGET_CAPTION_WORDS_PER_SEGMENT).min(words.len()); - while end < words.len() - && caption_boundary_word_is_weak(&words[end - 1]) - && end - start < MAX_CAPTION_WORDS_PER_SEGMENT - { - end += 1; - } - - let remaining_after = words.len() - end; - if remaining_after > 0 - && remaining_after < MIN_FINAL_CAPTION_WORDS - && caption_boundary_word_is_weak(&words[end]) - { - end = words.len(); - } - - chunks.push(&words[start..end]); - start = end; - } - - chunks -} - -fn normalize_audio_for_transcription(samples: &mut [f32]) -> f32 { - if samples.is_empty() { - return 1.0; - } - - let peak = samples - .iter() - .fold(0.0_f32, |max, sample| max.max(sample.abs())); - if peak <= f32::EPSILON { - return 1.0; - } - - let rms = - (samples.iter().map(|sample| sample * sample).sum::() / samples.len() as f32).sqrt(); - if rms <= f32::EPSILON { - return 1.0; - } - - let target_rms = 0.08_f32; - let desired_gain = (target_rms / rms).clamp(1.0, 8.0); - let peak_limited_gain = 0.98 / peak; - let gain = desired_gain.min(peak_limited_gain); - - if (gain - 1.0).abs() > 0.01 { - for sample in samples { - *sample = (*sample * gain).clamp(-0.98, 0.98); - } - } - - gain -} - -fn process_with_whisper( - audio_path: &PathBuf, - context: Arc, - language: &str, - transcription_hints: &[String], -) -> Result { - log::info!("=== WHISPER TRANSCRIPTION START ==="); - log::info!("Processing audio file: {audio_path:?}"); - log::info!("Language setting: {language}"); - - let mut params = FullParams::new(SamplingStrategy::BeamSearch { - beam_size: 5, - patience: 1.0, - }); - - params.set_translate(false); - params.set_print_special(false); - params.set_print_progress(false); - params.set_print_realtime(false); - params.set_token_timestamps(true); - params.set_language(Some(if language == "auto" { "auto" } else { language })); - params.set_max_len(i32::MAX); - - if let Some(initial_prompt) = build_initial_prompt(transcription_hints) { - params.set_initial_prompt(&initial_prompt); - } - - log::info!( - "Whisper params - translate: false, token_timestamps: true, beam_size: 5, max_len: MAX" - ); - - let mut audio_file = File::open(audio_path) - .map_err(|e| format!("Failed to open audio file: {e} at path: {audio_path:?}"))?; - let mut audio_data = Vec::new(); - audio_file - .read_to_end(&mut audio_data) - .map_err(|e| format!("Failed to read audio file: {e}"))?; - - log::info!("Processing audio file of size: {} bytes", audio_data.len()); - - let mut audio_data_f32 = Vec::new(); - for i in (0..audio_data.len()).step_by(2) { - if i + 1 < audio_data.len() { - let sample = i16::from_le_bytes([audio_data[i], audio_data[i + 1]]) as f32 / 32768.0; - audio_data_f32.push(sample); - } - } - - let gain = normalize_audio_for_transcription(&mut audio_data_f32); - if (gain - 1.0).abs() > 0.01 { - log::info!("Applied Whisper input gain: {gain:.2}x"); - } - - let duration_seconds = audio_data_f32.len() as f32 / WHISPER_SAMPLE_RATE as f32; - log::info!( - "Converted {} samples to f32 format (duration: {:.2}s at {}Hz)", - audio_data_f32.len(), - duration_seconds, - WHISPER_SAMPLE_RATE - ); - - if !audio_data_f32.is_empty() { - let min_sample = audio_data_f32.iter().fold(f32::MAX, |a, &b| a.min(b)); - let max_sample = audio_data_f32.iter().fold(f32::MIN, |a, &b| a.max(b)); - let avg_sample = audio_data_f32.iter().sum::() / audio_data_f32.len() as f32; - let rms = (audio_data_f32.iter().map(|&s| s * s).sum::() - / audio_data_f32.len() as f32) - .sqrt(); - log::info!( - "Audio samples - min: {min_sample:.4}, max: {max_sample:.4}, avg: {avg_sample:.6}, RMS: {rms:.4}" - ); - - if rms < 0.001 { - log::warn!("WARNING: Audio RMS is very low ({rms:.6}) - audio may be nearly silent!"); - } - - log::info!("First 20 audio samples:"); - for (i, sample) in audio_data_f32.iter().take(20).enumerate() { - log::info!(" Sample[{i}] = {sample:.6}"); - } - } - - let mut state = context - .create_state() - .map_err(|e| format!("Failed to create Whisper state: {e}"))?; - - state - .full(params, &audio_data_f32[..]) - .map_err(|e| format!("Failed to run Whisper transcription: {e}"))?; - - let num_segments = state - .full_n_segments() - .map_err(|e| format!("Failed to get number of segments: {e}"))?; - - log::info!("Found {num_segments} segments"); - - let mut segments = Vec::new(); - - for i in 0..num_segments { - let raw_text = state - .full_get_segment_text(i) - .map_err(|e| format!("Failed to get segment text: {e}"))?; - - let start_i64 = state - .full_get_segment_t0(i) - .map_err(|e| format!("Failed to get segment start time: {e}"))?; - let end_i64 = state - .full_get_segment_t1(i) - .map_err(|e| format!("Failed to get segment end time: {e}"))?; - - let start_time = (start_i64 as f32) / 100.0; - let end_time = (end_i64 as f32) / 100.0; - - // The transcript text itself stays out of the log: log files are - // uploaded from the Feedback page, and this is a verbatim record of - // what the user said on camera. - log::info!( - "=== Segment {}: start={:.2}s, end={:.2}s, chars={}", - i, - start_time, - end_time, - raw_text.trim().chars().count() - ); - - let mut words = Vec::new(); - let num_tokens = state - .full_n_tokens(i) - .map_err(|e| format!("Failed to get token count: {e}"))?; - - log::info!(" Segment {i} has {num_tokens} tokens"); - - let mut current_word = String::new(); - let mut word_start: Option = None; - let mut word_end: f32 = start_time; - - for t in 0..num_tokens { - let token_text = state.full_get_token_text(i, t).unwrap_or_default(); - let token_id = state.full_get_token_id(i, t).unwrap_or(0); - let token_prob = state.full_get_token_prob(i, t).unwrap_or(0.0); - - if is_special_token(&token_text) { - log::debug!( - " Token[{t}]: id={token_id}, text={token_text:?} -> SKIPPED (special)" - ); - continue; - } - - let token_data = state.full_get_token_data(i, t).ok(); - - if let Some(data) = token_data { - let token_start = (data.t0 as f32) / 100.0; - let token_end = (data.t1 as f32) / 100.0; - - // debug, not info: release builds filter at info, and these - // carry the transcript verbatim into an uploadable log. - log::debug!( - " Token[{t}]: id={token_id}, text={token_text:?}, t0={token_start:.2}s, t1={token_end:.2}s, prob={token_prob:.4}" - ); - - if token_text.starts_with(' ') || token_text.starts_with('\n') { - if !current_word.is_empty() - && let Some(ws) = word_start - { - log::debug!( - " -> Completing word: '{}' ({:.2}s - {:.2}s)", - current_word.trim(), - ws, - word_end - ); - words.push(CaptionWord { - text: current_word.trim().to_string(), - start: ws, - end: word_end, - }); - } - current_word = token_text.trim().to_string(); - word_start = Some(token_start); - log::debug!(" -> Starting new word: '{current_word}' at {token_start:.2}s"); - } else { - if word_start.is_none() { - word_start = Some(token_start); - log::debug!(" -> Word start set to {token_start:.2}s"); - } - current_word.push_str(&token_text); - log::debug!(" -> Appending to word: '{current_word}'"); - } - word_end = token_end; - } else { - log::warn!(" Token[{t}]: id={token_id}, text={token_text:?} -> NO TIMING DATA"); - } - } - - if !current_word.trim().is_empty() - && let Some(ws) = word_start - { - log::info!( - " -> Final word: '{}' ({:.2}s - {:.2}s)", - current_word.trim(), - ws, - word_end - ); - words.push(CaptionWord { - text: current_word.trim().to_string(), - start: ws, - end: word_end, - }); - } - - let words = normalize_caption_words(words); - - log::info!(" Segment {} produced {} words", i, words.len()); - for (w_idx, word) in words.iter().enumerate() { - log::info!( - " Word[{}]: '{}' ({:.2}s - {:.2}s)", - w_idx, - word.text, - word.start, - word.end - ); - } - - if words.is_empty() { - log::warn!(" Segment {i} has no words, skipping"); - continue; - } - - for (chunk_idx, chunk_words) in caption_word_chunks(&words).into_iter().enumerate() { - let segment_text = caption_text_from_words(chunk_words); - - let segment_start = chunk_words - .first() - .map(|word| word.start) - .unwrap_or(start_time); - let segment_end = chunk_words.last().map(|word| word.end).unwrap_or(end_time); - - segments.push(CaptionSegment { - id: format!("segment-{i}-{chunk_idx}"), - start: segment_start, - end: segment_end, - text: segment_text, - words: chunk_words.to_vec(), - }); - } - } - - log::info!("=== WHISPER TRANSCRIPTION COMPLETE ==="); - log::info!("Total segments: {}", segments.len()); - - let total_words: usize = segments.iter().map(|s| s.words.len()).sum(); - log::info!("Total words: {total_words}"); - - log::info!("=== FINAL TRANSCRIPTION SUMMARY ==="); - for segment in &segments { - log::info!( - "Segment '{}' ({:.2}s - {:.2}s): {}", - segment.id, - segment.start, - segment.end, - segment.text - ); - } - log::info!("=== END SUMMARY ==="); - - Ok(CaptionData { - segments, - settings: Some(cap_project::CaptionSettings::default()), - }) -} - -fn build_initial_prompt(transcription_hints: &[String]) -> Option { - let mut normalized = Vec::new(); - - for hint in transcription_hints { - let value = hint.replace('\0', "").trim().to_string(); - if value.is_empty() || normalized.contains(&value) { - continue; - } - normalized.push(value); - } - - if normalized.is_empty() { - None - } else { - Some(format!( - "Preferred spellings, names, and capitalization for this transcript: {}", - normalized.join("; ") - )) - } -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -fn process_with_parakeet( - audio_path: &std::path::Path, - model_dir: &str, -) -> Result { - tracing::info!("Processing audio file: {audio_path:?}"); - tracing::info!("Model directory: {model_dir}"); - - let cached_model = { - let guard = PARAKEET_CONTEXT.blocking_lock(); - guard.as_ref().and_then(|cached| { - if cached.model_dir == model_dir { - Some(Arc::clone(&cached.model)) - } else { - None - } - }) - }; - - let model_arc = if let Some(model) = cached_model { - tracing::info!("Reusing cached Parakeet TDT model"); - model - } else { - tracing::info!("Loading Parakeet TDT model from: {model_dir}"); - cap_camera_effects::initialize_onnx_runtime().map_err(|error| format!("{error:#}"))?; - let model = ParakeetTDT::from_pretrained(model_dir, None).map_err(|e| format!("{e}"))?; - let loaded_model = Arc::new(std::sync::Mutex::new(model)); - - let mut guard = PARAKEET_CONTEXT.blocking_lock(); - if let Some(cached) = guard - .as_ref() - .filter(|cached| cached.model_dir == model_dir) - { - tracing::info!("Reusing cached Parakeet TDT model"); - Arc::clone(&cached.model) - } else { - *guard = Some(CachedParakeetContext { - model_dir: model_dir.to_string(), - model: Arc::clone(&loaded_model), - }); - tracing::info!("Parakeet TDT model loaded successfully"); - loaded_model - } - }; - - let result = { - let mut parakeet = model_arc - .lock() - .map_err(|e| format!("Failed to lock Parakeet model: {e}"))?; - parakeet - .transcribe_file(audio_path, Some(TimestampMode::Words)) - .map_err(|e| format!("Parakeet transcription failed: {e}"))? - }; - - tracing::info!("Transcription text: {}", result.text); - tracing::info!("Got {} timed tokens", result.tokens.len()); - - let words = normalize_caption_words( - result - .tokens - .iter() - .filter(|t| !t.text.trim().is_empty()) - .map(|t| CaptionWord { - text: t.text.trim().to_string(), - start: t.start, - end: t.end, - }) - .collect(), - ); - - if words.is_empty() { - tracing::warn!("Parakeet produced no words"); - return Err("No speech detected in the audio".to_string()); - } - - let mut segments = Vec::new(); - - for (chunk_idx, chunk) in caption_word_chunks(&words).into_iter().enumerate() { - let segment_text = caption_text_from_words(chunk); - let segment_start = chunk.first().map(|w| w.start).unwrap_or(0.0); - let segment_end = chunk.last().map(|w| w.end).unwrap_or(0.0); - - segments.push(CaptionSegment { - id: format!("segment-{chunk_idx}"), - start: segment_start, - end: segment_end, - text: segment_text, - words: chunk.to_vec(), - }); - } - - tracing::info!("Total segments: {}", segments.len()); - tracing::info!( - "Total words: {}", - segments.iter().map(|s| s.words.len()).sum::() - ); - - Ok(CaptionData { - segments, - settings: Some(cap_project::CaptionSettings::default()), - }) -} - -#[cfg(all(target_os = "macos", target_arch = "x86_64"))] -fn process_with_parakeet( - _audio_path: &std::path::Path, - _model_dir: &str, -) -> Result { - Err(PARAKEET_UNSUPPORTED_MESSAGE.to_string()) -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub async fn transcribe_audio( - app: AppHandle, - video_path: String, - model_path: String, - language: String, - engine: TranscriptionEngine, -) -> Result { - log::info!("=== TRANSCRIBE AUDIO COMMAND START ==="); - log::info!("Video path: {}", video_path); - log::info!("Model path: {}", model_path); - log::info!("Language: {}", language); - - let validated_model_path = validate_model_path(&app, &model_path)?; - - if !std::path::Path::new(&video_path).exists() { - log::error!("Video file not found at path: {video_path}"); - return Err(format!("Video file not found at path: {video_path}")); - } - - if !validated_model_path.exists() { - log::error!("Model file not found at path: {model_path}"); - return Err(format!("Model file not found at path: {model_path}")); - } - - let model_path = validated_model_path.to_string_lossy().to_string(); - - let temp_dir = tempdir().map_err(|e| format!("Failed to create temporary directory: {e}"))?; - let audio_path = temp_dir.path().join("audio.wav"); - log::info!("Temp audio path: {:?}", audio_path); - - match extract_audio_from_video(&video_path, &audio_path).await { - Ok(_) => log::info!("Successfully extracted audio to {audio_path:?}"), - Err(e) => { - log::error!("Failed to extract audio: {e}"); - return Err(format!("Failed to extract audio from video: {e}")); - } - } - - if !audio_path.exists() { - log::error!("Audio file was not created at {audio_path:?}"); - return Err("Failed to create audio file for transcription".to_string()); - } - - let audio_metadata = std::fs::metadata(&audio_path).ok(); - if let Some(meta) = &audio_metadata { - log::info!( - "Audio file created at: {:?}, size: {} bytes", - audio_path, - meta.len() - ); - } - - let transcription_result = match engine { - TranscriptionEngine::Parakeet => { - log::info!("Using Parakeet TDT engine"); - let model_dir = model_path.clone(); - tokio::task::spawn_blocking(move || { - let _guard = lock_transcription_worker_slot(); - process_with_parakeet(&audio_path, &model_dir) - }) - .await - .map_err(|e| format!("Parakeet task panicked: {e}"))? - } - TranscriptionEngine::Whisper => { - let transcription_hints = GeneralSettingsStore::get(&app) - .ok() - .flatten() - .map(|settings| settings.transcription_hints) - .unwrap_or_default(); - - log::info!("Starting Whisper transcription in blocking task..."); - tokio::task::spawn_blocking(move || { - let _guard = lock_transcription_worker_slot(); - let context = match get_whisper_context_blocking(&model_path) { - Ok(ctx) => { - log::info!("Whisper context ready"); - ctx - } - Err(e) => { - log::error!("Failed to initialize Whisper context: {e}"); - return Err(format!("Failed to initialize transcription model: {e}")); - } - }; - let result = - process_with_whisper(&audio_path, context, &language, &transcription_hints); - release_whisper_context_after_transcription(); - result - }) - .await - .map_err(|e| format!("Whisper task panicked: {e}"))? - } - }; - - match transcription_result { - Ok(captions) => { - log::info!("=== TRANSCRIBE AUDIO RESULT ==="); - log::info!( - "Transcription produced {} segments", - captions.segments.len() - ); - - for (idx, segment) in captions.segments.iter().enumerate() { - log::info!( - " Result Segment[{}]: '{}' ({} words)", - idx, - segment.text, - segment.words.len() - ); - } - - if captions.segments.is_empty() { - log::warn!("No caption segments were generated"); - return Err("No speech detected in the audio".to_string()); - } - - log::info!("=== TRANSCRIBE AUDIO COMMAND END (success) ==="); - Ok(captions) - } - Err(e) => { - log::error!("Failed to process audio with Whisper: {e}"); - log::info!("=== TRANSCRIBE AUDIO COMMAND END (error) ==="); - Err(format!("Failed to transcribe audio: {e}")) - } - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn save_captions( - app: AppHandle, - video_id: String, - captions: CaptionData, -) -> Result<(), String> { - tracing::info!("=== SAVE CAPTIONS START ==="); - tracing::info!("Saving captions for video_id: {}", video_id); - tracing::info!("Received {} segments to save", captions.segments.len()); - - for (idx, segment) in captions.segments.iter().enumerate() { - tracing::info!( - " Segment[{}] '{}': '{}' ({} words, {:.2}s - {:.2}s)", - idx, - segment.id, - segment.text, - segment.words.len(), - segment.start, - segment.end - ); - for (w_idx, word) in segment.words.iter().enumerate() { - tracing::debug!( - " Word[{}]: '{}' ({:.2}s - {:.2}s)", - w_idx, - word.text, - word.start, - word.end - ); - } - } - - let captions_dir = app_captions_dir(&app, &video_id)?; - - if !captions_dir.exists() { - tracing::info!("Creating captions directory: {:?}", captions_dir); - std::fs::create_dir_all(&captions_dir).map_err(|e| { - tracing::error!("Failed to create captions directory: {}", e); - format!("Failed to create captions directory: {e}") - })?; - } - - let captions_path = captions_dir.join("captions.json"); - - tracing::info!("Writing captions to: {:?}", captions_path); - - let settings = captions.settings.unwrap_or_default(); - - let mut json_obj = serde_json::Map::new(); - - let segments_array = serde_json::to_value( - captions - .segments - .iter() - .map(|seg| { - let mut segment = serde_json::Map::new(); - segment.insert("id".to_string(), serde_json::Value::String(seg.id.clone())); - segment.insert( - "start".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(seg.start as f64).unwrap(), - ), - ); - segment.insert( - "end".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(seg.end as f64).unwrap(), - ), - ); - segment.insert( - "text".to_string(), - serde_json::Value::String(seg.text.clone()), - ); - let words_array: Vec = seg - .words - .iter() - .map(|w| { - serde_json::json!({ - "text": w.text, - "start": w.start, - "end": w.end - }) - }) - .collect(); - segment.insert("words".to_string(), serde_json::Value::Array(words_array)); - segment - }) - .collect::>(), - ) - .map_err(|e| { - tracing::error!("Failed to serialize captions segments: {}", e); - format!("Failed to serialize captions: {e}") - })?; - - json_obj.insert("segments".to_string(), segments_array); - - let mut settings_obj = serde_json::Map::new(); - settings_obj.insert( - "enabled".to_string(), - serde_json::Value::Bool(settings.enabled), - ); - settings_obj.insert( - "font".to_string(), - serde_json::Value::String(settings.font.clone()), - ); - settings_obj.insert( - "size".to_string(), - serde_json::Value::Number(serde_json::Number::from(settings.size)), - ); - settings_obj.insert( - "color".to_string(), - serde_json::Value::String(settings.color.clone()), - ); - settings_obj.insert( - "backgroundColor".to_string(), - serde_json::Value::String(settings.background_color.clone()), - ); - settings_obj.insert( - "backgroundOpacity".to_string(), - serde_json::Value::Number(serde_json::Number::from(settings.background_opacity)), - ); - settings_obj.insert( - "position".to_string(), - serde_json::Value::String(settings.position.clone()), - ); - settings_obj.insert( - "italic".to_string(), - serde_json::Value::Bool(settings.italic), - ); - settings_obj.insert( - "fontWeight".to_string(), - serde_json::Value::Number(serde_json::Number::from(settings.font_weight)), - ); - settings_obj.insert( - "outline".to_string(), - serde_json::Value::Bool(settings.outline), - ); - settings_obj.insert( - "outlineColor".to_string(), - serde_json::Value::String(settings.outline_color.clone()), - ); - settings_obj.insert( - "exportWithSubtitles".to_string(), - serde_json::Value::Bool(settings.export_with_subtitles), - ); - settings_obj.insert( - "highlightColor".to_string(), - serde_json::Value::String(settings.highlight_color.clone()), - ); - settings_obj.insert( - "fadeDuration".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(settings.fade_duration as f64).unwrap(), - ), - ); - settings_obj.insert( - "lingerDuration".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(settings.linger_duration as f64).unwrap(), - ), - ); - settings_obj.insert( - "wordTransitionDuration".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(settings.word_transition_duration as f64).unwrap(), - ), - ); - settings_obj.insert( - "activeWordHighlight".to_string(), - serde_json::Value::Bool(settings.active_word_highlight), - ); - - json_obj.insert( - "settings".to_string(), - serde_json::Value::Object(settings_obj), - ); - - let json = serde_json::to_string_pretty(&json_obj).map_err(|e| { - tracing::error!("Failed to serialize captions: {}", e); - format!("Failed to serialize captions: {e}") - })?; - - std::fs::write(captions_path, json).map_err(|e| { - tracing::error!("Failed to write captions file: {}", e); - format!("Failed to write captions file: {e}") - })?; - - tracing::info!("Successfully saved captions"); - tracing::info!("=== SAVE CAPTIONS END ==="); - Ok(()) -} - -pub fn parse_captions_json(json: &str) -> Result { - match serde_json::from_str::(json) { - Ok(json_value) => { - if let Some(segments_array) = json_value.get("segments").and_then(|v| v.as_array()) { - let mut segments = Vec::new(); - - for segment in segments_array { - if let (Some(id), Some(start), Some(end), Some(text)) = ( - segment.get("id").and_then(|v| v.as_str()), - segment.get("start").and_then(|v| v.as_f64()), - segment.get("end").and_then(|v| v.as_f64()), - segment.get("text").and_then(|v| v.as_str()), - ) { - let mut words = Vec::new(); - if let Some(words_array) = segment.get("words").and_then(|v| v.as_array()) { - for word in words_array { - if let (Some(w_text), Some(w_start), Some(w_end)) = ( - word.get("text").and_then(|v| v.as_str()), - word.get("start").and_then(|v| v.as_f64()), - word.get("end").and_then(|v| v.as_f64()), - ) { - words.push(cap_project::CaptionWord { - text: w_text.to_string(), - start: w_start as f32, - end: w_end as f32, - }); - } - } - } - segments.push(cap_project::CaptionSegment { - id: id.to_string(), - start: start as f32, - end: end as f32, - text: text.to_string(), - words, - }); - } - } - - let settings = if let Some(settings_obj) = json_value.get("settings") { - let enabled = settings_obj - .get("enabled") - .and_then(|v| v.as_bool()) - .unwrap_or_default(); - let font = settings_obj - .get("font") - .and_then(|v| v.as_str()) - .unwrap_or("System Sans-Serif") - .to_string(); - let size = settings_obj - .get("size") - .and_then(|v| v.as_u64()) - .unwrap_or(24) as u32; - let color = settings_obj - .get("color") - .and_then(|v| v.as_str()) - .unwrap_or("#A0A0A0") - .to_string(); - - let background_color = settings_obj - .get("backgroundColor") - .or_else(|| settings_obj.get("background_color")) - .and_then(|v| v.as_str()) - .unwrap_or("#000000") - .to_string(); - - let background_opacity = settings_obj - .get("backgroundOpacity") - .or_else(|| settings_obj.get("background_opacity")) - .and_then(|v| v.as_u64()) - .unwrap_or(80) as u32; - - let position = settings_obj - .get("position") - .and_then(|v| v.as_str()) - .unwrap_or("bottom") - .to_string(); - let italic = settings_obj - .get("italic") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - let font_weight = settings_obj - .get("fontWeight") - .or_else(|| settings_obj.get("font_weight")) - .and_then(|v| v.as_u64()) - .unwrap_or(700) as u32; - let outline = settings_obj - .get("outline") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let outline_color = settings_obj - .get("outlineColor") - .or_else(|| settings_obj.get("outline_color")) - .and_then(|v| v.as_str()) - .unwrap_or("#000000") - .to_string(); - - let export_with_subtitles = settings_obj - .get("exportWithSubtitles") - .or_else(|| settings_obj.get("export_with_subtitles")) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let highlight_color = settings_obj - .get("highlightColor") - .or_else(|| settings_obj.get("highlight_color")) - .and_then(|v| v.as_str()) - .unwrap_or("#FFFFFF") - .to_string(); - - let fade_duration = settings_obj - .get("fadeDuration") - .or_else(|| settings_obj.get("fade_duration")) - .and_then(|v| v.as_f64()) - .unwrap_or(0.15) as f32; - - let linger_duration = settings_obj - .get("lingerDuration") - .or_else(|| settings_obj.get("linger_duration")) - .and_then(|v| v.as_f64()) - .unwrap_or(0.4) as f32; - - let word_transition_duration = settings_obj - .get("wordTransitionDuration") - .or_else(|| settings_obj.get("word_transition_duration")) - .and_then(|v| v.as_f64()) - .unwrap_or(0.25) as f32; - - let active_word_highlight = settings_obj - .get("activeWordHighlight") - .or_else(|| settings_obj.get("active_word_highlight")) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let preset = settings_obj - .get("preset") - .and_then(|v| v.as_str()) - .unwrap_or("classic") - .to_string(); - - let animation = settings_obj - .get("animation") - .and_then(|v| v.as_str()) - .unwrap_or("bounce") - .to_string(); - - let highlight_style = settings_obj - .get("highlightStyle") - .or_else(|| settings_obj.get("highlight_style")) - .and_then(|v| v.as_str()) - .unwrap_or("color") - .to_string(); - - let uppercase = settings_obj - .get("uppercase") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let manual_position = settings_obj - .get("manualPosition") - .or_else(|| settings_obj.get("manual_position")) - .and_then(|value| { - Some(cap_project::XY { - x: value.get("x")?.as_f64()? as f32, - y: value.get("y")?.as_f64()? as f32, - }) - }); - - cap_project::CaptionSettings { - enabled, - font, - size, - color, - background_color, - background_opacity, - position, - italic, - font_weight, - outline, - outline_color, - export_with_subtitles, - highlight_color, - fade_duration, - linger_duration, - word_transition_duration, - active_word_highlight, - manual_position, - preset, - animation, - highlight_style, - uppercase, - } - } else { - cap_project::CaptionSettings::default() - }; - - Ok(cap_project::CaptionsData { - segments, - settings, - ..Default::default() - }) - } else { - Err("Missing or invalid segments array in captions file".to_string()) - } - } - Err(e) => Err(format!("Failed to parse captions JSON: {e}")), - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn load_captions( - app: AppHandle, - video_id: String, -) -> Result, String> { - tracing::info!("=== LOAD CAPTIONS START ==="); - tracing::info!("Loading captions for video_id: {}", video_id); - - let captions_dir = app_captions_dir(&app, &video_id)?; - let captions_path = captions_dir.join("captions.json"); - - if !captions_path.exists() { - tracing::info!("No captions file found at: {:?}", captions_path); - tracing::info!("=== LOAD CAPTIONS END (no file) ==="); - return Ok(None); - } - - tracing::info!("Reading captions from: {:?}", captions_path); - let json = match std::fs::read_to_string(captions_path.clone()) { - Ok(j) => j, - Err(e) => { - tracing::error!("Failed to read captions file: {}", e); - return Err(format!("Failed to read captions file: {e}")); - } - }; - - tracing::info!("Captions JSON length: {} bytes", json.len()); - - tracing::info!("Parsing captions JSON"); - match parse_captions_json(&json) { - Ok(project_captions) => { - tracing::info!( - "Successfully loaded {} caption segments", - project_captions.segments.len() - ); - - for (idx, segment) in project_captions.segments.iter().enumerate() { - tracing::info!( - " Loaded Segment[{}] '{}': '{}' ({} words, {:.2}s - {:.2}s)", - idx, - segment.id, - segment.text, - segment.words.len(), - segment.start, - segment.end - ); - } - - let tauri_captions = CaptionData { - segments: project_captions.segments, - settings: Some(project_captions.settings), - }; - - tracing::info!("=== LOAD CAPTIONS END (success) ==="); - Ok(Some(tauri_captions)) - } - Err(e) => { - tracing::error!("Failed to parse captions: {}", e); - tracing::info!("=== LOAD CAPTIONS END (error) ==="); - Err(format!("Failed to parse captions: {e}")) - } - } -} - -fn app_captions_dir(app: &AppHandle, video_id: &str) -> Result { - tracing::info!("Getting captions directory for video_id: {}", video_id); - - let app_dir = app - .path() - .app_data_dir() - .map_err(|_| "Failed to get app data directory".to_string())?; - - let clean_video_id = video_id.trim_end_matches(".cap"); - let captions_dir = app_dir.join("captions").join(clean_video_id); - - tracing::info!("Captions directory path: {:?}", captions_dir); - Ok(captions_dir) -} - -#[derive(Debug, Serialize, Type, tauri_specta::Event, Clone)] -pub struct DownloadProgress { - pub progress: f64, - pub message: String, -} - -#[derive(Debug, Serialize, Type, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum ModelDownloadState { - Downloading, - Completed, - Failed, -} - -#[derive(Debug, Serialize, Type, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ModelDownloadStatus { - pub state: ModelDownloadState, - pub progress: f64, - pub message: String, -} - -#[derive(Clone)] -struct ActiveModelDownload { - status: ModelDownloadStatus, - notify: Arc, -} - -fn model_download_key(path: &Path) -> String { - path.to_string_lossy().into_owned() -} - -fn model_download_status( - state: ModelDownloadState, - progress: f64, - message: String, -) -> ModelDownloadStatus { - ModelDownloadStatus { - state, - progress: progress.clamp(0.0, 100.0), - message, - } -} - -async fn begin_model_download(key: &str, message: String) -> bool { - let mut downloads = MODEL_DOWNLOADS.lock().await; - if downloads - .get(key) - .is_some_and(|entry| entry.status.state == ModelDownloadState::Downloading) - { - return false; - } - - downloads.insert( - key.to_string(), - ActiveModelDownload { - status: model_download_status(ModelDownloadState::Downloading, 0.0, message), - notify: Arc::new(Notify::new()), - }, - ); - true -} - -async fn wait_for_model_download(key: &str) -> Result<(), String> { - loop { - let entry = { - let downloads = MODEL_DOWNLOADS.lock().await; - downloads.get(key).cloned() - }; - - let Some(entry) = entry else { - return Ok(()); - }; - - match entry.status.state { - ModelDownloadState::Downloading => { - tokio::select! { - _ = entry.notify.notified() => {} - _ = tokio::time::sleep(Duration::from_millis(250)) => {} - } - } - ModelDownloadState::Completed => return Ok(()), - ModelDownloadState::Failed => return Err(entry.status.message), - } - } -} - -async fn set_model_download_status(app: &AppHandle, key: &str, status: ModelDownloadStatus) { - let notify = { - let mut downloads = MODEL_DOWNLOADS.lock().await; - downloads.get_mut(key).map(|entry| { - entry.status = status.clone(); - entry.notify.clone() - }) - }; - - let _ = DownloadProgress { - progress: status.progress, - message: status.message.clone(), - } - .emit(app); - - if status.state != ModelDownloadState::Downloading - && let Some(notify) = notify - { - notify.notify_waiters(); - } -} - -async fn set_model_download_progress(app: &AppHandle, key: &str, progress: f64, message: String) { - set_model_download_status( - app, - key, - model_download_status(ModelDownloadState::Downloading, progress, message), - ) - .await; -} - -async fn clear_model_download_status(path: &Path) { - let key = model_download_key(path); - let mut downloads = MODEL_DOWNLOADS.lock().await; - let _ = downloads.remove(&key); -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn get_model_download_status( - app: AppHandle, - target_path: String, -) -> Result, String> { - let validated_path = validate_model_path(&app, &target_path)?; - let key = model_download_key(&validated_path); - let downloads = MODEL_DOWNLOADS.lock().await; - Ok(downloads.get(&key).map(|entry| entry.status.clone())) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn download_whisper_model( - app: AppHandle, - model_name: String, - output_path: String, -) -> Result<(), String> { - let validated_path = validate_model_path(&app, &output_path)?; - let key = model_download_key(&validated_path); - - if !begin_model_download(&key, "Preparing model download".to_string()).await { - return wait_for_model_download(&key).await; - } - - let download_app = app.clone(); - let download_key = key.clone(); - tauri::async_runtime::spawn(async move { - let result = download_whisper_model_to_path( - &download_app, - &model_name, - &validated_path, - &download_key, - ) - .await; - let final_status = match &result { - Ok(_) => model_download_status( - ModelDownloadState::Completed, - 100.0, - "Download complete".to_string(), - ), - Err(error) => model_download_status( - ModelDownloadState::Failed, - 0.0, - format!("Download failed: {error}"), - ), - }; - set_model_download_status(&download_app, &download_key, final_status).await; - }); - - wait_for_model_download(&key).await -} - -async fn download_whisper_model_to_path( - app: &AppHandle, - model_name: &str, - validated_path: &Path, - download_key: &str, -) -> Result<(), String> { - let model_parts: &[&str] = match model_name { - "tiny" => &[ - "https://github.com/CapSoftware/transcription-models/releases/download/whisper-v1/ggml-tiny.bin", - ], - "base" => &[ - "https://github.com/CapSoftware/transcription-models/releases/download/whisper-v1/ggml-base.bin", - ], - "small" => &[ - "https://github.com/CapSoftware/transcription-models/releases/download/whisper-v1/ggml-small.bin", - ], - "medium" => &[ - "https://github.com/CapSoftware/transcription-models/releases/download/whisper-v1/ggml-medium.bin.part0", - "https://github.com/CapSoftware/transcription-models/releases/download/whisper-v1/ggml-medium.bin.part1", - ], - _ => &[ - "https://github.com/CapSoftware/transcription-models/releases/download/whisper-v1/ggml-tiny.bin", - ], - }; - - if let Some(parent) = validated_path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create parent directories: {e}"))?; - } - - let http_client = app.state::(); - let total_size = total_content_length(&http_client, model_parts).await; - - let mut file = tokio::fs::File::create(&validated_path) - .await - .map_err(|e| format!("Failed to create file: {e}"))?; - - let mut downloaded: u64 = 0; - let part_count = model_parts.len() as f64; - - for (idx, url) in model_parts.iter().enumerate() { - let response = http_client - .get(*url) - .timeout(MODEL_DOWNLOAD_REQUEST_TIMEOUT) - .send() - .await - .map_err(|e| format!("Failed to download model: {e}"))?; - - if !response.status().is_success() { - return Err(format!( - "Failed to download model: HTTP {}", - response.status() - )); - } - - let part_size = response.content_length().unwrap_or(0); - let mut downloaded_part: u64 = 0; - let mut stream = response.bytes_stream(); - while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result.map_err(|e| format!("Error while downloading: {e}"))?; - - file.write_all(&chunk) - .await - .map_err(|e| format!("Error while writing to file: {e}"))?; - - downloaded = downloaded.saturating_add(chunk.len() as u64); - downloaded_part = downloaded_part.saturating_add(chunk.len() as u64); - - let progress = if total_size > 0 { - (downloaded as f64 / total_size as f64) * 100.0 - } else if part_size > 0 { - ((idx as f64 + downloaded_part as f64 / part_size as f64) / part_count) * 100.0 - } else { - (idx as f64 / part_count) * 100.0 - }; - - set_model_download_progress( - app, - download_key, - progress, - format!("Downloading model: {progress:.0}%"), - ) - .await; - } - } - - file.flush() - .await - .map_err(|e| format!("Failed to flush file: {e}"))?; - - Ok(()) -} - -async fn total_content_length(client: &reqwest::Client, urls: &[&str]) -> u64 { - let mut total: u64 = 0; - for url in urls { - let Ok(resp) = client - .head(*url) - .timeout(Duration::from_secs(30)) - .send() - .await - else { - return 0; - }; - if !resp.status().is_success() { - return 0; - } - match resp.content_length() { - Some(size) => total = total.saturating_add(size), - None => return 0, - } - } - total -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn check_model_exists(app: AppHandle, model_path: String) -> Result { - let validated_path = validate_model_path(&app, &model_path)?; - Ok(validated_path.exists()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn delete_whisper_model(app: AppHandle, model_path: String) -> Result<(), String> { - let validated_path = validate_model_path(&app, &model_path)?; - - if !validated_path.exists() { - return Err(format!("Model file not found: {model_path}")); - } - - clear_model_download_status(&validated_path).await; - - tokio::fs::remove_file(&validated_path) - .await - .map_err(|e| format!("Failed to delete model file: {e}"))?; - - invalidate_whisper_cache_for_path(&validated_path).await; - - Ok(()) -} - -const MODEL_DOWNLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60); - -const PARAKEET_TDT_INT8_MODEL_FILES: &[(&str, &[&str])] = &[ - ( - "encoder-model.int8.onnx", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/encoder-model.int8.onnx", - ], - ), - ( - "decoder_joint-model.int8.onnx", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/decoder_joint-model.int8.onnx", - ], - ), - ( - "vocab.txt", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/vocab.txt", - ], - ), -]; - -const PARAKEET_TDT_FULL_MODEL_FILES: &[(&str, &[&str])] = &[ - ( - "encoder-model.onnx", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/encoder-model.onnx", - ], - ), - ( - "encoder-model.onnx.data", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/encoder-model.onnx.data.part0", - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/encoder-model.onnx.data.part1", - ], - ), - ( - "decoder_joint-model.onnx", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/decoder_joint-model.onnx", - ], - ), - ( - "vocab.txt", - &[ - "https://github.com/CapSoftware/transcription-models/releases/download/parakeet-tdt-v1/vocab.txt", - ], - ), -]; - -const PARAKEET_MODEL_CLEANUP_FILES: &[&str] = &[ - "encoder-model.onnx", - "encoder-model.onnx.data", - "decoder_joint-model.onnx", - "encoder-model.int8.onnx", - "decoder_joint-model.int8.onnx", - "nemo128.onnx", - "vocab.txt", -]; - -const PARAKEET_KNOWN_PART_SIZES: &[(&str, u64)] = &[ - ("encoder-model.int8.onnx", 652_183_999), - ("decoder_joint-model.int8.onnx", 18_202_004), - ("encoder-model.onnx", 41_770_866), - ("encoder-model.onnx.data.part0", 1_300_000_000), - ("encoder-model.onnx.data.part1", 1_135_420_160), - ("decoder_joint-model.onnx", 72_520_893), - ("vocab.txt", 93_939), -]; - -fn parakeet_known_part_size(url: &str) -> Option { - PARAKEET_KNOWN_PART_SIZES.iter().find_map(|(name, size)| { - if url.ends_with(name) { - Some(*size) - } else { - None - } - }) -} - -fn parakeet_model_files_for_dir( - output_dir: &std::path::Path, -) -> &'static [(&'static str, &'static [&'static str])] { - let dir_name = output_dir.file_name().and_then(|name| name.to_str()); - - match dir_name { - Some("parakeet-best-max") => PARAKEET_TDT_FULL_MODEL_FILES, - _ => PARAKEET_TDT_INT8_MODEL_FILES, - } -} - -fn parakeet_staging_dir(validated_dir: &std::path::Path) -> PathBuf { - validated_dir.with_file_name(format!( - "{}.downloading", - validated_dir - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("model") - )) -} - -async fn parakeet_model_file_sizes( - http_client: &reqwest::Client, - model_files: &'static [(&'static str, &'static [&'static str])], -) -> Result, String> { - let mut sizes = Vec::with_capacity(model_files.len()); - for (filename, urls) in model_files { - let mut file_size = 0_u64; - for url in *urls { - let resp = http_client - .head(*url) - .timeout(Duration::from_secs(30)) - .send() - .await - .map_err(|e| format!("Failed to get size for {filename}: {e}"))?; - - if !resp.status().is_success() { - return Err(format!( - "Failed to get size for {filename}: HTTP {}", - resp.status() - )); - } - - let part_size = resp - .content_length() - .filter(|size| *size > 0) - .or_else(|| parakeet_known_part_size(url)) - .unwrap_or(0); - file_size = file_size.saturating_add(part_size); - } - sizes.push((*filename, file_size)); - } - Ok(sizes) -} - -fn parakeet_model_files_match(dir: &std::path::Path, expected_files: &[(&str, u64)]) -> bool { - expected_files.iter().all(|(filename, expected_size)| { - let Ok(metadata) = std::fs::metadata(dir.join(filename)) else { - return false; - }; - - metadata.is_file() && (*expected_size == 0 || metadata.len() == *expected_size) - }) -} - -fn finalize_parakeet_model_download( - validated_dir: &std::path::Path, - staging_dir: &std::path::Path, - model_files: &'static [(&'static str, &'static [&'static str])], -) -> Result<(), String> { - std::fs::create_dir_all(validated_dir) - .map_err(|e| format!("Failed to create model directory: {e}"))?; - - for filename in PARAKEET_MODEL_CLEANUP_FILES { - let file_path = validated_dir.join(filename); - if file_path.exists() { - let _ = std::fs::remove_file(&file_path); - } - } - - for (filename, _) in model_files { - let src = staging_dir.join(filename); - let dst = validated_dir.join(filename); - std::fs::rename(&src, &dst) - .map_err(|e| format!("Failed to move {filename} to final location: {e}"))?; - } - - let _ = std::fs::remove_dir_all(staging_dir); - - Ok(()) -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn download_parakeet_model(app: AppHandle, output_dir: String) -> Result<(), String> { - let validated_dir = validate_model_path(&app, &output_dir)?; - let key = model_download_key(&validated_dir); - - if !begin_model_download(&key, "Preparing model download".to_string()).await { - return wait_for_model_download(&key).await; - } - - let download_app = app.clone(); - let download_key = key.clone(); - tauri::async_runtime::spawn(async move { - let result = - download_parakeet_model_to_dir(&download_app, &validated_dir, &download_key).await; - let final_status = match &result { - Ok(_) => model_download_status( - ModelDownloadState::Completed, - 100.0, - "Download complete".to_string(), - ), - Err(error) => model_download_status( - ModelDownloadState::Failed, - 0.0, - format!("Download failed: {error}"), - ), - }; - set_model_download_status(&download_app, &download_key, final_status).await; - }); - - wait_for_model_download(&key).await -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -async fn download_parakeet_model_to_dir( - app: &AppHandle, - validated_dir: &Path, - download_key: &str, -) -> Result<(), String> { - std::fs::create_dir_all(validated_dir) - .map_err(|e| format!("Failed to create model directory: {e}"))?; - - let http_client = app.state::(); - let model_files = parakeet_model_files_for_dir(validated_dir); - let expected_file_sizes = parakeet_model_file_sizes(&http_client, model_files).await?; - - let staging_dir = parakeet_staging_dir(validated_dir); - if parakeet_model_files_match(&staging_dir, &expected_file_sizes) { - tracing::info!("Finalizing previously completed Parakeet model download"); - finalize_parakeet_model_download(validated_dir, &staging_dir, model_files)?; - invalidate_parakeet_cache_for_dir(validated_dir).await; - return Ok(()); - } - - if staging_dir.exists() { - std::fs::remove_dir_all(&staging_dir) - .map_err(|e| format!("Failed to clean staging directory: {e}"))?; - } - std::fs::create_dir_all(&staging_dir) - .map_err(|e| format!("Failed to create staging directory: {e}"))?; - - let total_size = expected_file_sizes - .iter() - .fold(0_u64, |acc, (_, size)| acc.saturating_add(*size)); - - let mut downloaded_total: u64 = 0; - - let download_result: Result<(), String> = async { - for (idx, (filename, urls)) in model_files.iter().enumerate() { - let file_path = staging_dir.join(filename); - let mut file = tokio::fs::File::create(&file_path) - .await - .map_err(|e| format!("Failed to create {filename}: {e}"))?; - - for url in *urls { - tracing::info!("Downloading {filename} part from {url}"); - - let response = http_client - .get(*url) - .timeout(MODEL_DOWNLOAD_REQUEST_TIMEOUT) - .send() - .await - .map_err(|e| format!("Failed to download {filename}: {e}"))?; - - if !response.status().is_success() { - return Err(format!( - "Failed to download {filename}: HTTP {}", - response.status() - )); - } - - let mut stream = response.bytes_stream(); - while let Some(chunk_result) = stream.next().await { - let chunk = - chunk_result.map_err(|e| format!("Download error for {filename}: {e}"))?; - file.write_all(&chunk) - .await - .map_err(|e| format!("Write error for {filename}: {e}"))?; - - downloaded_total = downloaded_total.saturating_add(chunk.len() as u64); - - let progress = if total_size > 0 { - (downloaded_total as f64 / total_size as f64) * 100.0 - } else { - ((idx as f64 + 0.5) / model_files.len() as f64) * 100.0 - }; - - set_model_download_progress( - app, - download_key, - progress, - format!("Downloading {filename}: {progress:.0}%"), - ) - .await; - } - } - - file.flush() - .await - .map_err(|e| format!("Failed to flush {filename}: {e}"))?; - - tracing::info!("Finished downloading {filename}"); - } - Ok(()) - } - .await; - - if let Err(e) = &download_result { - tracing::warn!("Download failed, cleaning up staging directory: {e}"); - let _ = std::fs::remove_dir_all(&staging_dir); - return Err(e.clone()); - } - - if !parakeet_model_files_match(&staging_dir, &expected_file_sizes) { - let _ = std::fs::remove_dir_all(&staging_dir); - return Err("Downloaded model files did not match expected sizes".to_string()); - } - - finalize_parakeet_model_download(validated_dir, &staging_dir, model_files)?; - - invalidate_parakeet_cache_for_dir(validated_dir).await; - - Ok(()) -} - -#[cfg(all(target_os = "macos", target_arch = "x86_64"))] -#[tauri::command] -#[specta::specta] -#[instrument(skip(_app))] -pub async fn download_parakeet_model(_app: AppHandle, _output_dir: String) -> Result<(), String> { - Err(PARAKEET_UNSUPPORTED_MESSAGE.to_string()) -} - -#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn check_parakeet_model_exists( - app: AppHandle, - model_dir: String, -) -> Result { - let validated_dir = validate_model_path(&app, &model_dir)?; - - if !validated_dir.is_dir() { - return Ok(false); - } - - let has_vocab = validated_dir.join("vocab.txt").exists(); - let has_full_model = validated_dir.join("encoder-model.onnx").exists() - && validated_dir.join("encoder-model.onnx.data").exists() - && validated_dir.join("decoder_joint-model.onnx").exists(); - let has_int8_model = validated_dir.join("encoder-model.int8.onnx").exists() - && validated_dir.join("decoder_joint-model.int8.onnx").exists(); - - Ok(has_vocab && (has_full_model || has_int8_model)) -} - -#[cfg(all(target_os = "macos", target_arch = "x86_64"))] -#[tauri::command] -#[specta::specta] -#[instrument(skip(_app))] -pub async fn check_parakeet_model_exists( - _app: AppHandle, - _model_dir: String, -) -> Result { - Ok(false) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn delete_parakeet_model(app: AppHandle, model_dir: String) -> Result<(), String> { - let validated_dir = validate_model_path(&app, &model_dir)?; - - if !validated_dir.exists() { - return Err(format!("Model directory not found: {model_dir}")); - } - - invalidate_parakeet_cache_for_dir(&validated_dir).await; - clear_model_download_status(&validated_dir).await; - - tokio::fs::remove_dir_all(&validated_dir) - .await - .map_err(|e| format!("Failed to delete model directory: {e}"))?; - - Ok(()) -} - -fn captions_to_srt(captions: &CaptionData) -> String { - let mut srt = String::new(); - for (i, segment) in captions.segments.iter().enumerate() { - let start_time = format_srt_time(f64::from(segment.start)); - let end_time = format_srt_time(f64::from(segment.end)); - - srt.push_str(&format!( - "{}\n{} --> {}\n{}\n\n", - i + 1, - start_time, - end_time, - segment.text.trim() - )); - } - srt -} - -fn format_srt_time(seconds: f64) -> String { - let hours = (seconds / 3600.0) as i32; - let minutes = ((seconds % 3600.0) / 60.0) as i32; - let secs = (seconds % 60.0) as i32; - let millis = ((seconds % 1.0) * 1000.0) as i32; - format!("{hours:02}:{minutes:02}:{secs:02},{millis:03}") -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -pub async fn export_captions_srt( - app: AppHandle, - video_id: String, -) -> Result, String> { - tracing::info!("Starting SRT export for video_id: {}", video_id); - - let captions = match load_captions(app.clone(), video_id.clone()).await? { - Some(c) => { - tracing::info!("Found {} caption segments to export", c.segments.len()); - c - } - None => { - tracing::info!("No captions found for video_id: {}", video_id); - return Ok(None); - } - }; - - let captions_with_settings = CaptionData { - segments: captions.segments, - settings: captions - .settings - .or_else(|| Some(CaptionSettings::default())), - }; - - tracing::info!("Converting captions to SRT format"); - let srt_content = captions_to_srt(&captions_with_settings); - - let captions_dir = app_captions_dir(&app, &video_id)?; - let srt_path = captions_dir.join("captions.srt"); - tracing::info!("Will write SRT file to: {:?}", srt_path); - - match std::fs::write(&srt_path, srt_content) { - Ok(_) => { - tracing::info!("Successfully wrote SRT file to: {:?}", srt_path); - Ok(Some(srt_path)) - } - Err(e) => { - tracing::error!("Failed to write SRT file: {}", e); - Err(format!("Failed to write SRT file: {e}")) - } - } -} - -#[cfg(test)] -mod tests { - use super::{ - AudioExtractionSource, CaptionWord, caption_text_from_words, caption_word_chunks, - normalize_caption_words, resolve_audio_extraction_source, resolve_path_with_base, - }; - use tempfile::tempdir; - - fn word(text: &str, index: usize) -> CaptionWord { - CaptionWord { - text: text.to_string(), - start: index as f32, - end: index as f32 + 0.5, - } - } - - #[test] - fn resolve_path_with_base_rejects_parent_dir_escape() { - let dir = tempdir().unwrap(); - let base = dir.path().join("app-data"); - std::fs::create_dir_all(base.join("models")).unwrap(); - - let escaped = base.join("..").join("outside.bin"); - - let result = resolve_path_with_base(&base, escaped.to_string_lossy().as_ref()); - - assert!(result.is_err()); - } - - #[test] - fn resolve_path_with_base_allows_nested_model_path() { - let dir = tempdir().unwrap(); - let base = dir.path().join("app-data"); - std::fs::create_dir_all(base.join("models")).unwrap(); - - let target = base.join("models").join("nested").join("model.bin"); - let expected = base - .canonicalize() - .unwrap() - .join("models") - .join("nested") - .join("model.bin"); - - let resolved = resolve_path_with_base(&base, target.to_string_lossy().as_ref()).unwrap(); - - assert_eq!(resolved, expected); - } - - #[test] - fn audio_extraction_source_accepts_project_directory_without_cap_extension() { - let dir = tempdir().unwrap(); - let project_dir = dir.path().join("recording"); - std::fs::create_dir_all(&project_dir).unwrap(); - std::fs::write(project_dir.join("recording-meta.json"), "{}").unwrap(); - - match resolve_audio_extraction_source(project_dir.to_string_lossy().as_ref()).unwrap() { - AudioExtractionSource::ProjectDirectory { base_path } => { - assert_eq!(base_path, project_dir); - } - AudioExtractionSource::MediaFile(_) => panic!("expected project directory"), - } - } - - #[test] - fn audio_extraction_source_rejects_directory_without_recording_metadata() { - let dir = tempdir().unwrap(); - let result = resolve_audio_extraction_source(dir.path().to_string_lossy().as_ref()); - - match result { - Ok(_) => panic!("expected missing metadata error"), - Err(error) => assert_eq!(error, "Recording directory is missing recording-meta.json"), - } - } - - #[test] - fn audio_extraction_source_accepts_media_file() { - let dir = tempdir().unwrap(); - let media_file = dir.path().join("recording.mp4"); - std::fs::write(&media_file, []).unwrap(); - - match resolve_audio_extraction_source(media_file.to_string_lossy().as_ref()).unwrap() { - AudioExtractionSource::MediaFile(path) => assert_eq!(path, media_file), - AudioExtractionSource::ProjectDirectory { .. } => panic!("expected media file"), - } - } - - #[test] - fn normalize_caption_words_attaches_punctuation() { - let words = normalize_caption_words(vec![ - word("test", 0), - word(",", 1), - word("test", 2), - word(".", 3), - ]); - - assert_eq!(caption_text_from_words(&words), "test, test."); - assert_eq!(words.len(), 2); - } - - #[test] - fn normalize_caption_words_clamps_inflated_trailing_word() { - let words = normalize_caption_words(vec![CaptionWord { - text: "seconds.".to_string(), - start: 53.92, - end: 70.16, - }]); - - assert_eq!(words.len(), 1); - assert!((words[0].end - (53.92 + super::MAX_CAPTION_WORD_DURATION)).abs() < 1e-4); - } - - #[test] - fn normalize_caption_words_keeps_normal_word_durations() { - let words = normalize_caption_words(vec![CaptionWord { - text: "hello".to_string(), - start: 1.0, - end: 1.4, - }]); - - assert_eq!(words.len(), 1); - assert!((words[0].end - 1.4).abs() < 1e-4); - } - - #[test] - fn caption_word_chunks_do_not_end_on_short_connector_when_more_words_follow() { - let words = [ - "This", "is", "where", "we", "record", "I", "want", "clean", "captions", - ] - .iter() - .enumerate() - .map(|(index, text)| word(text, index)) - .collect::>(); - - let chunks = caption_word_chunks(&words); - - assert_eq!( - caption_text_from_words(chunks[0]), - "This is where we record I want" - ); - assert_eq!(caption_text_from_words(chunks[1]), "clean captions"); - } - - #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] - mod parakeet { - use super::super::parakeet_model_dir_matches; - use tempfile::tempdir; - - #[test] - fn parakeet_model_dir_match_uses_full_directory_path() { - let dir = tempdir().unwrap(); - let model_dir = dir.path().join("models").join("parakeet-best"); - - assert!(parakeet_model_dir_matches( - model_dir.to_string_lossy().as_ref(), - &model_dir - )); - assert!(!parakeet_model_dir_matches( - dir.path() - .join("models") - .join("parakeet-best-max") - .to_string_lossy() - .as_ref(), - &model_dir - )); - } - } -} diff --git a/apps/desktop/src-tauri/src/clean_capture.rs b/apps/desktop/src-tauri/src/clean_capture.rs deleted file mode 100644 index d2fc47e234f..00000000000 --- a/apps/desktop/src-tauri/src/clean_capture.rs +++ /dev/null @@ -1,4536 +0,0 @@ -use std::{ - path::PathBuf, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; - -use serde::Serialize; -use specta::Type; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_specta::Event; -use tokio::sync::Notify; - -use crate::{CurrentRecordingChanged, windows::CapWindowId}; - -pub const STOP_SHORTCUT: &str = "Ctrl+Shift+F9"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub enum Phase { - AwaitingShortcut, - Starting, - Recording, - Pausing, - Paused, - Resuming, - ResumeFailed, - Restarting, - Stopping, - Restoring, -} - -impl Phase { - fn can_stop(self) -> bool { - matches!( - self, - Self::Recording | Self::Paused | Self::Resuming | Self::ResumeFailed - ) - } -} - -#[derive(Clone, Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct Snapshot { - pub generation: u32, - pub phase: Option, - pub mode: Option, - pub shortcut: Option, - pub error: Option, -} - -#[derive(Clone)] -struct SavedWindow { - label: String, - native_id: u64, - visible: bool, -} - -impl SavedWindow { - fn visibility_for(&self, native_id: u64) -> Option { - (self.native_id == native_id).then_some(self.visible) - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum StopRoute { - #[cfg(any(target_os = "linux", test))] - Tray, - #[cfg(any(target_os = "linux", test))] - Portal, -} - -struct Lease { - mode: cap_recording::RecordingMode, - generation: u32, - phase: Phase, - pressed: bool, - stop_requested: bool, - registered_shortcut: bool, - wayland: bool, - stop_route: Option, - stop_description: Option, - stop_error: Option, - lost_stop_routes: [bool; 2], - recording_dir: Option, - windows: Vec, -} - -impl StopRoute { - fn index(self) -> usize { - match self { - #[cfg(any(target_os = "linux", test))] - Self::Tray => 0, - #[cfg(any(target_os = "linux", test))] - Self::Portal => 1, - } - } -} - -impl Lease { - fn accept_stop_input(&mut self, route: Option<(u32, StopRoute)>, pressed: bool) -> bool { - match route { - Some((generation, route)) - if self.wayland - && self.generation == generation - && !self.lost_stop_routes[route.index()] => - { - if self.phase == Phase::AwaitingShortcut { - if pressed { - self.stop_route = Some(route); - } else if self.stop_route != Some(route) { - return false; - } - } - true - } - None if !self.wayland => true, - _ => false, - } - } - - #[cfg(any(target_os = "linux", test))] - fn lose_stop_route(&mut self, route: StopRoute) -> bool { - self.lost_stop_routes[route.index()] = true; - let lost = self.stop_route == Some(route) || self.lost_stop_routes.iter().all(|lost| *lost); - if lost { - self.stop_requested = true; - } - lost && self.phase.can_stop() - } -} - -struct ControlError { - generation: u32, - dir: PathBuf, - message: String, -} - -#[derive(Clone)] -pub(crate) struct StopNoticeOwner { - sequence: u64, - directory: PathBuf, - generation: Option, - confirmed: Arc, -} - -impl StopNoticeOwner { - pub(crate) fn is_confirmed(&self) -> bool { - self.confirmed.load(Ordering::Acquire) - } - - pub(crate) fn same_attempt(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.confirmed, &other.confirmed) - } -} - -#[derive(Clone)] -pub(crate) struct StopNoticeTicket { - sequence: u64, - pub(crate) owner: StopNoticeOwner, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum StopNoticeKind { - Unconfirmed, - ConfirmedFailure, - ControlFailure, -} - -struct StopNotice { - ticket: StopNoticeTicket, - kind: StopNoticeKind, - message: String, - previous_message: Option, - restoration: Option>, -} - -impl StopNotice { - fn error(&self) -> String { - let confirmed = - self.ticket.owner.is_confirmed() || self.kind == StopNoticeKind::ConfirmedFailure; - let status = if confirmed { - match (&self.restoration, self.ticket.owner.generation) { - (Some(Err(_)), _) => "Recording stopped; window restoration failed", - (None, Some(_)) => "Recording stopped; window restoration is pending", - _ => "Recording stopped", - } - } else { - "Stop could not be confirmed" - }; - let mut message = format!( - "{status} ({}): {}", - self.ticket.owner.directory.display(), - self.message - ); - if let Some(previous) = &self.previous_message { - message.push('\n'); - message.push_str(previous); - } - if let Some(Err(error)) = &self.restoration - && !self.message.contains(error) - { - message.push('\n'); - message.push_str(error); - } - message - } -} - -struct RestorationReceipt { - generation: u32, - result: Result<(), String>, - #[cfg(target_os = "linux")] - stop_requested: bool, -} - -impl RestorationReceipt { - fn restoration_result(&self) -> Result<(), String> { - self.result.clone() - } - - #[cfg(target_os = "linux")] - fn restart_result(&self) -> Result<(), String> { - self.restoration_result()?; - if self.stop_requested { - Err("Recording restart was cancelled by Stop".into()) - } else { - Ok(()) - } - } -} - -#[derive(Default)] -struct Inner { - generation: u32, - lease: Option, - control_error: Option, - restored: Option, - stop_notice_sequence: u64, - stop_notice: Option, - #[cfg(target_os = "linux")] - x11_cleanup_sequence: u64, - #[cfg(target_os = "linux")] - x11_cleanup: Option, -} - -#[cfg(target_os = "linux")] -#[derive(Clone, Copy, PartialEq, Eq)] -struct X11StudioCleanup { - generation: u32, - sequence: u64, - registered_shortcut: bool, -} - -#[derive(Default)] -pub struct State { - #[cfg(target_os = "linux")] - pub(crate) instant: Mutex>, - #[cfg(target_os = "linux")] - pub(crate) portal_stop: tokio::sync::Mutex>, - inner: Mutex, - changed: Notify, -} - -impl Inner { - #[cfg(target_os = "linux")] - fn reserve_x11_studio_cleanup( - &mut self, - generation: u32, - windows: Result<(), String>, - ) -> Option { - if self.x11_cleanup.is_some() - || self - .restored - .as_ref() - .is_some_and(|receipt| receipt.generation == generation) - { - return None; - } - let lease = self.lease.as_ref().filter(|lease| { - self.generation == generation - && lease.generation == generation - && lease.phase == Phase::Restoring - && lease.mode == cap_recording::RecordingMode::Studio - && !lease.wayland - })?; - let registered_shortcut = lease.registered_shortcut; - if let Err(error) = windows { - let _ = self.complete_instant_restoration(generation, Err(error)); - return None; - } - self.x11_cleanup_sequence = self.x11_cleanup_sequence.wrapping_add(1); - let cleanup = X11StudioCleanup { - generation, - sequence: self.x11_cleanup_sequence, - registered_shortcut, - }; - self.x11_cleanup = Some(cleanup); - Some(cleanup) - } - - #[cfg(target_os = "linux")] - fn complete_x11_studio_restoration( - &mut self, - cleanup: X11StudioCleanup, - result: Result<(), String>, - ) -> bool { - if self.x11_cleanup != Some(cleanup) - || self.generation != cleanup.generation - || self.lease.as_ref().is_none_or(|lease| { - lease.generation != cleanup.generation - || lease.phase != Phase::Restoring - || lease.mode != cap_recording::RecordingMode::Studio - || lease.wayland - || lease.registered_shortcut != cleanup.registered_shortcut - }) - { - return false; - } - self.x11_cleanup = None; - self.complete_instant_restoration(cleanup.generation, result) - .is_some() - } - - #[cfg(target_os = "linux")] - fn begin_x11_studio_restore_retry(&mut self, pressed: bool) -> Option { - if !pressed || self.x11_cleanup.is_some() { - return None; - } - let generation = self.generation; - let lease = self.lease.as_mut().filter(|lease| { - lease.generation == generation - && lease.phase == Phase::Restoring - && lease.mode == cap_recording::RecordingMode::Studio - && !lease.wayland - })?; - if !self.restored.as_ref().is_some_and(|receipt| { - receipt.generation == lease.generation && receipt.result.is_err() - }) { - return None; - } - lease.stop_requested = true; - self.restored = None; - Some(lease.generation) - } - - #[cfg(target_os = "linux")] - fn complete_instant_restoration( - &mut self, - generation: u32, - result: Result<(), String>, - ) -> Option { - if self - .lease - .as_ref() - .is_none_or(|lease| lease.generation != generation || lease.phase != Phase::Restoring) - { - return None; - } - let succeeded = result.is_ok(); - let _ = self.record_stop_restoration(generation, result); - if succeeded { - let owned = self.lease.take().unwrap().registered_shortcut; - self.generation = self.generation.wrapping_add(1); - Some(owned) - } else { - None - } - } - - fn record_stop_restoration(&mut self, generation: u32, result: Result<(), String>) -> bool { - let Some(lease) = self - .lease - .as_ref() - .filter(|lease| lease.generation == generation && lease.phase == Phase::Restoring) - else { - return false; - }; - if let Some(notice) = &mut self.stop_notice - && notice.ticket.owner.generation == Some(generation) - && lease.recording_dir.as_ref() == Some(¬ice.ticket.owner.directory) - { - notice.restoration = Some(result.clone()); - } - self.restored = Some(RestorationReceipt { - generation, - result, - #[cfg(target_os = "linux")] - stop_requested: lease.stop_requested, - }); - true - } - - fn next_stop_notice_sequence(&mut self) -> u64 { - self.stop_notice_sequence = self - .stop_notice_sequence - .checked_add(1) - .expect("Stop notice identity exhausted"); - self.stop_notice_sequence - } - - fn reserve_stop_notice_owner( - &mut self, - directory: PathBuf, - generation: Option, - ) -> StopNoticeOwner { - StopNoticeOwner { - sequence: self.next_stop_notice_sequence(), - directory, - generation, - confirmed: Arc::new(AtomicBool::new(false)), - } - } - - fn reserve_stop_notice_ticket(&mut self, owner: StopNoticeOwner) -> StopNoticeTicket { - StopNoticeTicket { - sequence: self.next_stop_notice_sequence(), - owner, - } - } - - fn retain_stop_notice( - &mut self, - ticket: &StopNoticeTicket, - kind: StopNoticeKind, - message: String, - ) -> bool { - if let Some(previous) = &mut self.stop_notice { - if previous.ticket.sequence == ticket.sequence - && previous.ticket.owner.same_attempt(&ticket.owner) - { - if previous.message == message - || previous.previous_message.as_ref() == Some(&message) - { - return false; - } - if kind == StopNoticeKind::ConfirmedFailure { - previous.previous_message = - Some(std::mem::replace(&mut previous.message, message)); - previous.kind = kind; - } else { - previous.previous_message = Some(message); - } - tracing::info!(project = %ticket.owner.directory.display(), "Retained distinct failure from the same Stop cohort"); - return true; - } - if (previous.ticket.owner.sequence, previous.ticket.sequence) - >= (ticket.owner.sequence, ticket.sequence) - { - return false; - } - tracing::info!( - previous_project = %previous.ticket.owner.directory.display(), - next_project = %ticket.owner.directory.display(), - "Retained Stop notice superseded by a newer failure" - ); - } - let restoration = self - .restored - .as_ref() - .filter(|receipt| Some(receipt.generation) == ticket.owner.generation) - .map(RestorationReceipt::restoration_result); - self.stop_notice = Some(StopNotice { - ticket: ticket.clone(), - kind, - message, - previous_message: None, - restoration, - }); - true - } - - fn owner(&self, dir: &std::path::Path) -> Option { - self.lease.as_ref().and_then(|lease| { - (lease.recording_dir.as_deref() == Some(dir)).then_some(lease.generation) - }) - } - fn queue_stop(&mut self) -> bool { - let Some(lease) = self.lease.as_mut() else { - return false; - }; - lease.stop_requested = true; - let deferred = !lease.phase.can_stop(); - if !deferred { - lease.phase = Phase::Stopping; - } - deferred - } - - fn control_current(&self, generation: u32, dir: &std::path::Path, resume: bool) -> bool { - let transition = if resume { - Phase::Resuming - } else { - Phase::Pausing - }; - self.generation == generation - && self.lease.as_ref().is_some_and(|lease| { - lease.generation == generation - && lease.phase == transition - && lease.recording_dir.as_deref() == Some(dir) - }) - } - - fn complete_control( - &mut self, - generation: u32, - dir: &std::path::Path, - resume: bool, - outcome: &ControlOutcome, - ) -> bool { - if !self.control_current(generation, dir, resume) { - return false; - } - self.control_error = match outcome { - ControlOutcome::Succeeded | ControlOutcome::StoppedBeforeResume => None, - ControlOutcome::HideFailed(message) - | ControlOutcome::ActorFailed { error: message, .. } => Some(ControlError { - generation, - dir: dir.to_path_buf(), - message: message.clone(), - }), - }; - self.lease.as_mut().unwrap().phase = match (resume, outcome) { - (true, ControlOutcome::Succeeded) | (false, ControlOutcome::ActorFailed { .. }) => { - Phase::Recording - } - (true, ControlOutcome::ActorFailed { paused: false, .. }) => Phase::ResumeFailed, - _ => Phase::Paused, - }; - true - } - - fn may_restore_paused_main(&self, generation: u32, dir: &std::path::Path) -> bool { - self.owner(dir) == Some(generation) - && self - .lease - .as_ref() - .is_some_and(|lease| lease.phase == Phase::Paused && !lease.stop_requested) - && self.may_reveal(generation, "main") - } - - fn snapshot(&self) -> Snapshot { - let mut errors = Vec::with_capacity(4); - if let Some(error) = self.control_error.as_ref().filter(|error| { - error.generation == self.generation - && self - .lease - .as_ref() - .is_none_or(|lease| lease.recording_dir.as_ref() == Some(&error.dir)) - }) { - errors.push(error.message.clone()); - } - for error in [ - self.lease - .as_ref() - .and_then(|lease| lease.stop_error.clone()), - self.restored.as_ref().and_then(|receipt| { - self.lease - .as_ref() - .filter(|lease| lease.generation == receipt.generation) - .and_then(|_| receipt.result.as_ref().err().cloned()) - }), - self.stop_notice.as_ref().map(StopNotice::error), - ] - .into_iter() - .flatten() - { - if !errors.contains(&error) { - errors.push(error); - } - } - Snapshot { - generation: self.generation, - phase: self.lease.as_ref().map(|lease| lease.phase), - mode: self.lease.as_ref().map(|lease| lease.mode), - shortcut: self.lease.as_ref().map(|lease| { - lease.stop_description.clone().unwrap_or_else(|| { - if lease.wayland { - "the Cap Stop tray icon".to_string() - } else { - STOP_SHORTCUT.to_string() - } - }) - }), - error: (!errors.is_empty()).then(|| errors.join("\n")), - } - } - - fn may_reveal(&self, generation: u32, label: &str) -> bool { - if generation != self.generation { - return false; - } - let Some(lease) = &self.lease else { - return true; - }; - if lease.wayland && wayland_blocks_mapping(lease.phase) { - return false; - } - match label.parse::() { - Ok(CapWindowId::Main) => { - matches!( - lease.phase, - Phase::AwaitingShortcut | Phase::Paused | Phase::Restoring - ) && (!lease.stop_requested || lease.phase == Phase::Restoring) - } - Ok( - CapWindowId::Camera - | CapWindowId::RecordingControls - | CapWindowId::TargetSelectOverlay { .. }, - ) => false, - _ => true, - } - } - - fn shortcut(&mut self, pressed: bool) -> bool { - let Some(lease) = self.lease.as_mut() else { - return false; - }; - if lease.phase == Phase::AwaitingShortcut { - if pressed { - lease.pressed = true; - } else if lease.pressed { - lease.phase = Phase::Starting; - lease.pressed = false; - } - } else if pressed { - lease.stop_requested = true; - } - true - } -} - -pub fn generation(app: &AppHandle) -> u32 { - app.try_state::() - .map(|state| state.inner.lock().unwrap().generation) - .unwrap_or(0) -} - -pub fn phase(app: &AppHandle) -> Option { - app.try_state::() - .and_then(|state| state.inner.lock().unwrap().lease.as_ref().map(|l| l.phase)) -} - -pub fn blocks_idle_cleanup(app: &AppHandle) -> bool { - #[cfg(target_os = "linux")] - if crate::recording::linux_instant::current(app).is_some() { - return true; - } - phase(app).is_some_and(|phase| phase != Phase::Restoring) -} - -pub fn cancel_closed_preflight(app: &AppHandle) { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let Some(lease) = inner - .lease - .as_mut() - .filter(|lease| lease.phase == Phase::AwaitingShortcut) - else { - return; - }; - lease.stop_requested = true; - for saved in &mut lease.windows { - if saved.label == CapWindowId::Main.label() { - saved.visible = false; - } - } - drop(inner); - notify(app); -} - -pub fn is_current(app: &AppHandle, generation: u32) -> bool { - app.state::() - .inner - .lock() - .unwrap() - .lease - .as_ref() - .is_some_and(|lease| lease.generation == generation) -} - -pub fn stop_requested(app: &AppHandle, generation: u32) -> bool { - app.state::() - .inner - .lock() - .unwrap() - .lease - .as_ref() - .filter(|lease| lease.generation == generation) - .is_none_or(|lease| lease.stop_requested) -} - -fn notify(app: &AppHandle) { - app.state::().changed.notify_waiters(); - let _ = CurrentRecordingChanged.emit(app); -} - -pub(crate) fn reserve_stop_notice_owner( - app: &AppHandle, - directory: PathBuf, - generation: Option, -) -> StopNoticeOwner { - app.state::() - .inner - .lock() - .unwrap() - .reserve_stop_notice_owner(directory, generation) -} - -pub(crate) fn reserve_stop_notice_ticket( - app: &AppHandle, - owner: StopNoticeOwner, -) -> StopNoticeTicket { - app.state::() - .inner - .lock() - .unwrap() - .reserve_stop_notice_ticket(owner) -} - -pub(crate) fn retain_stop_notice( - app: &AppHandle, - ticket: &StopNoticeTicket, - kind: StopNoticeKind, - message: String, -) { - let retained = app - .state::() - .inner - .lock() - .unwrap() - .retain_stop_notice(ticket, kind, message); - if retained { - notify(app); - } -} - -pub(crate) fn confirm_stop_notice(app: &AppHandle, owner: &StopNoticeOwner) { - owner.confirmed.store(true, Ordering::Release); - notify(app); -} - -pub fn set_phase(app: &AppHandle, generation: u32, phase: Phase) -> bool { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let Some(lease) = inner.lease.as_mut().filter(|l| l.generation == generation) else { - return false; - }; - lease.phase = phase; - drop(inner); - notify(app); - true -} - -pub fn publish(app: &AppHandle, generation: u32, dir: PathBuf) -> bool { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let Some(lease) = inner.lease.as_mut().filter(|l| l.generation == generation) else { - return false; - }; - if lease.phase != Phase::Starting { - return false; - } - lease.recording_dir = Some(dir); - lease.phase = Phase::Recording; - true -} - -pub fn set_start_directory(app: &AppHandle, generation: u32, dir: PathBuf) -> Result<(), String> { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let lease = inner - .lease - .as_mut() - .filter(|lease| lease.generation == generation && lease.phase == Phase::Starting) - .ok_or("Recording startup was superseded")?; - lease.recording_dir = Some(dir); - Ok(()) -} - -pub fn owner(app: &AppHandle, dir: &std::path::Path) -> Option { - let state = app.state::(); - let inner = state.inner.lock().unwrap(); - inner.owner(dir) -} - -pub fn queue_stop(app: &AppHandle) -> bool { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - if inner.lease.is_none() { - return false; - } - let deferred = inner.queue_stop(); - drop(inner); - notify(app); - deferred -} - -pub(crate) fn queue_owned_studio_stop( - app: &AppHandle, - generation: u32, - directory: &std::path::Path, -) -> bool { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - if inner.lease.as_ref().is_none_or(|lease| { - lease.generation != generation - || lease.mode != cap_recording::RecordingMode::Studio - || lease.recording_dir.as_deref() != Some(directory) - || !(lease.phase.can_stop() || lease.phase == Phase::Stopping) - }) { - return false; - } - let _ = inner.queue_stop(); - drop(inner); - notify(app); - true -} - -pub fn handle_shortcut(app: &AppHandle, pressed: bool) -> bool { - handle_stop_input(app, pressed, None) -} - -#[cfg(target_os = "linux")] -pub(crate) fn handle_wayland_stop( - app: &AppHandle, - generation: u32, - route: StopRoute, - pressed: bool, -) -> bool { - handle_stop_input(app, pressed, Some((generation, route))) -} - -fn handle_stop_input(app: &AppHandle, pressed: bool, route: Option<(u32, StopRoute)>) -> bool { - let Some(state) = app.try_state::() else { - return false; - }; - let mut inner = state.inner.lock().unwrap(); - if !inner - .lease - .as_mut() - .is_some_and(|lease| lease.accept_stop_input(route, pressed)) - { - return false; - } - #[cfg(target_os = "linux")] - if let Some(generation) = inner.begin_x11_studio_restore_retry(pressed) { - drop(inner); - spawn_x11_studio_restore(app.clone(), generation, true); - return true; - } - #[cfg(target_os = "linux")] - if pressed - && inner.lease.as_ref().is_some_and(|lease| { - (lease.mode == cap_recording::RecordingMode::Instant || lease.wayland) - && lease.phase == Phase::Restoring - }) - && inner - .restored - .as_ref() - .is_some_and(|receipt| receipt.result.is_err()) - { - let generation = inner.lease.as_ref().unwrap().generation; - inner.restored = None; - drop(inner); - spawn_instant_restore(app.clone(), generation, true); - return true; - } - #[cfg(target_os = "linux")] - let instant_stop = pressed - && crate::recording::linux_instant::current(app).is_some() - && inner - .lease - .as_ref() - .is_some_and(|lease| lease.phase != Phase::AwaitingShortcut); - let handled = inner.shortcut(pressed); - let studio_stop = pressed - .then(|| { - inner.lease.as_ref().and_then(|lease| { - (lease.mode == cap_recording::RecordingMode::Studio - && (lease.phase.can_stop() || lease.phase == Phase::Stopping)) - .then(|| { - lease - .recording_dir - .clone() - .map(|dir| (lease.generation, dir)) - }) - .flatten() - }) - }) - .flatten(); - let stop = handled - && pressed - && inner - .lease - .as_ref() - .is_some_and(|lease| lease.phase.can_stop()); - #[cfg(target_os = "linux")] - let stop = stop || (handled && instant_stop); - drop(inner); - if handled { - notify(app); - } - if let Some((generation, directory)) = studio_stop { - crate::recording::queue_clean_studio_stop(app, generation, directory); - } else if stop { - let app = app.clone(); - drop(tauri::async_runtime::spawn(async move { - if let Err(error) = crate::recording::stop_recording(app.clone(), app.state()).await { - tracing::error!(%error, "Clean capture shortcut could not stop recording"); - } - })); - } - handled -} - -pub fn reveal_now(window: &WebviewWindow, expected_generation: u32) -> tauri::Result { - reveal_now_with_options(window, expected_generation, false, false) -} - -fn reveal_now_with_options( - window: &WebviewWindow, - expected_generation: u32, - focus: bool, - unminimize: bool, -) -> tauri::Result { - let app = window.app_handle(); - #[cfg(target_os = "linux")] - if crate::recording::linux_instant::current(app) - .is_some_and(|attempt| attempt.has_capture() && !attempt.ui_ready()) - { - return Ok(false); - } - let Some(state) = app.try_state::() else { - #[cfg(target_os = "macos")] - if window.label() == CapWindowId::Main.label() { - crate::platform::constrain_main_window_to_visible_frame(&window.as_ref().window()) - .map_err(|error| tauri::Error::Io(std::io::Error::other(error)))?; - } - window.show()?; - return Ok(true); - }; - let inner = state.inner.lock().unwrap(); - if !inner.may_reveal(expected_generation, window.label()) { - #[cfg(target_os = "linux")] - let deferred_editor = inner.lease.as_ref().is_some_and(|lease| { - lease.wayland - && lease.generation == expected_generation - && lease.phase == Phase::Stopping - && matches!(window.label().parse(), Ok(CapWindowId::Editor { .. })) - }); - drop(inner); - #[cfg(target_os = "linux")] - if deferred_editor { - use gtk::prelude::*; - let native = window.gtk_window()?; - remember_wayland_window(app, expected_generation, native.upcast_ref(), true); - } - return Ok(false); - } - // Mapping runs on the UI thread; a new capture must acknowledge its queued hide - // before starting. Do not retain this mutex across synchronous GTK callbacks. - drop(inner); - #[cfg(target_os = "macos")] - if window.label() == CapWindowId::Main.label() { - crate::platform::constrain_main_window_to_visible_frame(&window.as_ref().window()) - .map_err(|error| tauri::Error::Io(std::io::Error::other(error)))?; - } - set_native_visibility(window, true) - .map_err(|error| tauri::Error::Io(std::io::Error::other(error)))?; - if unminimize { - window.unminimize()?; - } - if focus { - window.set_focus()?; - } - Ok(true) -} - -pub async fn reveal(window: WebviewWindow, expected_generation: u32) -> Result { - reveal_with_options(window, expected_generation, false, false).await -} - -async fn reveal_with_options( - window: WebviewWindow, - expected_generation: u32, - focus: bool, - unminimize: bool, -) -> Result { - #[cfg(target_os = "linux")] - if window.label() == CapWindowId::Main.label() - && let Some(attempt) = crate::recording::linux_instant::current(window.app_handle()) - && attempt.has_capture() - && !attempt.ui_ready() - { - crate::recording::linux_instant::control(window.app_handle().clone(), attempt, false) - .await?; - } - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = window.clone(); - window - .run_on_main_thread(move || { - let _ = tx.send( - reveal_now_with_options(&handle, expected_generation, focus, unminimize) - .map_err(|e| e.to_string()), - ); - }) - .map_err(|e| e.to_string())?; - tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Timed out revealing recording window".to_string())? - .map_err(|_| "Recording window closed before it could be shown".to_string())? -} - -pub async fn guarded_show( - window: WebviewWindow, - generation: u32, - focus: bool, - unminimize: bool, -) -> tauri::Result<()> { - reveal_with_options(window, generation, focus, unminimize) - .await - .map(|_| ()) - .map_err(|error| tauri::Error::Io(std::io::Error::other(error))) -} - -async fn finish_main_controls( - phase: Option, - instant_stop: Option, - studio_pause: S, -) -> Result<(), String> -where - I: std::future::Future>, - S: std::future::Future>, -{ - if phase != Some(Phase::AwaitingShortcut) - && let Some(stop) = instant_stop - { - return stop.await; - } - match phase { - Some(Phase::Recording) => studio_pause.await, - None | Some(Phase::AwaitingShortcut | Phase::Paused | Phase::Restoring) => Ok(()), - _ => Err("Recording is changing state. Use Ctrl+Shift+F9 to stop.".into()), - } -} - -pub async fn show_main_controls(app: &AppHandle) -> Result<(), String> { - #[cfg(target_os = "linux")] - let instant_stop = crate::recording::linux_instant::current(app) - .filter(|attempt| !attempt.ui_ready()) - .map(|attempt| crate::recording::linux_instant::control(app.clone(), attempt, false)); - #[cfg(not(target_os = "linux"))] - let instant_stop = None::>>; - finish_main_controls(phase(app), instant_stop, control(app, false)).await -} - -pub fn control( - app: &AppHandle, - resume: bool, -) -> futures::future::BoxFuture<'_, Result<(), String>> { - Box::pin(async move { - let app_state = app.state::>(); - let (handle, dir, generation) = { - let app_state = app_state.read().await; - let Some(crate::recording::InProgressRecording::Studio { handle, common, .. }) = - app_state.current_recording() - else { - return Err("No Studio recording is available".into()); - }; - let generation = owner(app, &common.recording_dir).ok_or("Recording was superseded")?; - (handle.clone(), common.recording_dir.clone(), generation) - }; - let expected = if resume { - Phase::Paused - } else { - Phase::Recording - }; - let transition = if resume { - Phase::Resuming - } else { - Phase::Pausing - }; - { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let lease = inner - .lease - .as_mut() - .filter(|lease| { - lease.generation == generation - && lease.phase == expected - && lease.recording_dir.as_deref() == Some(dir.as_path()) - }) - .ok_or("Recording is already changing state. Use Ctrl+Shift+F9 to stop.")?; - lease.phase = transition; - inner.control_error = None; - } - notify(app); - let state = app.state::(); - ControlOperation { - resume, - generation, - dir: &dir, - hide: hide(app, generation), - change: async { - if resume { - handle.resume().await.map_err(|error| error.to_string()) - } else { - handle.pause().await.map_err(|error| error.to_string()) - } - }, - paused: async { - matches!( - tokio::time::timeout(Duration::from_secs(2), handle.is_paused()).await, - Ok(Ok(true)) - ) - }, - restore: restore_paused_main(app, generation, dir.clone()), - stop: async { - Box::pin(crate::recording::stop_clean_studio_recording( - app.clone(), - handle.clone(), - generation, - dir.clone(), - )) - .await - }, - notify: || notify(app), - } - .run(&state.inner) - .await?; - let event = if resume { - crate::recording::RecordingEvent::Resumed - } else { - crate::recording::RecordingEvent::Paused - }; - let _ = event.emit(app); - Ok(()) - }) -} - -#[derive(Debug)] -enum ControlOutcome { - Succeeded, - StoppedBeforeResume, - HideFailed(String), - ActorFailed { error: String, paused: bool }, -} - -struct ControlOperation<'a, H, C, P, R, S, N> { - resume: bool, - generation: u32, - dir: &'a std::path::Path, - hide: H, - change: C, - paused: P, - restore: R, - stop: S, - notify: N, -} - -impl ControlOperation<'_, H, C, P, R, S, N> -where - H: std::future::Future>, - C: std::future::Future>, - P: std::future::Future, - R: std::future::Future>, - S: std::future::Future>, - N: Fn(), -{ - async fn run(self, state: &Mutex) -> Result<(), String> { - let hide_result = if self.resume { self.hide.await } else { Ok(()) }; - let outcome = match hide_result { - Err(error) => ControlOutcome::HideFailed(error), - Ok(()) => { - let stop_requested = { - let inner = state.lock().unwrap(); - if !inner.control_current(self.generation, self.dir, self.resume) { - return Err("Recording stopped or changed during the operation".into()); - } - inner.lease.as_ref().unwrap().stop_requested - }; - if self.resume && stop_requested { - ControlOutcome::StoppedBeforeResume - } else { - match self.change.await { - Ok(()) => ControlOutcome::Succeeded, - Err(error) => ControlOutcome::ActorFailed { - error, - // This acknowledgement relies on Studio's transactional Resume teardown. - paused: self.resume && self.paused.await, - }, - } - } - } - }; - let stop_requested = { - let mut inner = state.lock().unwrap(); - if !inner.complete_control(self.generation, self.dir, self.resume, &outcome) { - return Err("Recording stopped or changed during the operation".into()); - } - inner.lease.as_ref().unwrap().stop_requested - }; - (self.notify)(); - if stop_requested { - self.stop.await?; - return Err("Recording stopped".into()); - } - match outcome { - ControlOutcome::Succeeded => Ok(()), - ControlOutcome::StoppedBeforeResume => Err("Recording stopped".into()), - ControlOutcome::HideFailed(error) - | ControlOutcome::ActorFailed { - error, - paused: true, - } => { - if let Err(reveal_error) = self.restore.await { - return Err(format!( - "{error}. Could not restore controls: {reveal_error}. Use {STOP_SHORTCUT} to stop." - )); - } - Err(error) - } - ControlOutcome::ActorFailed { - error, - paused: false, - } if self.resume => Err(format!( - "{error}. Recording pause could not be confirmed. Controls remain hidden; use {STOP_SHORTCUT} to stop." - )), - ControlOutcome::ActorFailed { error, .. } => Err(error), - } - } -} - -async fn restore_paused_main(app: &AppHandle, generation: u32, dir: PathBuf) -> Result<(), String> { - let main = CapWindowId::Main - .get(app) - .ok_or("Recording controls window is unavailable")?; - let (tx, rx) = tokio::sync::oneshot::channel(); - let window = main.clone(); - main.run_on_main_thread(move || { - let allowed = window - .app_handle() - .state::() - .inner - .lock() - .unwrap() - .may_restore_paused_main(generation, &dir); - let result = if allowed { - reveal_now_with_options(&window, generation, true, true) - .map_err(|error| error.to_string()) - } else { - Ok(false) - }; - let _ = tx.send(result); - }) - .map_err(|error| error.to_string())?; - let revealed = tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Timed out restoring paused controls".to_string())? - .map_err(|_| "Paused controls restore was cancelled".to_string())??; - if !revealed { - return Err("Recording stopped or changed before controls could be restored".into()); - } - Ok(()) -} - -pub async fn is_paused(app: &AppHandle) -> Result { - let state = app.state::>(); - let handle = { - let state = state.read().await; - match state.current_recording() { - Some(crate::recording::InProgressRecording::Studio { handle, .. }) => handle.clone(), - _ => return Err("No Studio recording is available".into()), - } - }; - handle.is_paused().await.map_err(|error| error.to_string()) -} - -pub fn begin_restart(app: &AppHandle, dir: &std::path::Path) -> Result, String> { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let Some(lease) = inner.lease.as_mut() else { - return Ok(None); - }; - if lease.recording_dir.as_deref() != Some(dir) - || !matches!(lease.phase, Phase::Recording | Phase::Paused) - || lease.stop_requested - { - return Err("Recording is changing state. Use Ctrl+Shift+F9 to stop.".into()); - } - lease.phase = Phase::Restarting; - lease.recording_dir = None; - Ok(Some(lease.generation)) -} - -#[cfg(target_os = "linux")] -fn native_id(window: &WebviewWindow) -> Result { - use wgpu::rwh::{HasWindowHandle, RawWindowHandle}; - if cap_recording::screenshot::uses_wayland_portal() { - use gtk::prelude::*; - return window - .gtk_window() - .map(|window| window.as_ptr() as u64) - .map_err(|error| error.to_string()); - } - match window.window_handle().map_err(|e| e.to_string())?.as_raw() { - RawWindowHandle::Xlib(handle) => Ok(handle.window), - RawWindowHandle::Xcb(handle) => Ok(u64::from(handle.window.get())), - RawWindowHandle::Wayland(_) => { - use gtk::prelude::*; - let gtk = window.gtk_window().map_err(|error| error.to_string())?; - Ok(gtk.as_ptr() as u64) - } - _ => Err("Clean capture requires an X11 or Wayland window".into()), - } -} - -#[cfg(not(target_os = "linux"))] -fn native_id(_window: &WebviewWindow) -> Result { - Err("Clean Studio capture is only available on X11".into()) -} - -fn x11_environment(display: bool, wayland: bool, session: Option<&str>) -> bool { - display && !wayland && session.is_some_and(|session| session.eq_ignore_ascii_case("x11")) -} - -pub(crate) async fn wait_for_shortcut(state: &State, generation: u32) -> Result<(), String> { - loop { - let notified = state.changed.notified(); - tokio::pin!(notified); - notified.as_mut().enable(); - { - let inner = state.inner.lock().unwrap(); - let lease = inner - .lease - .as_ref() - .filter(|lease| lease.generation == generation) - .ok_or("Recording cancelled")?; - if lease.stop_requested { - return Err( - if lease.wayland && lease.lost_stop_routes.iter().any(|lost| *lost) { - lease.stop_error.clone().unwrap_or_else(|| { - "The recording Stop control became unavailable".into() - }) - } else { - "Recording cancelled".into() - }, - ); - } - if lease.phase == Phase::Starting { - return Ok(()); - } - } - notified.await; - } -} - -#[cfg(target_os = "linux")] -pub(crate) async fn await_instant_shortcut( - state: &State, - generation: u32, - attempt: &crate::recording::linux_instant::Attempt, -) -> Result<(), String> { - attempt - .while_active(wait_for_shortcut(state, generation)) - .await -} - -#[cfg(all(test, target_os = "linux"))] -pub(crate) fn instant_preflight_fixture(generation: u32) -> State { - State { - instant: Mutex::new(None), - portal_stop: tokio::sync::Mutex::new(None), - inner: Mutex::new(Inner { - generation, - lease: Some(Lease { - mode: cap_recording::RecordingMode::Instant, - generation, - phase: Phase::AwaitingShortcut, - pressed: false, - stop_requested: false, - registered_shortcut: true, - wayland: false, - stop_route: None, - stop_description: None, - stop_error: None, - lost_stop_routes: [false; 2], - recording_dir: None, - windows: Vec::new(), - }), - ..Inner::default() - }), - changed: Notify::new(), - } -} - -#[cfg(all(test, target_os = "linux"))] -pub(crate) fn deliver_preflight_shortcut(state: &State) { - let mut inner = state.inner.lock().unwrap(); - assert!(inner.shortcut(true)); - assert!(inner.shortcut(false)); - drop(inner); - state.changed.notify_waiters(); -} - -fn capture_environment_is_x11( - mode: cap_recording::RecordingMode, - strict_x11: bool, - uses_wayland_portal: bool, -) -> bool { - if mode == cap_recording::RecordingMode::Instant { - !uses_wayland_portal - } else { - strict_x11 - } -} - -fn validate_capture_visibility( - mode: cap_recording::RecordingMode, - target: &cap_recording::screen_capture::ScreenCaptureTarget, - camera_requested: bool, - uses_wayland_portal: bool, - supported: bool, -) -> Result { - use cap_recording::{RecordingMode, screen_capture::ScreenCaptureTarget}; - let needs_visibility = match target { - ScreenCaptureTarget::Display { .. } | ScreenCaptureTarget::Area { .. } => { - matches!(mode, RecordingMode::Studio | RecordingMode::Instant) - } - ScreenCaptureTarget::Window { .. } => { - mode == RecordingMode::Instant && camera_requested && !uses_wayland_portal - } - ScreenCaptureTarget::CameraOnly => false, - }; - if !needs_visibility { - return Ok(false); - } - if !supported { - return Err("This display backend cannot acknowledge hidden recording windows. Recording has not started and selected inputs are unchanged.".into()); - } - Ok(true) -} - -pub async fn prepare( - app: &AppHandle, - inputs: &crate::recording::StartRecordingInputs, - restart: Option, -) -> Result, String> { - if !cfg!(target_os = "linux") { - return Ok(None); - } - if let Some(generation) = restart { - if phase(app) != Some(Phase::Restarting) || stop_requested(app, generation) { - return Err("Recording restart was cancelled".into()); - } - set_phase(app, generation, Phase::Starting); - return Ok(Some(generation)); - } - if phase(app).is_some() { - return Err("Finish or cancel the current recording before starting another".into()); - } - #[cfg(target_os = "linux")] - let uses_wayland_portal = cap_recording::screenshot::uses_wayland_portal(); - #[cfg(not(target_os = "linux"))] - let uses_wayland_portal = false; - if !validate_capture_visibility( - inputs.mode, - &inputs.capture_target, - app.state::() - .snapshot() - .camera - .value - .is_some(), - uses_wayland_portal, - capture_environment_is_x11( - inputs.mode, - x11_environment( - std::env::var_os("DISPLAY").is_some(), - std::env::var_os("WAYLAND_DISPLAY").is_some(), - std::env::var("XDG_SESSION_TYPE").ok().as_deref(), - ), - uses_wayland_portal, - ) || uses_wayland_portal, - )? { - return Ok(None); - } - #[cfg(target_os = "linux")] - let instant_attempt = if inputs.mode == cap_recording::RecordingMode::Instant { - let attempt = crate::recording::linux_instant::current(app) - .ok_or("Instant preflight owner was lost")?; - attempt.checked(Ok(()))?; - Some(attempt) - } else { - None - }; - let main = CapWindowId::Main - .get(app) - .ok_or("Open Cap before starting this recording")?; - - let app_state = app.state::>(); - let mut app_state = app_state.write().await; - if !matches!(app_state.recording_state, crate::RecordingState::None) { - return Err("Recording already in progress".into()); - } - let state = app.state::(); - let generation = { - let mut inner = state.inner.lock().unwrap(); - if inner.lease.is_some() { - return Err("Recording preflight already in progress".into()); - } - inner.generation = inner.generation.wrapping_add(1); - inner.control_error = None; - let generation = inner.generation; - inner.restored = None; - inner.lease = Some(Lease { - mode: inputs.mode, - generation, - phase: Phase::AwaitingShortcut, - pressed: false, - stop_requested: false, - registered_shortcut: false, - wayland: uses_wayland_portal, - stop_route: None, - stop_description: None, - stop_error: None, - lost_stop_routes: [false; 2], - recording_dir: None, - windows: Vec::new(), - }); - generation - }; - app_state.set_pending_recording(inputs.mode, inputs.capture_target.clone())?; - drop(app_state); - let result = async { - let saved = save_windows(app, generation).await?; - state.inner.lock().unwrap().lease.as_mut().unwrap().windows = saved; - #[cfg(target_os = "linux")] - if uses_wayland_portal { - crate::hotkeys::reserve_wayland_stop(app, generation).await?; - } else { - let registered = crate::hotkeys::reserve_clean_capture_stop(app)?; - state - .inner - .lock() - .unwrap() - .lease - .as_mut() - .unwrap() - .registered_shortcut = registered; - } - crate::target_select_overlay::close_target_select_overlay_windows(app); - if stop_requested(app, generation) { - return Err("Recording cancelled".into()); - } - guarded_show(main, generation, true, true) - .await - .map_err(|error| error.to_string())?; - notify(app); - #[cfg(target_os = "linux")] - if let Some(attempt) = &instant_attempt { - await_instant_shortcut(&state, generation, attempt).await?; - } else { - wait_for_shortcut(&state, generation).await?; - } - #[cfg(not(target_os = "linux"))] - wait_for_shortcut(&state, generation).await?; - #[cfg(target_os = "linux")] - if uses_wayland_portal { - crate::tray::set_clean_stop_mode(app, generation, true).await?; - } - if inputs.mode == cap_recording::RecordingMode::Studio { - hide(app, generation).await?; - } - Ok(Some(generation)) - } - .await; - if result.is_err() { - app.state::>() - .write() - .await - .clear_pending_recording(); - release(app, generation, false); - } - result -} - -fn hide_windows(app: &AppHandle) -> Result<(), String> { - #[cfg(target_os = "linux")] - if let Some(generation) = wayland_generation(app) { - return hide_wayland_windows(app, generation); - } - for (label, window) in app.webview_windows() { - if matches!( - label.parse::(), - Ok(CapWindowId::Main - | CapWindowId::Camera - | CapWindowId::RecordingControls - | CapWindowId::TargetSelectOverlay { .. }) - ) { - native_id(&window)?; - set_native_visibility(&window, false)?; - if window.is_visible().map_err(|e| e.to_string())? { - return Err("Cap could not hide its recording windows safely".into()); - } - } - } - Ok(()) -} - -pub async fn hide(app: &AppHandle, generation: u32) -> Result<(), String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let result = (|| { - if !is_current(&handle, generation) { - return Err("Recording preflight was superseded".to_string()); - } - hide_windows(&handle)?; - Ok(()) - })(); - let _ = tx.send(result); - }) - .map_err(|e| e.to_string())?; - tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Timed out hiding Cap windows".to_string())? - .map_err(|_| "Cap window hide task was cancelled".to_string())??; - #[cfg(target_os = "linux")] - if wayland_generation(app) == Some(generation) { - wayland_fence(app, generation, true).await?; - } - tokio::time::sleep(Duration::from_millis(150)).await; - if stop_requested(app, generation) { - return Err("Recording cancelled".into()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -pub(crate) struct InstantSeal { - pub generation: u32, - pub attempt: crate::recording::linux_instant::Attempt, - pub requested: crate::RequestedInputs, - pub target: cap_recording::screen_capture::ScreenCaptureTarget, - pub capture: crate::linux_instant_camera::PhysicalRect, - pub presentation: Option, -} - -#[cfg(target_os = "linux")] -fn validate_instant_seal(app: &AppHandle, seal: &InstantSeal) -> Result<(), String> { - let requested = app.state::(); - requested.ready_snapshot()?; - if !is_current(app, seal.generation) - || phase(app) != Some(Phase::Starting) - || stop_requested(app, seal.generation) - || !requested.is_current(&seal.requested) - || crate::recording::linux_instant::current(app) - .is_none_or(|attempt| !attempt.same(&seal.attempt)) - { - return Err("Instant preparation was cancelled or changed".into()); - } - seal.attempt.checked(Ok(()))?; - if crate::recording::linux_instant::capture_rect(&seal.target)? != seal.capture { - return Err("Capture bounds changed while preparing Instant recording".into()); - } - if let Some(presentation) = &seal.presentation { - let window = CapWindowId::Camera - .get(app) - .ok_or("Camera window disappeared")?; - presentation.validate_before_hide(&window)?; - } - Ok(()) -} - -#[cfg(target_os = "linux")] -async fn instant_ui_seal( - app: &AppHandle, - seal: InstantSeal, - hide: bool, -) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - let attempt = seal.attempt.clone(); - app.run_on_main_thread(move || { - let result = (|| { - validate_instant_seal(&handle, &seal)?; - if hide { - hide_windows(&handle)?; - } - Ok(seal) - })(); - let _ = tx.send(result); - }) - .map_err(|error| error.to_string())?; - let result = tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Instant camera hide acknowledgement timed out".to_string()) - .and_then(|result| { - result.map_err(|_| "Instant camera hide acknowledgement was lost".to_string()) - }) - .and_then(|result| result); - if result.is_err() { - attempt.cancel(); - } - result -} - -#[cfg(target_os = "linux")] -async fn seal_then_settle(hide: H, settle: S, validate: V) -> Result -where - H: std::future::Future>, - S: std::future::Future>, - V: FnOnce(T) -> F, - F: std::future::Future>, -{ - let sealed = hide.await?; - settle.await?; - validate(sealed).await -} - -#[cfg(target_os = "linux")] -pub(crate) async fn seal_instant( - app: &AppHandle, - seal: InstantSeal, -) -> Result { - let attempt = seal.attempt.clone(); - let generation = seal.generation; - seal_then_settle( - instant_ui_seal(app, seal, true), - attempt.while_active(async { - #[cfg(target_os = "linux")] - if wayland_generation(app) == Some(generation) { - wayland_fence(app, generation, true).await?; - } - tokio::time::sleep(Duration::from_millis(150)).await; - Ok(()) - }), - |seal| instant_ui_seal(app, seal, false), - ) - .await -} - -#[cfg(target_os = "linux")] -async fn restore_instant_windows( - app: &AppHandle, - generation: u32, - main_only: bool, -) -> Result<(), String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let result = (|| { - if crate::recording::linux_instant::current(&handle) - .is_some_and(|attempt| attempt.has_capture() && !attempt.ui_ready()) - { - return Err("Instant capture cleanup is unconfirmed".to_string()); - } - if wayland_generation(&handle) == Some(generation) { - return restore_wayland_windows(&handle, generation, main_only); - } - let windows = { - let state = handle.state::(); - let inner = state.inner.lock().unwrap(); - inner - .lease - .as_ref() - .filter(|lease| { - lease.generation == generation && lease.phase == Phase::Restoring - }) - .ok_or("Instant restoration was superseded")? - .windows - .clone() - }; - let requested = handle - .state::() - .ready_snapshot(); - if !main_only { - requested.as_ref().map_err(Clone::clone)?; - } - for saved in windows { - let main = saved.label == CapWindowId::Main.label(); - if main != main_only { - continue; - } - if !main - && requested - .as_ref() - .is_ok_and(|snapshot| snapshot.camera.value.is_none()) - { - continue; - } - let window = match handle.get_webview_window(&saved.label) { - Some(window) => window, - None if wayland_generation(&handle) == Some(generation) && !saved.visible => { - continue; - } - None => return Err("Recording restoration window disappeared".into()), - }; - let visible = saved - .visibility_for(native_id(&window)?) - .ok_or("Instant restoration window identity changed")?; - if !main - && !handle - .state::() - .is_current(requested.as_ref().map_err(Clone::clone)?) - { - return Err("Requested inputs changed during Instant restoration".into()); - } - set_native_visibility(&window, visible)?; - if window.is_visible().map_err(|error| error.to_string())? != visible { - return Err("Instant window restoration was not acknowledged".into()); - } - } - Ok(()) - })(); - let _ = tx.send(result); - }) - .map_err(|error| error.to_string())?; - tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Instant restore acknowledgement timed out".to_string())? - .map_err(|_| "Instant restore acknowledgement was lost".to_string())? -} - -#[cfg(target_os = "linux")] -async fn restore_instant_sequence( - main: M, - inputs: I, - camera: C, -) -> Result<(), String> -where - M: std::future::Future>, - I: FnOnce() -> IF, - IF: std::future::Future, - C: FnOnce() -> CF, - CF: std::future::Future>, -{ - main.await?; - inputs().await; - camera().await -} - -#[cfg(target_os = "linux")] -fn finish_x11_studio_restoration( - inner: &Mutex, - generation: u32, - windows: Result<(), String>, - release_shortcut: impl FnOnce() -> Result<(), String>, -) -> bool { - let cleanup = { - let mut inner = inner.lock().unwrap(); - inner.reserve_x11_studio_cleanup(generation, windows) - }; - let Some(cleanup) = cleanup else { - return false; - }; - let result = if cleanup.registered_shortcut { - release_shortcut() - } else { - Ok(()) - }; - inner - .lock() - .unwrap() - .complete_x11_studio_restoration(cleanup, result) -} - -#[cfg(target_os = "linux")] -fn spawn_x11_studio_restore(app: AppHandle, generation: u32, restore_inputs: bool) { - drop(tauri::async_runtime::spawn(async move { - let result = restore_instant_sequence( - restore_instant_windows(&app, generation, true), - || async { - if restore_inputs { - crate::restore_requested_inputs(&app).await; - } - }, - || restore_instant_windows(&app, generation, false), - ) - .await; - let handle = app.clone(); - let scheduled = app.run_on_main_thread(move || { - use tauri_plugin_global_shortcut::GlobalShortcutExt; - - { - let state = handle.state::(); - finish_x11_studio_restoration(&state.inner, generation, result, || { - handle - .global_shortcut() - .unregister(STOP_SHORTCUT) - .map_err(|error| { - format!("Recording Stop shortcut cleanup is unconfirmed: {error}") - }) - }); - } - notify(&handle); - }); - if let Err(error) = scheduled { - { - let state = app.state::(); - finish_x11_studio_restoration( - &state.inner, - generation, - Err(format!( - "Recording restoration completion could not be scheduled: {error}" - )), - || Ok(()), - ); - } - notify(&app); - } - })); -} - -#[cfg(target_os = "linux")] -fn spawn_instant_restore(app: AppHandle, generation: u32, restore_inputs: bool) { - drop(tauri::async_runtime::spawn(async move { - let result = restore_instant_sequence( - restore_instant_windows(&app, generation, true), - || async { - if restore_inputs { - crate::restore_requested_inputs(&app).await; - } - }, - || restore_instant_windows(&app, generation, false), - ) - .await; - let result = if wayland_generation(&app) == Some(generation) { - match result { - Ok(()) => { - async { - wayland_fence(&app, generation, false).await?; - crate::tray::set_clean_stop_mode(&app, generation, false).await?; - crate::hotkeys::release_wayland_stop(&app, generation).await - } - .await - } - Err(error) => Err(error), - } - } else { - result - }; - let owned = { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - inner.complete_instant_restoration(generation, result) - }; - if let Some(owned) = owned { - crate::hotkeys::release_clean_capture_stop(&app, owned); - let _ = app.run_on_main_thread(move || { - WAYLAND_WINDOWS.with_borrow_mut(|windows| { - windows.remove(&generation); - }); - WAYLAND_CAPTURE_WINDOWS.with_borrow_mut(|windows| { - windows.remove(&generation); - }); - }); - } - notify(&app); - })); -} - -#[cfg(target_os = "linux")] -pub(crate) async fn wait_restored(app: &AppHandle, generation: u32) -> Result<(), String> { - let state = app.state::(); - wait_for_restoration(&state, generation).await -} - -#[cfg(target_os = "linux")] -async fn wait_for_restoration(state: &State, generation: u32) -> Result<(), String> { - loop { - let changed = state.changed.notified(); - tokio::pin!(changed); - changed.as_mut().enable(); - { - let inner = state.inner.lock().unwrap(); - if let Some(receipt) = &inner.restored - && receipt.generation == generation - { - return receipt.restart_result(); - } - if inner - .lease - .as_ref() - .is_none_or(|lease| lease.generation != generation) - { - return Err("Instant restoration ownership changed without acknowledgement".into()); - } - } - changed.await; - } -} - -pub fn release(app: &AppHandle, generation: u32, editor_took_foreground: bool) { - release_inner(app, generation, editor_took_foreground, false); -} - -pub fn release_after_recording(app: &AppHandle, generation: u32, editor_took_foreground: bool) { - release_inner(app, generation, editor_took_foreground, true); -} - -async fn restore_pass_acknowledged( - scheduled: bool, - acknowledgement: tokio::sync::oneshot::Receiver>, -) -> bool { - scheduled && matches!(acknowledgement.await, Ok(Ok(()))) -} - -fn restore_saved_window(app: &AppHandle, saved: &SavedWindow) -> Result<(), String> { - let window = app.get_webview_window(&saved.label).ok_or_else(|| { - format!( - "Recording window {} disappeared before restoration", - saved.label - ) - })?; - let visible = saved.visibility_for(native_id(&window)?).ok_or_else(|| { - format!( - "Recording window {} was replaced before restoration", - saved.label - ) - })?; - set_native_visibility(&window, visible)?; - if window.is_visible().map_err(|error| error.to_string())? != visible { - return Err(format!( - "Recording window {} restoration was not acknowledged", - saved.label - )); - } - Ok(()) -} - -fn release_inner( - app: &AppHandle, - generation: u32, - editor_took_foreground: bool, - restore_inputs: bool, -) { - #[cfg(target_os = "linux")] - if crate::recording::linux_instant::current(app) - .is_some_and(|attempt| attempt.has_capture() && !attempt.ui_ready()) - { - return; - } - { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let Some(lease) = inner - .lease - .as_mut() - .filter(|lease| lease.generation == generation && lease.phase != Phase::Restoring) - else { - return; - }; - if editor_took_foreground - && (lease.wayland || lease.mode == cap_recording::RecordingMode::Studio) - { - for window in &mut lease.windows { - if window.label == CapWindowId::Main.label() { - window.visible = false; - } - } - } - lease.phase = Phase::Restoring; - } - notify(app); - #[cfg(target_os = "linux")] - if app - .state::() - .inner - .lock() - .unwrap() - .lease - .as_ref() - .is_some_and(|lease| { - lease.generation == generation - && (lease.mode == cap_recording::RecordingMode::Instant || lease.wayland) - }) - { - spawn_instant_restore(app.clone(), generation, restore_inputs); - return; - } - #[cfg(target_os = "linux")] - if app - .state::() - .inner - .lock() - .unwrap() - .lease - .as_ref() - .is_some_and(|lease| { - lease.generation == generation - && lease.mode == cap_recording::RecordingMode::Studio - && !lease.wayland - }) - { - spawn_x11_studio_restore(app.clone(), generation, restore_inputs); - return; - } - let app = app.clone(); - drop(tauri::async_runtime::spawn(async move { - let (tx, rx) = tokio::sync::oneshot::channel::>(); - let restoration_error = Arc::new(Mutex::new(None::)); - let first_error = restoration_error.clone(); - let handle = app.clone(); - let scheduled = app.run_on_main_thread(move || { - #[cfg(target_os = "linux")] - if crate::recording::linux_instant::current(&handle) - .is_some_and(|attempt| attempt.has_capture() && !attempt.ui_ready()) - { - let _ = tx.send(Err("Instant cleanup is unconfirmed".to_string())); - return; - } - let saved = { - let state = handle.state::(); - let mut inner = state.inner.lock().unwrap(); - inner - .lease - .as_mut() - .filter(|lease| lease.generation == generation) - .map(|lease| { - let owned = std::mem::take(&mut lease.registered_shortcut); - ( - lease - .windows - .iter() - .find(|saved| saved.label == CapWindowId::Main.label()) - .cloned(), - owned, - ) - }) - }; - if let Some((saved, owned)) = saved { - crate::hotkeys::release_clean_capture_stop(&handle, owned); - if !editor_took_foreground && let Some(saved) = saved { - let result = restore_saved_window(&handle, &saved); - if let Err(error) = result { - *first_error.lock().unwrap() = Some(error.to_string()); - tracing::warn!(%error, "Could not restore Main after recording"); - } - } - } - let _ = tx.send(Ok(())); - }); - if !restore_pass_acknowledged(scheduled.is_ok(), rx).await { - return; - } - if restore_inputs { - crate::restore_requested_inputs(&app).await; - } - let requested = app.state::().snapshot(); - let handle = app.clone(); - let _ = app.run_on_main_thread(move || { - #[cfg(target_os = "linux")] - if crate::recording::linux_instant::current(&handle) - .is_some_and(|attempt| attempt.has_capture() && !attempt.ui_ready()) - { - return; - } - let state = handle.state::(); - let windows = { - let inner = state.inner.lock().unwrap(); - let Some(lease) = inner.lease.as_ref().filter(|lease| { - lease.generation == generation && lease.phase == Phase::Restoring - }) else { - return; - }; - lease.windows.clone() - }; - for saved in windows { - if saved.label == CapWindowId::Main.label() { - continue; - } - if matches!(saved.label.parse::(), Ok(CapWindowId::Camera)) - && (requested.camera.value.is_none() - || requested.camera.pending - || requested.camera.error.is_some() - || !handle - .state::() - .is_current(&requested)) - { - continue; - } - if let Err(error) = restore_saved_window(&handle, &saved) { - restoration_error - .lock() - .unwrap() - .get_or_insert_with(|| error.to_string()); - tracing::warn!(%error, "Could not restore clean capture window"); - } - } - { - let mut inner = state.inner.lock().unwrap(); - let result = restoration_error.lock().unwrap().take().map_or(Ok(()), Err); - if !inner.record_stop_restoration(generation, result) { - return; - } - inner.lease = None; - inner.generation = inner.generation.wrapping_add(1); - } - notify(&handle); - }); - })); -} - -#[tauri::command] -#[specta::specta] -pub fn get_clean_capture_state(app: AppHandle) -> Snapshot { - app.state::().inner.lock().unwrap().snapshot() -} - -#[tauri::command] -#[specta::specta] -pub async fn reveal_capture_window( - window: WebviewWindow, - generation: u32, - target_overlay: Option, -) -> Result { - if let Some(label) = target_overlay { - if window.label() != CapWindowId::Main.label() - || !matches!( - label.parse::(), - Ok(CapWindowId::TargetSelectOverlay { .. }) - ) - { - return Err("Only Main can restore a target selection overlay".into()); - } - let target = window - .app_handle() - .get_webview_window(&label) - .ok_or("Target selection overlay no longer exists")?; - return reveal(target, generation).await; - } - if !matches!( - window.label().parse::(), - Ok(CapWindowId::Main | CapWindowId::Camera | CapWindowId::RecordingControls) - ) { - return Err("Only Cap recording windows can use this command".into()); - } - if matches!( - window.label().parse::(), - Ok(CapWindowId::RecordingControls) - ) && window - .app_handle() - .state::>() - .read() - .await - .current_recording() - .is_none() - { - return Ok(false); - } - let main = matches!(window.label().parse::(), Ok(CapWindowId::Main)); - reveal_with_options(window, generation, main, main).await -} - -#[cfg(all(test, target_os = "linux"))] -mod instant_activation_tests { - use super::*; - use std::sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }; - - #[tokio::test] - async fn instant_seal_waits_for_hide_ack_then_settle_then_revalidates() { - let (hide_tx, hide_rx) = tokio::sync::oneshot::channel(); - let (settle_tx, settle_rx) = tokio::sync::oneshot::channel(); - let (settling_tx, settling_rx) = tokio::sync::oneshot::channel(); - let stage = Arc::new(AtomicUsize::new(0)); - let observed = stage.clone(); - let task = tokio::spawn(async move { - let validated = observed.clone(); - seal_then_settle( - async { hide_rx.await.map_err(|_| "hide ack lost".to_string()) }, - async { - observed.store(1, Ordering::SeqCst); - settling_tx.send(()).unwrap(); - settle_rx.await.map_err(|_| "settle cancelled".to_string()) - }, - |owner| async move { - validated.store(2, Ordering::SeqCst); - Ok(owner) - }, - ) - .await - }); - tokio::task::yield_now().await; - assert_eq!(stage.load(Ordering::SeqCst), 0); - assert!(!task.is_finished()); - hide_tx.send(7).unwrap(); - settling_rx.await.unwrap(); - assert_eq!(stage.load(Ordering::SeqCst), 1); - assert!(!task.is_finished()); - settle_tx.send(()).unwrap(); - assert_eq!(task.await.unwrap().unwrap(), 7); - assert_eq!(stage.load(Ordering::SeqCst), 2); - } - - #[tokio::test] - async fn instant_lost_hide_ack_never_settles_or_builds() { - let (tx, rx) = tokio::sync::oneshot::channel::(); - drop(tx); - let result = seal_then_settle( - async { rx.await.map_err(|_| "hide ack lost".to_string()) }, - async { panic!("settle cannot begin before hide acknowledgement") }, - |_| async { panic!("build validation cannot begin") }, - ) - .await; - assert_eq!(result.unwrap_err(), "hide ack lost"); - } - - #[tokio::test] - async fn instant_cancelled_settle_does_not_revalidate_or_build() { - let result = seal_then_settle( - async { Ok(7) }, - async { Err("cancelled".into()) }, - |_| async { panic!("cancelled preparation cannot build") }, - ) - .await; - assert_eq!(result.unwrap_err(), "cancelled"); - } - - #[tokio::test] - async fn instant_changed_effect_or_geometry_after_settle_rejects_owner() { - let result = seal_then_settle(async { Ok(7) }, async { Ok(()) }, |owner| async move { - assert_eq!(owner, 7); - Err("prepared presentation changed".into()) - }) - .await; - assert_eq!(result.unwrap_err(), "prepared presentation changed"); - } - - #[tokio::test] - async fn instant_restore_waits_for_main_and_camera_acknowledgements() { - let (main_tx, main_rx) = tokio::sync::oneshot::channel(); - let (camera_tx, camera_rx) = tokio::sync::oneshot::channel(); - let (inputs_tx, inputs_rx) = tokio::sync::oneshot::channel(); - let stage = Arc::new(AtomicUsize::new(0)); - let observed = stage.clone(); - let task = tokio::spawn(async move { - restore_instant_sequence( - async { main_rx.await.map_err(|_| "main ack lost".to_string()) }, - || async { - observed.store(1, Ordering::SeqCst); - inputs_tx.send(()).unwrap(); - }, - || async { camera_rx.await.map_err(|_| "camera ack lost".to_string()) }, - ) - .await - }); - tokio::task::yield_now().await; - assert_eq!(stage.load(Ordering::SeqCst), 0); - main_tx.send(()).unwrap(); - inputs_rx.await.unwrap(); - assert_eq!(stage.load(Ordering::SeqCst), 1); - assert!(!task.is_finished()); - camera_tx.send(()).unwrap(); - task.await.unwrap().unwrap(); - } - - #[tokio::test] - async fn instant_restore_rejected_or_lost_main_ack_never_restores_inputs() { - for rejected in [true, false] { - let (tx, rx) = tokio::sync::oneshot::channel::>(); - if rejected { - tx.send(Err("unsafe cleanup".into())).unwrap(); - } else { - drop(tx); - } - let result = restore_instant_sequence( - async { rx.await.map_err(|_| "main ack lost".to_string())? }, - || async { panic!("inputs must remain untouched") }, - || async { panic!("camera must remain hidden") }, - ) - .await; - assert!(result.is_err()); - } - } - - #[tokio::test] - async fn instant_open_cap_waits_for_stop_ack_and_never_polls_studio_pause() { - let (tx, rx) = tokio::sync::oneshot::channel(); - let task = tokio::spawn(finish_main_controls( - Some(Phase::Recording), - Some(async { rx.await.map_err(|_| "stop ack lost".to_string()) }), - async { panic!("Instant OpenCap must not pause") }, - )); - tokio::task::yield_now().await; - assert!(!task.is_finished()); - tx.send(()).unwrap(); - task.await.unwrap().unwrap(); - } - - #[tokio::test] - async fn instant_open_cap_preserves_stop_failure_without_pause_or_reveal_permission() { - let result = finish_main_controls( - Some(Phase::Starting), - Some(async { Err("capture cleanup unconfirmed".into()) }), - async { panic!("Instant OpenCap must not pause") }, - ) - .await; - assert_eq!(result.unwrap_err(), "capture cleanup unconfirmed"); - } - - #[tokio::test] - async fn studio_open_cap_waits_for_pause_ack_before_allowing_reveal() { - let (started_tx, started_rx) = tokio::sync::oneshot::channel(); - let (paused_tx, paused_rx) = tokio::sync::oneshot::channel(); - let reveals = Arc::new(AtomicUsize::new(0)); - let observed = reveals.clone(); - let task = tokio::spawn(async move { - finish_main_controls( - Some(Phase::Recording), - None::>>, - async { - started_tx.send(()).unwrap(); - paused_rx.await.map_err(|_| "pause ack lost".to_string()) - }, - ) - .await?; - observed.fetch_add(1, Ordering::SeqCst); - Ok::<(), String>(()) - }); - started_rx.await.unwrap(); - assert!(!task.is_finished()); - assert_eq!(reveals.load(Ordering::SeqCst), 0); - paused_tx.send(()).unwrap(); - task.await.unwrap().unwrap(); - assert_eq!(reveals.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn studio_open_cap_preserves_pause_failure_without_allowing_reveal() { - let result: Result<(), String> = async { - finish_main_controls( - Some(Phase::Recording), - None::>>, - async { Err("pause acknowledgement failed".into()) }, - ) - .await?; - panic!("a failed pause must not permit controls to be revealed") - } - .await; - assert_eq!(result.unwrap_err(), "pause acknowledgement failed"); - } - - #[tokio::test] - async fn repeated_studio_open_cap_keeps_the_recording_paused() { - for _ in 0..3 { - finish_main_controls( - Some(Phase::Paused), - None::>>, - async { panic!("an already paused recording must not be changed") }, - ) - .await - .unwrap(); - } - } - - #[tokio::test] - async fn studio_open_cap_rejects_pending_transitions_without_changing_capture() { - for phase in [ - Phase::Starting, - Phase::Pausing, - Phase::Resuming, - Phase::ResumeFailed, - Phase::Restarting, - Phase::Stopping, - ] { - let result = finish_main_controls( - Some(phase), - None::>>, - async { panic!("a pending recording transition must not be changed") }, - ) - .await; - assert_eq!( - result.unwrap_err(), - "Recording is changing state. Use Ctrl+Shift+F9 to stop." - ); - } - } - - #[tokio::test] - async fn open_cap_keeps_preflight_visible_and_studio_pause_semantics() { - finish_main_controls( - Some(Phase::AwaitingShortcut), - Some(async { panic!("preflight must not issue Stop") }), - async { panic!("preflight must not issue Pause") }, - ) - .await - .unwrap(); - finish_main_controls( - Some(Phase::Recording), - None::>>, - async { Ok(()) }, - ) - .await - .unwrap(); - } - - fn restoring() -> Inner { - Inner { - generation: 7, - lease: Some(Lease { - mode: cap_recording::RecordingMode::Instant, - generation: 7, - phase: Phase::Restoring, - pressed: false, - stop_requested: false, - registered_shortcut: true, - wayland: false, - stop_route: None, - stop_description: None, - stop_error: None, - lost_stop_routes: [false; 2], - recording_dir: None, - windows: Vec::new(), - }), - ..Inner::default() - } - } - - #[test] - fn instant_restore_error_retains_lease_shortcut_and_error_until_successful_retry() { - let mut inner = restoring(); - assert_eq!( - inner.complete_instant_restoration(7, Err("camera ack lost".into())), - None - ); - assert_eq!(inner.snapshot().error.as_deref(), Some("camera ack lost")); - assert!(inner.lease.as_ref().unwrap().registered_shortcut); - assert_eq!(inner.generation, 7); - assert_eq!(inner.complete_instant_restoration(7, Ok(())), Some(true)); - assert!(inner.lease.is_none()); - assert_eq!(inner.generation, 8); - let receipt = inner.restored.as_ref().unwrap(); - assert_eq!(receipt.generation, 7); - assert_eq!(receipt.restart_result(), Ok(())); - } - - #[test] - fn instant_stop_during_restoration_prevents_restart_after_successful_window_ack() { - let mut inner = restoring(); - assert!(inner.shortcut(true)); - assert_eq!(inner.complete_instant_restoration(7, Ok(())), Some(true)); - assert!(inner.lease.is_none()); - assert_eq!(inner.generation, 8); - assert!(inner.restored.as_ref().unwrap().restart_result().is_err()); - } - - #[test] - fn instant_stale_restore_receipt_cannot_clear_new_lease_or_error() { - let mut inner = restoring(); - assert_eq!(inner.complete_instant_restoration(6, Ok(())), None); - assert!(inner.restored.is_none()); - inner.lease.as_mut().unwrap().phase = Phase::Starting; - assert_eq!(inner.complete_instant_restoration(7, Ok(())), None); - assert!(inner.lease.as_ref().unwrap().registered_shortcut); - assert!(inner.restored.is_none()); - } -} - -#[cfg(target_os = "linux")] -thread_local! { - static WAYLAND_WINDOWS: std::cell::RefCell>> = std::cell::RefCell::new(std::collections::HashMap::new()); -} - -#[cfg(target_os = "linux")] -pub(crate) fn wayland_generation(app: &AppHandle) -> Option { - let state = app.try_state::()?; - let inner = state.inner.lock().unwrap(); - inner - .lease - .as_ref() - .filter(|lease| lease.wayland) - .map(|lease| lease.generation) -} - -#[cfg(target_os = "linux")] -pub(crate) fn describe_wayland_stop(app: &AppHandle, generation: u32, description: String) { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - if let Some(lease) = inner - .lease - .as_mut() - .filter(|lease| lease.generation == generation && lease.wayland) - { - lease.stop_description = Some(description); - } - drop(inner); - notify(app); -} - -async fn save_windows(app: &AppHandle, generation: u32) -> Result, String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let result = (|| { - if !is_current(&handle, generation) { - return Err("Recording preflight was superseded".to_string()); - } - #[cfg(target_os = "linux")] - if wayland_generation(&handle) == Some(generation) { - install_wayland_window_guard(&handle)?; - remember_wayland_windows(&handle, generation)?; - } - #[cfg(target_os = "linux")] - let save_all = wayland_generation(&handle) == Some(generation); - #[cfg(not(target_os = "linux"))] - let save_all = false; - let mut saved = Vec::new(); - for (label, window) in handle.webview_windows() { - if save_all - || matches!( - label.parse::(), - Ok(CapWindowId::Main | CapWindowId::Camera) - ) - { - #[cfg(target_os = "linux")] - if wayland_generation(&handle) == Some(generation) { - let gtk = window.gtk_window().map_err(|error| error.to_string())?; - WAYLAND_WINDOWS.with_borrow_mut(|windows| { - windows.entry(generation).or_default().push(gtk) - }); - } - saved.push(SavedWindow { - label, - native_id: native_id(&window)?, - visible: window.is_visible().map_err(|error| error.to_string())?, - }); - } - } - Ok(saved) - })(); - let _ = tx.send(result); - }) - .map_err(|error| error.to_string())?; - tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Timed out saving recording windows".to_string())? - .map_err(|_| "Recording windows could not be saved".to_string())? -} - -fn set_native_visibility(window: &WebviewWindow, visible: bool) -> Result<(), String> { - #[cfg(target_os = "linux")] - { - use gtk::prelude::*; - let gtk = window.gtk_window().map_err(|error| error.to_string())?; - // Tao queues GTK visibility changes even on the UI thread; this gate needs - // the native change to complete before checking its acknowledgement. - if visible { - gtk.show_all(); - } else { - gtk.hide(); - } - if gtk.is_visible() != visible || (!visible && gtk.is_mapped()) { - return Err("GTK did not acknowledge the recording window visibility change".into()); - } - Ok(()) - } - #[cfg(not(target_os = "linux"))] - { - if visible { - window.show() - } else { - window.hide() - } - .map_err(|error| error.to_string()) - } -} - -#[cfg(target_os = "linux")] -async fn wayland_fence(app: &AppHandle, generation: u32, hidden: bool) -> Result<(), String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let display = (|| { - use gtk::prelude::*; - if !is_current(&handle, generation) { - return Err("Recording visibility fence was superseded".to_string()); - } - let gtk = if let Some(window) = CapWindowId::Main.get(&handle) { - window.gtk_window().map_err(|error| error.to_string())? - } else if !hidden && phase(&handle) == Some(Phase::Restoring) { - WAYLAND_WINDOWS - .with_borrow(|windows| { - windows - .get(&generation) - .and_then(|windows| windows.first()) - .cloned() - }) - .ok_or("Retained recording display disappeared")? - } else { - return Err("Main recording window disappeared".into()); - }; - let display = gtk.display(); - if display.type_().name() != "GdkWaylandDisplay" { - return Err( - "The recording window is not connected to the Wayland compositor".into(), - ); - } - Ok(display) - })(); - match display { - Ok(display) => wayland_ack::start(display, handle, generation, hidden, tx), - Err(error) => { - let _ = tx.send(Err(error)); - } - } - }) - .map_err(|error| error.to_string())?; - tokio::time::timeout(Duration::from_secs(3), rx) - .await - .map_err(|_| "Wayland visibility acknowledgement timed out".to_string())? - .map_err(|_| "Wayland visibility acknowledgement was lost".to_string())??; - if !is_current(app, generation) { - return Err("Recording visibility acknowledgement was superseded".into()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -mod wayland_ack { - use gtk::{gdk, prelude::*}; - use std::{ - cell::{Cell, RefCell}, - ffi::c_void, - rc::Rc, - time::Duration, - }; - use wayland_sys::{client::*, common::*}; - - type Reply = tokio::sync::oneshot::Sender>; - struct Pending { - callback: Cell<*mut wl_proxy>, - reply: RefCell>, - _display: gdk::Display, - app: tauri::AppHandle, - generation: u32, - hidden: bool, - } - impl Pending { - fn finish(&self, result: Result<(), String>) { - let callback = self.callback.replace(std::ptr::null_mut()); - if !callback.is_null() { - unsafe { - (wayland_client_handle().wl_proxy_destroy)(callback); - } - } - if let Some(reply) = self.reply.borrow_mut().take() { - let _ = reply.send(result); - } - } - } - static CALLBACK: wl_interface = wl_interface { - name: c"wl_callback".as_ptr(), - version: 1, - request_count: 0, - requests: std::ptr::null(), - event_count: 1, - events: &wl_message { - name: c"done".as_ptr(), - signature: c"u".as_ptr(), - types: std::ptr::null(), - }, - }; - unsafe extern "C" fn dispatch( - _: *const c_void, - data: *mut c_void, - opcode: u32, - _: *const wl_message, - _: *const wl_argument, - ) -> i32 { - let pending = unsafe { - &*((wayland_client_handle().wl_proxy_get_user_data)(data.cast()).cast::()) - }; - pending.finish(if opcode == 0 { - if pending.hidden { - super::verify_wayland_hidden(&pending.app, pending.generation) - } else { - super::verify_wayland_restored(&pending.app, pending.generation) - } - } else { - Err("Unexpected Wayland visibility acknowledgement".into()) - }); - 0 - } - pub(super) fn start( - display: gdk::Display, - app: tauri::AppHandle, - generation: u32, - hidden: bool, - reply: Reply, - ) { - let Some(client) = wayland_client_option() else { - let _ = reply.send(Err("Wayland client library is unavailable".into())); - return; - }; - let raw = - unsafe { gdk_wayland_sys::gdk_wayland_display_get_wl_display(display.as_ptr().cast()) } - .cast::(); - if raw.is_null() { - let _ = reply.send(Err("Wayland display connection is unavailable".into())); - return; - } - let mut args = [wl_argument { - o: std::ptr::null(), - }]; - let callback = unsafe { - (client.wl_proxy_marshal_array_constructor)(raw.cast(), 0, args.as_mut_ptr(), &CALLBACK) - }; - if callback.is_null() { - let _ = reply.send(Err( - "Could not queue Wayland visibility acknowledgement".into() - )); - return; - } - let pending = Rc::new(Pending { - callback: Cell::new(callback), - reply: RefCell::new(Some(reply)), - _display: display.clone(), - app, - generation, - hidden, - }); - let result = unsafe { - (client.wl_proxy_add_dispatcher)( - callback, - dispatch, - std::ptr::null(), - Rc::as_ptr(&pending).cast_mut().cast(), - ) - }; - if result != 0 { - pending.finish(Err( - "Could not subscribe to Wayland visibility acknowledgement".into(), - )); - return; - } - // GDK owns dispatch on this connection. The timeout retains listener data until - // either the callback or timeout destroys the proxy, without reading GDK's queue. - gtk::glib::timeout_add_local_once(Duration::from_secs(2), move || { - pending.finish(Err( - "Wayland compositor did not acknowledge window visibility".into(), - )) - }); - display.flush(); - } -} - -#[cfg(target_os = "linux")] -pub(crate) fn wayland_stop_lost(app: &AppHandle, generation: u32, route: StopRoute, error: String) { - let state = app.state::(); - let mut inner = state.inner.lock().unwrap(); - let Some(lease) = inner - .lease - .as_mut() - .filter(|lease| lease.generation == generation && lease.wayland) - else { - return; - }; - let stop = lease.lose_stop_route(route); - let studio_stop = (lease.mode == cap_recording::RecordingMode::Studio - && (stop || lease.stop_requested && lease.phase == Phase::Stopping)) - .then(|| lease.recording_dir.clone()) - .flatten(); - if lease.stop_requested { - lease.stop_error = Some(error); - } else if route == StopRoute::Tray { - lease.stop_description = Some("the portal shortcut shown by your desktop".into()); - } - drop(inner); - notify(app); - if let Some(directory) = studio_stop { - crate::recording::queue_clean_studio_stop(app, generation, directory); - } else if stop { - let app = app.clone(); - drop(tauri::async_runtime::spawn(async move { - if let Err(error) = crate::recording::stop_recording(app.clone(), app.state()).await { - tracing::error!(%error, "Could not stop recording after losing its Stop control"); - } - })); - } -} - -#[cfg(target_os = "linux")] -fn verify_wayland_hidden(app: &AppHandle, generation: u32) -> Result<(), String> { - use gtk::prelude::*; - if !is_current(app, generation) || stop_requested(app, generation) { - return Err("Recording hide acknowledgement was superseded or cancelled".into()); - } - let main = CapWindowId::Main - .get(app) - .ok_or("Main recording window disappeared")? - .gtk_window() - .map_err(|error| error.to_string())?; - let display = main.display(); - for window in wayland_application(app)?.windows() { - if window.display() != display || window.is_visible() || window.is_mapped() { - return Err("A Cap window is visible or uses another display connection".into()); - } - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn verify_wayland_restored(app: &AppHandle, generation: u32) -> Result<(), String> { - use gtk::prelude::*; - for (saved, wanted) in wayland_restore_plan(app, generation, true)? { - if saved.window.is_visible() != wanted || saved.window.is_mapped() != wanted { - return Err("The compositor restore acknowledgement did not match Cap windows".into()); - } - } - Ok(()) -} - -fn wayland_blocks_mapping(phase: Phase) -> bool { - !matches!( - phase, - Phase::AwaitingShortcut | Phase::Paused | Phase::Restoring - ) -} - -#[cfg(any(target_os = "linux", test))] -fn restore_floating_window(label: Option<&str>) -> bool { - !matches!( - label.and_then(|label| label.parse::().ok()), - Some( - CapWindowId::RecordingControls - | CapWindowId::TargetSelectOverlay { .. } - | CapWindowId::WindowCaptureOccluder { .. } - | CapWindowId::CaptureArea - ) - ) -} - -#[cfg(target_os = "linux")] -#[derive(Clone)] -struct RetainedWaylandWindow { - window: gtk::Window, - label: Option, - visible: bool, - requested_during_stop: bool, -} - -#[cfg(target_os = "linux")] -thread_local! { - static WAYLAND_GUARD_APPLICATION: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; - static WAYLAND_GUARDED_WINDOWS: std::cell::RefCell>> = const { std::cell::RefCell::new(Vec::new()) }; - static WAYLAND_CAPTURE_WINDOWS: std::cell::RefCell>> = std::cell::RefCell::new(std::collections::HashMap::new()); -} - -#[cfg(target_os = "linux")] -fn wayland_application(app: &AppHandle) -> Result { - use gtk::prelude::*; - CapWindowId::Main - .get(app) - .ok_or("Main recording window disappeared")? - .gtk_window() - .map_err(|error| error.to_string())? - .application() - .ok_or("Recording GTK application disappeared".into()) -} - -#[cfg(target_os = "linux")] -fn wayland_window_label(app: &AppHandle, native: >k::Window) -> Option { - use gtk::prelude::*; - app.webview_windows() - .into_iter() - .find_map(|(label, window)| { - window - .gtk_window() - .ok() - .filter(|window| window.upcast_ref::() == native) - .map(|_| label) - }) -} - -#[cfg(target_os = "linux")] -fn remember_wayland_window( - app: &AppHandle, - generation: u32, - window: >k::Window, - during_stop: bool, -) { - use gtk::prelude::*; - let label = wayland_window_label(app, window); - WAYLAND_CAPTURE_WINDOWS.with_borrow_mut(|windows| { - let windows = windows.entry(generation).or_default(); - if let Some(saved) = windows.iter_mut().find(|saved| saved.window == *window) { - if during_stop { - saved.requested_during_stop = true; - } - } else { - windows.push(RetainedWaylandWindow { - window: window.clone(), - label, - visible: window.is_visible(), - requested_during_stop: during_stop, - }); - } - }); -} - -#[cfg(target_os = "linux")] -fn guard_wayland_window(app: &AppHandle, window: >k::Window) { - use gtk::prelude::*; - let already_guarded = WAYLAND_GUARDED_WINDOWS.with_borrow_mut(|windows| { - windows.retain(|window| window.upgrade().is_some()); - if windows - .iter() - .any(|saved| saved.upgrade().as_ref() == Some(window)) - { - true - } else { - windows.push(window.downgrade()); - false - } - }); - if already_guarded { - return; - } - let app = app.clone(); - window.connect_map(move |window| { - let blocked = { - let state = app.state::(); - let inner = state.inner.lock().unwrap(); - inner - .lease - .as_ref() - .filter(|lease| lease.wayland && wayland_blocks_mapping(lease.phase)) - .map(|lease| (lease.generation, lease.phase == Phase::Stopping)) - }; - if let Some((generation, during_stop)) = blocked { - remember_wayland_window(&app, generation, window, during_stop); - // GTK's map default handler sends an empty Wayland commit with updates frozen - // until initial configure. This synchronous handler unmaps before that dispatch. - window.hide(); - } - }); -} - -#[cfg(target_os = "linux")] -fn install_wayland_window_guard(app: &AppHandle) -> Result<(), String> { - use gtk::prelude::*; - let application = wayland_application(app)?; - let installed = WAYLAND_GUARD_APPLICATION - .with_borrow(|saved| saved.as_ref().and_then(gtk::glib::WeakRef::upgrade)); - if let Some(installed) = installed { - if installed != application { - return Err("Recording GTK application identity changed".into()); - } - return Ok(()); - } - let handle = app.clone(); - application.connect_window_added(move |_, window| guard_wayland_window(&handle, window)); - for window in application.windows() { - guard_wayland_window(app, &window); - } - WAYLAND_GUARD_APPLICATION.with_borrow_mut(|saved| *saved = Some(application.downgrade())); - Ok(()) -} - -#[cfg(target_os = "linux")] -fn remember_wayland_windows(app: &AppHandle, generation: u32) -> Result<(), String> { - use gtk::prelude::*; - for window in wayland_application(app)?.windows() { - remember_wayland_window(app, generation, &window, false); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn hide_wayland_windows(app: &AppHandle, generation: u32) -> Result<(), String> { - use gtk::prelude::*; - remember_wayland_windows(app, generation)?; - for window in wayland_application(app)?.windows() { - window.hide(); - if window.is_visible() || window.is_mapped() { - return Err("GTK could not hide a Cap window".into()); - } - } - Ok(()) -} - -#[cfg(target_os = "linux")] -pub(crate) fn admit_wayland_window_creation(app: &AppHandle) -> Result<(), String> { - let state = app.state::(); - let inner = state.inner.lock().unwrap(); - if inner.lease.as_ref().is_some_and(|lease| { - lease.wayland && wayland_blocks_mapping(lease.phase) && lease.phase != Phase::Stopping - }) { - return Err("Pause or stop recording before opening another Cap window".into()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn wayland_restore_plan( - app: &AppHandle, - generation: u32, - require_inputs: bool, -) -> Result, String> { - use gtk::prelude::*; - let saved_main = { - let state = app.state::(); - let inner = state.inner.lock().unwrap(); - let lease = inner - .lease - .as_ref() - .filter(|lease| lease.generation == generation && lease.phase == Phase::Restoring) - .ok_or("Wayland restoration was superseded")?; - lease - .windows - .iter() - .find(|window| window.label == CapWindowId::Main.label()) - .cloned() - }; - let requested = if require_inputs { - Some( - app.state::() - .ready_snapshot()?, - ) - } else { - None - }; - let retained = WAYLAND_CAPTURE_WINDOWS - .with_borrow(|windows| windows.get(&generation).cloned()) - .ok_or("Retained Wayland windows disappeared")?; - let application = WAYLAND_GUARD_APPLICATION - .with_borrow(|saved| saved.as_ref().and_then(gtk::glib::WeakRef::upgrade)) - .ok_or("Retained GTK application disappeared")?; - let current = application.windows(); - let mut result = Vec::new(); - for mut saved in retained { - let label = wayland_window_label(app, &saved.window); - let label_changed = saved.label.is_some() && saved.label != label; - if saved.label.is_none() { - saved.label = label; - } - let camera = saved - .label - .as_deref() - .is_some_and(|label| matches!(label.parse(), Ok(CapWindowId::Camera))); - let editor = saved - .label - .as_deref() - .is_some_and(|label| matches!(label.parse(), Ok(CapWindowId::Editor { .. }))); - let mut wanted = (saved.visible || (editor && saved.requested_during_stop)) - && restore_floating_window(saved.label.as_deref()) - && (!camera - || requested - .as_ref() - .is_some_and(|snapshot| snapshot.camera.value.is_some())); - if saved.label.as_deref() == Some("main") { - wanted = saved_main.as_ref().is_some_and(|saved| saved.visible); - } - if wanted && label_changed { - return Err("A retained Cap window changed identity".into()); - } - if wanted && !current.contains(&saved.window) { - return Err("A retained Cap window disappeared before restoration".into()); - } - result.push((saved, wanted)); - } - if requested.as_ref().is_some_and(|requested| { - !app.state::() - .is_current(requested) - }) { - return Err("Requested inputs changed during Wayland restoration".into()); - } - Ok(result) -} - -#[cfg(target_os = "linux")] -fn restore_wayland_windows( - app: &AppHandle, - generation: u32, - main_only: bool, -) -> Result<(), String> { - use gtk::prelude::*; - for (saved, wanted) in wayland_restore_plan(app, generation, !main_only)? { - if (saved.label.as_deref() == Some("main")) != main_only { - continue; - } - if wanted { - saved.window.show_all(); - if saved.requested_during_stop - && saved - .label - .as_deref() - .is_some_and(|label| matches!(label.parse(), Ok(CapWindowId::Editor { .. }))) - { - saved.window.present(); - } - } else { - saved.window.hide(); - } - if saved.window.is_visible() != wanted || (!wanted && saved.window.is_mapped()) { - return Err("GTK did not acknowledge Cap window restoration".into()); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn wayland_blocks_all_floating_labels_while_capture_can_run() { - for mode in [ - cap_recording::RecordingMode::Studio, - cap_recording::RecordingMode::Instant, - ] { - for phase in [ - Phase::Starting, - Phase::Recording, - Phase::Pausing, - Phase::Resuming, - Phase::ResumeFailed, - Phase::Restarting, - Phase::Stopping, - ] { - let mut inner = fixture(); - let lease = inner.lease.as_mut().unwrap(); - lease.mode = mode; - lease.wayland = true; - lease.phase = phase; - for label in [ - "main", - "camera", - "teleprompter", - "settings", - "editor-4", - "unknown-js-window", - ] { - assert!(!inner.may_reveal(1, label), "{phase:?}: {label}"); - } - } - } - } - - #[test] - fn x11_floating_admission_is_unchanged() { - let mut inner = fixture(); - inner.lease.as_mut().unwrap().phase = Phase::Recording; - assert!(!inner.lease.as_ref().unwrap().wayland); - assert!(inner.may_reveal(1, "teleprompter")); - assert!(inner.may_reveal(1, "settings")); - assert!(!inner.may_reveal(1, "camera")); - } - - #[test] - fn wayland_restore_keeps_non_camera_window_intent() { - for label in ["teleprompter", "settings", "editor-4", "unknown-js-window"] { - assert!(restore_floating_window(Some(label))); - } - assert!(restore_floating_window(None)); - for label in [ - "capture-area", - "in-progress-recording", - "target-select-overlay-1", - "window-capture-occluder-1", - ] { - assert!(!restore_floating_window(Some(label))); - } - } - - #[test] - fn wayland_safe_ui_phases_allow_mapping_but_old_generation_does_not() { - for phase in [Phase::AwaitingShortcut, Phase::Paused, Phase::Restoring] { - assert!(!wayland_blocks_mapping(phase)); - let mut inner = fixture(); - let lease = inner.lease.as_mut().unwrap(); - lease.wayland = true; - lease.phase = phase; - assert!(inner.may_reveal(1, "teleprompter")); - assert!(!inner.may_reveal(0, "teleprompter")); - } - } - - #[tokio::test] - async fn restore_pass_waits_for_successful_ack_before_following_work() { - let (tx, rx) = tokio::sync::oneshot::channel(); - let work = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let observed = work.clone(); - let task = tokio::spawn(async move { - if restore_pass_acknowledged(true, rx).await { - observed.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } - }); - tokio::task::yield_now().await; - assert!(!task.is_finished()); - assert_eq!(work.load(std::sync::atomic::Ordering::SeqCst), 0); - tx.send(Ok(())).unwrap(); - task.await.unwrap(); - assert_eq!(work.load(std::sync::atomic::Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn rejected_restore_pass_does_not_authorize_following_work() { - let (tx, rx) = tokio::sync::oneshot::channel(); - tx.send(Err("Instant cleanup is unconfirmed".into())) - .unwrap(); - assert!(!restore_pass_acknowledged(true, rx).await); - } - - #[tokio::test] - async fn dropped_restore_ack_does_not_authorize_following_work() { - let (tx, rx) = tokio::sync::oneshot::channel(); - drop(tx); - assert!(!restore_pass_acknowledged(true, rx).await); - } - - #[tokio::test] - async fn unscheduled_restore_pass_does_not_wait_for_an_ack() { - let (tx, rx) = tokio::sync::oneshot::channel(); - assert!( - !tokio::time::timeout( - std::time::Duration::from_millis(100), - restore_pass_acknowledged(false, rx), - ) - .await - .unwrap() - ); - assert!(tx.is_closed()); - } - - #[cfg(target_os = "linux")] - fn x11_studio_restoring_fixture() -> Mutex { - let mut inner = fixture(); - inner.lease.as_mut().unwrap().phase = Phase::Restoring; - Mutex::new(inner) - } - - #[cfg(target_os = "linux")] - #[tokio::test] - async fn x11_studio_restore_waits_for_windows_before_stop_cleanup_and_receipt() { - use std::sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }; - - let inner = Arc::new(x11_studio_restoring_fixture()); - let inputs = Arc::new(AtomicUsize::new(0)); - let cleanup = Arc::new(AtomicUsize::new(0)); - let (main_tx, main_rx) = tokio::sync::oneshot::channel::>(); - let (camera_tx, camera_rx) = tokio::sync::oneshot::channel::>(); - let (inputs_tx, inputs_rx) = tokio::sync::oneshot::channel(); - let task = tokio::spawn({ - let inner = inner.clone(); - let inputs = inputs.clone(); - let cleanup = cleanup.clone(); - async move { - let result = restore_instant_sequence( - async { main_rx.await.unwrap() }, - || async move { - inputs.fetch_add(1, Ordering::SeqCst); - inputs_tx.send(()).unwrap(); - }, - || async { camera_rx.await.unwrap() }, - ) - .await; - assert!(finish_x11_studio_restoration(&inner, 1, result, || { - cleanup.fetch_add(1, Ordering::SeqCst); - Ok(()) - })); - } - }); - tokio::task::yield_now().await; - assert!(!task.is_finished()); - assert_eq!(inputs.load(Ordering::SeqCst), 0); - assert_eq!(cleanup.load(Ordering::SeqCst), 0); - assert!(inner.lock().unwrap().restored.is_none()); - main_tx.send(Ok(())).unwrap(); - inputs_rx.await.unwrap(); - assert!(!task.is_finished()); - assert_eq!(inputs.load(Ordering::SeqCst), 1); - assert_eq!(cleanup.load(Ordering::SeqCst), 0); - assert!(inner.lock().unwrap().lease.is_some()); - assert!(inner.lock().unwrap().restored.is_none()); - camera_tx.send(Ok(())).unwrap(); - task.await.unwrap(); - let inner = inner.lock().unwrap(); - assert!(inner.lease.is_none()); - assert_eq!(inner.generation, 2); - let receipt = inner.restored.as_ref().unwrap(); - assert_eq!(receipt.generation, 1); - assert_eq!(receipt.restart_result(), Ok(())); - assert_eq!(cleanup.load(Ordering::SeqCst), 1); - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_restore_failures_retain_ownership_and_report_failure_until_retry() { - for (windows, cleanup, error, cleanup_calls) in [ - ( - Err("window restoration failed".to_string()), - Ok(()), - "window restoration failed", - 0, - ), - ( - Ok(()), - Err("Stop cleanup failed".to_string()), - "Stop cleanup failed", - 1, - ), - ] { - let state = x11_studio_restoring_fixture(); - let calls = std::cell::Cell::new(0); - assert!(!finish_x11_studio_restoration(&state, 1, windows, || { - calls.set(calls.get() + 1); - cleanup - })); - assert_eq!(calls.get(), cleanup_calls); - { - let inner = state.lock().unwrap(); - assert_eq!(inner.generation, 1); - let lease = inner.lease.as_ref().unwrap(); - assert_eq!(lease.phase, Phase::Restoring); - assert!(lease.registered_shortcut); - assert!(inner.x11_cleanup.is_none()); - assert_eq!(inner.snapshot().error.as_deref(), Some(error)); - let receipt = inner.restored.as_ref().unwrap(); - assert_eq!(receipt.generation, 1); - assert_eq!(receipt.restart_result(), Err(error.to_string())); - } - assert!(!finish_x11_studio_restoration(&state, 1, Ok(()), || { - panic!("a duplicate completion cannot retry cleanup") - })); - { - let mut inner = state.lock().unwrap(); - assert_eq!(inner.begin_x11_studio_restore_retry(false), None); - assert_eq!(inner.begin_x11_studio_restore_retry(true), Some(1)); - assert!(inner.lease.as_ref().unwrap().stop_requested); - } - assert!(finish_x11_studio_restoration(&state, 1, Ok(()), || Ok(()))); - let inner = state.lock().unwrap(); - assert!(inner.lease.is_none()); - assert_eq!(inner.generation, 2); - assert_eq!( - inner.restored.as_ref().unwrap().restart_result(), - Err("Recording restart was cancelled by Stop".into()) - ); - } - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_stale_and_duplicate_restore_cannot_release_another_owner() { - let state = x11_studio_restoring_fixture(); - assert!(!finish_x11_studio_restoration(&state, 0, Ok(()), || { - panic!("a stale generation must not release the shortcut") - })); - assert!(state.lock().unwrap().restored.is_none()); - state.lock().unwrap().lease.as_mut().unwrap().phase = Phase::Starting; - assert!(!finish_x11_studio_restoration(&state, 1, Ok(()), || { - panic!("a live recording must not release the shortcut") - })); - state.lock().unwrap().lease.as_mut().unwrap().phase = Phase::Restoring; - assert!(finish_x11_studio_restoration(&state, 1, Ok(()), || Ok(()))); - assert!(!finish_x11_studio_restoration(&state, 1, Ok(()), || { - panic!("a duplicate completion must not release the shortcut") - })); - assert_eq!(state.lock().unwrap().generation, 2); - let next = x11_studio_restoring_fixture(); - { - let mut inner = next.lock().unwrap(); - inner.generation = 2; - inner.lease.as_mut().unwrap().generation = 2; - } - assert!(!finish_x11_studio_restoration(&next, 1, Ok(()), || { - panic!("an old completion must not release the new shortcut") - })); - let inner = next.lock().unwrap(); - assert_eq!(inner.generation, 2); - assert!(inner.lease.as_ref().unwrap().registered_shortcut); - assert!(inner.restored.is_none()); - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_restore_keeps_the_users_existing_stop_shortcut() { - let state = x11_studio_restoring_fixture(); - state - .lock() - .unwrap() - .lease - .as_mut() - .unwrap() - .registered_shortcut = false; - assert!(finish_x11_studio_restoration(&state, 1, Ok(()), || { - panic!("the user shortcut is not owned by this restoration") - })); - let inner = state.lock().unwrap(); - assert_eq!(inner.restored.as_ref().unwrap().restart_result(), Ok(())); - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_stop_during_restoration_prevents_restart_after_cleanup() { - let state = x11_studio_restoring_fixture(); - assert!(state.lock().unwrap().shortcut(true)); - assert!(finish_x11_studio_restoration(&state, 1, Ok(()), || Ok(()))); - let inner = state.lock().unwrap(); - assert!(inner.lease.is_none()); - assert_eq!( - inner.restored.as_ref().unwrap().restart_result(), - Err("Recording restart was cancelled by Stop".into()) - ); - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_completion_does_not_accept_instant_or_wayland_leases() { - for (mode, wayland) in [ - (cap_recording::RecordingMode::Instant, false), - (cap_recording::RecordingMode::Studio, true), - ] { - let state = x11_studio_restoring_fixture(); - { - let mut inner = state.lock().unwrap(); - let lease = inner.lease.as_mut().unwrap(); - lease.mode = mode; - lease.wayland = wayland; - assert_eq!(inner.begin_x11_studio_restore_retry(true), None); - } - assert!(!finish_x11_studio_restoration(&state, 1, Ok(()), || { - panic!("another restoration path owns this shortcut") - })); - let inner = state.lock().unwrap(); - assert!(inner.restored.is_none()); - assert!(inner.lease.is_some()); - } - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_unregister_allows_the_f9_callback_to_lock_and_latch_stop() { - let state = x11_studio_restoring_fixture(); - let (request_tx, request_rx) = std::sync::mpsc::channel(); - let (reply_tx, reply_rx) = std::sync::mpsc::channel(); - std::thread::scope(|scope| { - let callback_state = &state; - let callback = scope.spawn(move || { - request_rx.recv_timeout(Duration::from_secs(1)).unwrap(); - let mut inner = callback_state.lock().unwrap(); - assert!(inner.x11_cleanup.is_some()); - assert!(inner.lease.as_ref().unwrap().registered_shortcut); - assert!(inner.reserve_x11_studio_cleanup(1, Ok(())).is_none()); - assert!(inner.shortcut(true)); - drop(inner); - reply_tx.send(()).unwrap(); - }); - assert!(finish_x11_studio_restoration(&state, 1, Ok(()), || { - request_tx.send(()).unwrap(); - reply_rx - .recv_timeout(Duration::from_secs(1)) - .map_err(|_| "F9 callback could not acquire restoration state".into()) - })); - callback.join().unwrap(); - }); - let inner = state.lock().unwrap(); - assert!(inner.x11_cleanup.is_none()); - assert!(inner.lease.is_none()); - assert_eq!( - inner.restored.as_ref().unwrap().restart_result(), - Err("Recording restart was cancelled by Stop".into()) - ); - } - - #[cfg(target_os = "linux")] - #[test] - fn x11_studio_old_cleanup_completion_cannot_finish_a_retry() { - let state = x11_studio_restoring_fixture(); - let mut inner = state.lock().unwrap(); - let first = inner.reserve_x11_studio_cleanup(1, Ok(())).unwrap(); - assert!(inner.reserve_x11_studio_cleanup(1, Ok(())).is_none()); - assert!(!inner.complete_x11_studio_restoration(first, Err("unregister failed".into()))); - assert_eq!(inner.begin_x11_studio_restore_retry(true), Some(1)); - let retry = inner.reserve_x11_studio_cleanup(1, Ok(())).unwrap(); - assert_ne!(first.sequence, retry.sequence); - assert!(!inner.complete_x11_studio_restoration(first, Ok(()))); - assert!(inner.x11_cleanup == Some(retry)); - assert!(inner.lease.as_ref().unwrap().registered_shortcut); - assert!(inner.complete_x11_studio_restoration(retry, Ok(()))); - assert!(!inner.complete_x11_studio_restoration(retry, Ok(()))); - assert_eq!(inner.generation, 2); - } - - #[cfg(target_os = "linux")] - #[tokio::test] - async fn x11_studio_pending_waiter_rejects_restart_after_error_f9_and_retry_success() { - let state = State { - inner: x11_studio_restoring_fixture(), - ..State::default() - }; - let mut waiter = Box::pin(wait_for_restoration(&state, 1)); - assert!(futures::poll!(&mut waiter).is_pending()); - assert!(!finish_x11_studio_restoration( - &state.inner, - 1, - Err("window acknowledgement failed".into()), - || panic!("window failure must retain the Stop shortcut"), - )); - state.changed.notify_waiters(); - { - let mut inner = state.inner.lock().unwrap(); - assert_eq!(inner.begin_x11_studio_restore_retry(true), Some(1)); - assert!(inner.restored.is_none()); - assert!(inner.lease.as_ref().unwrap().stop_requested); - } - assert!(finish_x11_studio_restoration( - &state.inner, - 1, - Ok(()), - || Ok(()) - )); - state.changed.notify_waiters(); - assert_eq!( - waiter.await, - Err("Recording restart was cancelled by Stop".into()) - ); - } - - fn fixture() -> Inner { - Inner { - generation: 1, - stop_notice: None, - stop_notice_sequence: 0, - control_error: None, - restored: None, - #[cfg(target_os = "linux")] - x11_cleanup_sequence: 0, - #[cfg(target_os = "linux")] - x11_cleanup: None, - lease: Some(Lease { - mode: cap_recording::RecordingMode::Studio, - generation: 1, - phase: Phase::AwaitingShortcut, - pressed: false, - stop_requested: false, - registered_shortcut: true, - wayland: false, - stop_route: None, - stop_description: None, - stop_error: None, - lost_stop_routes: [false; 2], - recording_dir: None, - windows: Vec::new(), - }), - } - } - - #[test] - fn requires_delivered_press_then_release() { - let mut state = fixture(); - assert!(state.shortcut(false)); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::AwaitingShortcut); - assert!(state.shortcut(true)); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::AwaitingShortcut); - assert!(state.shortcut(false)); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::Starting); - assert!(!state.lease.as_ref().unwrap().stop_requested); - assert!(state.shortcut(true)); - assert!(state.lease.as_ref().unwrap().stop_requested); - } - - #[test] - fn delayed_reveal_cannot_cross_generation() { - let mut state = fixture(); - assert!(state.may_reveal(1, "main")); - state.generation = 2; - state.lease = None; - assert!(!state.may_reveal(1, "main")); - assert!(!state.may_reveal(1, "camera")); - assert!(state.may_reveal(2, "main")); - } - - #[test] - fn wayland_requires_delivered_current_stop_input() { - let mut state = fixture(); - let lease = state.lease.as_mut().unwrap(); - lease.wayland = true; - assert!(!lease.accept_stop_input(None, true)); - assert!(!lease.accept_stop_input(Some((2, StopRoute::Tray)), true)); - assert!(!lease.accept_stop_input(Some((1, StopRoute::Portal)), false)); - assert_eq!(lease.phase, Phase::AwaitingShortcut); - assert!(lease.accept_stop_input(Some((1, StopRoute::Portal)), true)); - assert!(state.shortcut(true)); - assert!( - !state - .lease - .as_mut() - .unwrap() - .accept_stop_input(Some((1, StopRoute::Tray)), false) - ); - assert!( - state - .lease - .as_mut() - .unwrap() - .accept_stop_input(Some((1, StopRoute::Portal)), false) - ); - assert!(state.shortcut(false)); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::Starting); - } - - #[test] - fn wayland_tray_activation_starts_and_next_activation_stops() { - let mut state = fixture(); - state.lease.as_mut().unwrap().wayland = true; - for pressed in [true, false] { - assert!( - state - .lease - .as_mut() - .unwrap() - .accept_stop_input(Some((1, StopRoute::Tray)), pressed) - ); - assert!(state.shortcut(pressed)); - } - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::Starting); - state.lease.as_mut().unwrap().phase = Phase::Recording; - assert!( - state - .lease - .as_mut() - .unwrap() - .accept_stop_input(Some((1, StopRoute::Tray)), true) - ); - assert!(state.shortcut(true)); - assert!(state.lease.as_ref().unwrap().stop_requested); - } - - #[test] - fn wayland_loss_cannot_start_or_silently_switch_control() { - let mut state = fixture(); - let lease = state.lease.as_mut().unwrap(); - lease.wayland = true; - assert!(!lease.lose_stop_route(StopRoute::Portal)); - assert!(!lease.stop_requested); - assert!(!lease.accept_stop_input(Some((1, StopRoute::Portal)), true)); - assert!(lease.accept_stop_input(Some((1, StopRoute::Tray)), true)); - lease.phase = Phase::Recording; - assert!(lease.lose_stop_route(StopRoute::Tray)); - assert!(lease.stop_requested); - assert!(!lease.accept_stop_input(Some((1, StopRoute::Tray)), true)); - } - - #[test] - fn wayland_both_missing_controls_cancel_visible_preflight() { - let mut state = fixture(); - let lease = state.lease.as_mut().unwrap(); - lease.wayland = true; - assert!(!lease.lose_stop_route(StopRoute::Tray)); - assert!(!lease.lose_stop_route(StopRoute::Portal)); - assert!(lease.stop_requested); - assert_eq!(lease.phase, Phase::AwaitingShortcut); - } - - #[test] - fn wayland_control_loss_during_start_is_retained_until_cleanup() { - let mut state = fixture(); - let lease = state.lease.as_mut().unwrap(); - lease.wayland = true; - lease.stop_route = Some(StopRoute::Tray); - lease.phase = Phase::Starting; - assert!(!lease.lose_stop_route(StopRoute::Tray)); - assert!(lease.stop_requested); - assert_eq!(lease.phase, Phase::Starting); - } - - #[test] - fn only_acknowledged_pause_allows_main() { - let mut state = fixture(); - for phase in [ - Phase::Starting, - Phase::Recording, - Phase::Pausing, - Phase::Resuming, - Phase::Restarting, - Phase::Stopping, - ] { - state.lease.as_mut().unwrap().phase = phase; - assert!(!state.may_reveal(1, "main")); - assert!(!state.may_reveal(1, "camera")); - assert!(!state.may_reveal(1, "in-progress-recording")); - } - state.lease.as_mut().unwrap().phase = Phase::Paused; - assert!(state.may_reveal(1, "main")); - assert!(!state.may_reveal(1, "camera")); - } - - #[test] - fn instant_uses_capture_backend_without_loosening_studio_environment() { - use cap_recording::RecordingMode; - for strict_x11 in [false, true] { - assert!(capture_environment_is_x11( - RecordingMode::Instant, - strict_x11, - false - )); - assert!(!capture_environment_is_x11( - RecordingMode::Instant, - strict_x11, - true - )); - for uses_wayland_portal in [false, true] { - assert_eq!( - capture_environment_is_x11( - RecordingMode::Studio, - strict_x11, - uses_wayland_portal - ), - strict_x11 - ); - } - } - assert!(!x11_environment(true, false, None)); - assert!(capture_environment_is_x11( - RecordingMode::Instant, - x11_environment(true, false, None), - false - )); - assert!(!x11_environment(true, true, Some("x11"))); - assert!(capture_environment_is_x11( - RecordingMode::Instant, - x11_environment(true, true, Some("x11")), - false - )); - } - - #[test] - fn monitor_visibility_requires_x11_for_both_recording_modes() { - use cap_recording::{RecordingMode, screen_capture::ScreenCaptureTarget}; - let display = ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }; - let area = ScreenCaptureTarget::Area { - screen: "1".parse().unwrap(), - bounds: scap_targets::bounds::LogicalBounds::new( - scap_targets::bounds::LogicalPosition::new(0.0, 0.0), - scap_targets::bounds::LogicalSize::new(100.0, 100.0), - ), - }; - let window = ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }; - for mode in [RecordingMode::Studio, RecordingMode::Instant] { - for target in [&display, &area] { - assert_eq!( - validate_capture_visibility(mode, target, false, false, true), - Ok(true) - ); - assert!(validate_capture_visibility(mode, target, false, false, false).is_err()); - } - for target in [&window, &ScreenCaptureTarget::CameraOnly] { - assert_eq!( - validate_capture_visibility(mode, target, false, false, true), - Ok(false) - ); - assert_eq!( - validate_capture_visibility(mode, target, false, false, false), - Ok(false) - ); - } - } - assert_eq!( - validate_capture_visibility(RecordingMode::Screenshot, &display, false, false, false), - Ok(false) - ); - } - - #[test] - fn window_camera_visibility_is_required_only_for_x11_instant() { - use cap_recording::{RecordingMode, screen_capture::ScreenCaptureTarget}; - let target = ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }; - for mode in [ - RecordingMode::Studio, - RecordingMode::Instant, - RecordingMode::Screenshot, - ] { - for camera in [false, true] { - for wayland in [false, true] { - let required = mode == RecordingMode::Instant && camera && !wayland; - assert_eq!( - validate_capture_visibility(mode, &target, camera, wayland, true), - Ok(required) - ); - let unsupported = - validate_capture_visibility(mode, &target, camera, wayland, false); - if required { - assert!(unsupported.is_err()); - } else { - assert_eq!(unsupported, Ok(false)); - } - } - } - } - } - - #[test] - fn refuses_wayland_and_ambiguous_sessions() { - assert!(x11_environment(true, false, Some("x11"))); - assert!(!x11_environment(true, true, Some("x11"))); - assert!(!x11_environment(true, false, Some("wayland"))); - assert!(!x11_environment(true, false, None)); - assert!(!x11_environment(false, false, Some("x11"))); - } - - #[test] - fn cancel_preflight_never_starts() { - let mut state = fixture(); - assert!(state.queue_stop()); - assert!(state.shortcut(true)); - assert!(state.shortcut(false)); - assert!(state.lease.as_ref().unwrap().stop_requested); - } - - #[test] - fn stop_is_retained_through_start_restart_and_control_transitions() { - for phase in [Phase::Starting, Phase::Restarting, Phase::Pausing] { - let mut state = fixture(); - state.lease.as_mut().unwrap().phase = phase; - assert!(state.queue_stop()); - assert!(state.lease.as_ref().unwrap().stop_requested); - assert_eq!(state.lease.as_ref().unwrap().phase, phase); - } - } - - #[test] - fn active_stop_claims_cleanup_once() { - let mut state = fixture(); - state.lease.as_mut().unwrap().phase = Phase::Recording; - assert!(!state.queue_stop()); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::Stopping); - assert!(state.queue_stop()); - assert!(state.lease.as_ref().unwrap().stop_requested); - } - - #[test] - fn unconfirmed_resume_failure_keeps_main_hidden_and_stop_actionable() { - let mut state = fixture(); - let dir = PathBuf::from("recording-a"); - let lease = state.lease.as_mut().unwrap(); - lease.recording_dir = Some(dir.clone()); - lease.phase = Phase::Resuming; - assert!(state.complete_control( - 1, - &dir, - true, - &ControlOutcome::ActorFailed { - error: "actor transport failed".into(), - paused: false, - } - )); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::ResumeFailed); - assert!(state.shortcut(true)); - assert!(state.lease.as_ref().unwrap().phase.can_stop()); - assert!(!state.may_reveal(1, "main")); - assert!(!state.queue_stop()); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::Stopping); - } - - #[test] - fn old_control_acknowledgement_cannot_publish_into_new_recording() { - let mut state = fixture(); - let lease = state.lease.as_mut().unwrap(); - lease.phase = Phase::Pausing; - lease.recording_dir = Some(PathBuf::from("recording-b")); - lease.generation = 2; - state.generation = 2; - assert!(!state.complete_control( - 1, - std::path::Path::new("recording-a"), - false, - &ControlOutcome::Succeeded - )); - assert!(!state.complete_control( - 2, - std::path::Path::new("recording-a"), - false, - &ControlOutcome::Succeeded - )); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::Pausing); - } - - #[test] - fn target_overlays_stay_hidden_even_during_preflight_and_pause() { - let mut state = fixture(); - for phase in [ - Phase::AwaitingShortcut, - Phase::Recording, - Phase::Paused, - Phase::Restarting, - Phase::Restoring, - ] { - state.lease.as_mut().unwrap().phase = phase; - assert!(!state.may_reveal(1, "target-select-overlay-1")); - } - } - - #[test] - fn restore_preserves_hidden_camera_and_rejects_replaced_native_window() { - for visible in [false, true] { - let saved = SavedWindow { - label: "camera".into(), - native_id: 42, - visible, - }; - assert_eq!(saved.visibility_for(42), Some(visible)); - assert_eq!(saved.visibility_for(43), None); - } - } - - #[test] - fn startup_failure_cannot_own_a_different_pending_recording() { - let mut state = fixture(); - let old = PathBuf::from("failed-start-a"); - let new = PathBuf::from("pending-start-b"); - state.lease.as_mut().unwrap().phase = Phase::Starting; - state.lease.as_mut().unwrap().recording_dir = Some(old.clone()); - assert_eq!(state.owner(&old), Some(1)); - assert_eq!(state.owner(&new), None); - state.lease.as_mut().unwrap().recording_dir = Some(new.clone()); - state.lease.as_mut().unwrap().generation = 2; - state.generation = 2; - assert_eq!(state.owner(&old), None); - assert_eq!(state.owner(&new), Some(2)); - } - - #[tokio::test] - async fn pending_pause_does_not_reveal_main_and_retains_queued_stop_after_ack() { - let state = std::sync::Arc::new(Mutex::new(fixture())); - let dir = PathBuf::from("recording-a"); - { - let mut state = state.lock().unwrap(); - let lease = state.lease.as_mut().unwrap(); - lease.phase = Phase::Pausing; - lease.recording_dir = Some(dir.clone()); - } - let (acknowledge, received) = tokio::sync::oneshot::channel(); - let pending = { - let state = state.clone(); - tokio::spawn(async move { - received.await.unwrap(); - state - .lock() - .unwrap() - .complete_control(1, &dir, false, &ControlOutcome::Succeeded) - }) - }; - { - let mut state = state.lock().unwrap(); - assert!(!state.may_reveal(1, "main")); - assert!(state.queue_stop()); - } - acknowledge.send(()).unwrap(); - assert!(pending.await.unwrap()); - let mut state = state.lock().unwrap(); - assert!(state.lease.as_ref().unwrap().stop_requested); - assert!(!state.queue_stop()); - assert!(!state.may_reveal(1, "main")); - } - - #[tokio::test] - async fn stopped_owner_rejects_delayed_resume_acknowledgement() { - let state = std::sync::Arc::new(Mutex::new(fixture())); - let dir = PathBuf::from("recording-a"); - { - let mut state = state.lock().unwrap(); - let lease = state.lease.as_mut().unwrap(); - lease.phase = Phase::Resuming; - lease.recording_dir = Some(dir.clone()); - } - let (acknowledge, received) = tokio::sync::oneshot::channel(); - let pending = { - let state = state.clone(); - tokio::spawn(async move { - received.await.unwrap(); - state - .lock() - .unwrap() - .complete_control(1, &dir, true, &ControlOutcome::Succeeded) - }) - }; - state.lock().unwrap().lease.as_mut().unwrap().phase = Phase::Restoring; - acknowledge.send(()).unwrap(); - assert!(!pending.await.unwrap()); - assert_eq!( - state.lock().unwrap().lease.as_ref().unwrap().phase, - Phase::Restoring - ); - } - - struct RunningControl { - state: std::sync::Arc>, - stages: tokio::sync::mpsc::UnboundedReceiver<&'static str>, - hide: Option>>, - change: Option>>, - paused: Option>, - reveal: Option>, - result: tokio::task::JoinHandle>, - } - - impl RunningControl { - fn start(resume: bool) -> Self { - let state = std::sync::Arc::new(Mutex::new(fixture())); - { - let mut inner = state.lock().unwrap(); - let lease = inner.lease.as_mut().unwrap(); - lease.phase = if resume { - Phase::Resuming - } else { - Phase::Pausing - }; - lease.recording_dir = Some(PathBuf::from("recording-a")); - } - let (stages, observed) = tokio::sync::mpsc::unbounded_channel(); - let (hide, hidden) = tokio::sync::oneshot::channel(); - let (change, changed) = tokio::sync::oneshot::channel(); - let (paused, acknowledged) = tokio::sync::oneshot::channel(); - let (reveal, mapping) = tokio::sync::oneshot::channel(); - let control_state = state.clone(); - let result = tokio::spawn(async move { - let dir = PathBuf::from("recording-a"); - ControlOperation { - resume, - generation: 1, - dir: &dir, - hide: async { - stages.send("hide").unwrap(); - hidden.await.unwrap() - }, - change: async { - stages.send("actor").unwrap(); - changed.await.unwrap() - }, - paused: async { - stages.send("confirm-paused").unwrap(); - acknowledged.await.unwrap() - }, - restore: async { - stages.send("queue-main").unwrap(); - mapping.await.unwrap(); - if control_state - .lock() - .unwrap() - .may_restore_paused_main(1, &dir) - { - stages.send("show-main").unwrap(); - Ok(()) - } else { - stages.send("suppress-main").unwrap(); - Err("Recording changed before mapping".into()) - } - }, - stop: async { - stages.send("stop").unwrap(); - assert!(!control_state.lock().unwrap().queue_stop()); - Ok(()) - }, - notify: || stages.send("notify").unwrap(), - } - .run(&control_state) - .await - }); - Self { - state, - stages: observed, - hide: Some(hide), - change: Some(change), - paused: Some(paused), - reveal: Some(reveal), - result, - } - } - - async fn dispatch_stop( - &self, - ) -> ( - tokio::sync::oneshot::Sender<()>, - tokio::task::JoinHandle<()>, - ) { - { - let mut inner = self.state.lock().unwrap(); - assert!(inner.shortcut(true)); - assert!(inner.lease.as_ref().unwrap().phase.can_stop()); - assert!(!inner.queue_stop()); - } - let (entered, delivered) = tokio::sync::oneshot::channel(); - let (acknowledge, joined) = tokio::sync::oneshot::channel(); - let state = self.state.clone(); - let stop = tokio::spawn(async move { - entered.send(()).unwrap(); - joined.await.unwrap(); - let mut inner = state.lock().unwrap(); - if inner.owner(std::path::Path::new("recording-a")) == Some(1) - && inner.lease.as_ref().unwrap().phase == Phase::Stopping - { - inner.lease.as_mut().unwrap().phase = Phase::Restoring; - } - }); - tokio::time::timeout(Duration::from_secs(1), delivered) - .await - .unwrap() - .unwrap(); - (acknowledge, stop) - } - - async fn expect(&mut self, stage: &'static str) { - assert_eq!( - tokio::time::timeout(Duration::from_secs(1), self.stages.recv()) - .await - .unwrap(), - Some(stage) - ); - } - - async fn finish(self) -> Result<(), String> { - tokio::time::timeout(Duration::from_secs(1), self.result) - .await - .unwrap() - .unwrap() - } - } - - #[tokio::test] - async fn resume_hide_failure_restores_main_without_calling_actor() { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide - .take() - .unwrap() - .send(Err("partial unmap failed".into())) - .unwrap(); - run.expect("notify").await; - run.expect("queue-main").await; - { - let inner = run.state.lock().unwrap(); - assert_eq!(inner.lease.as_ref().unwrap().phase, Phase::Paused); - assert!(!inner.may_reveal(1, "camera")); - assert!(!inner.may_reveal(1, "in-progress-recording")); - assert!(inner.lease.as_ref().unwrap().registered_shortcut); - } - run.reveal.take().unwrap().send(()).unwrap(); - run.expect("show-main").await; - assert_eq!(run.finish().await.unwrap_err(), "partial unmap failed"); - } - - #[tokio::test] - async fn actor_resume_error_waits_for_quiescent_pause_ack_before_mapping() { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide.take().unwrap().send(Ok(())).unwrap(); - run.expect("actor").await; - run.change - .take() - .unwrap() - .send(Err("segment setup failed".into())) - .unwrap(); - run.expect("confirm-paused").await; - assert!(!run.state.lock().unwrap().may_reveal(1, "main")); - run.paused.take().unwrap().send(true).unwrap(); - run.expect("notify").await; - run.expect("queue-main").await; - run.reveal.take().unwrap().send(()).unwrap(); - run.expect("show-main").await; - assert_eq!(run.finish().await.unwrap_err(), "segment setup failed"); - } - - #[tokio::test] - async fn unknown_resume_error_does_not_map_and_stop_remains_executable() { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide.take().unwrap().send(Ok(())).unwrap(); - run.expect("actor").await; - run.change - .take() - .unwrap() - .send(Err("transport failed".into())) - .unwrap(); - run.expect("confirm-paused").await; - run.paused.take().unwrap().send(false).unwrap(); - run.expect("notify").await; - let state = run.state.clone(); - assert!( - run.finish() - .await - .unwrap_err() - .contains("pause could not be confirmed") - ); - let mut state = state.lock().unwrap(); - assert_eq!(state.lease.as_ref().unwrap().phase, Phase::ResumeFailed); - assert!(state.shortcut(true)); - assert!(state.lease.as_ref().unwrap().phase.can_stop()); - assert!(!state.may_reveal(1, "main")); - assert!(!state.queue_stop()); - } - - #[tokio::test] - async fn stop_during_hide_executes_before_resume_and_rejects_late_hide() { - for hidden in [Ok(()), Err("hide failed".into())] { - let mut run = RunningControl::start(true); - run.expect("hide").await; - let (ack, stopped) = run.dispatch_stop().await; - assert!(!run.result.is_finished()); - ack.send(()).unwrap(); - stopped.await.unwrap(); - run.hide.take().unwrap().send(hidden).unwrap(); - assert!(run.finish().await.unwrap_err().contains("changed during")); - } - } - - #[tokio::test] - async fn stop_during_pause_confirmation_executes_without_waiting_for_it() { - for paused in [true, false] { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide.take().unwrap().send(Ok(())).unwrap(); - run.expect("actor").await; - run.change - .take() - .unwrap() - .send(Err("setup failed".into())) - .unwrap(); - run.expect("confirm-paused").await; - let (ack, stopped) = run.dispatch_stop().await; - assert!(!run.result.is_finished()); - ack.send(()).unwrap(); - stopped.await.unwrap(); - run.paused.take().unwrap().send(paused).unwrap(); - assert!(run.finish().await.unwrap_err().contains("changed during")); - } - } - - #[tokio::test] - async fn pending_resume_does_not_block_stop_and_late_result_cannot_publish() { - for resumed in [Ok(()), Err("cancelled setup".into())] { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide.take().unwrap().send(Ok(())).unwrap(); - run.expect("actor").await; - let (ack, stopped) = run.dispatch_stop().await; - assert!(!run.result.is_finished()); - assert_eq!( - run.state.lock().unwrap().lease.as_ref().unwrap().phase, - Phase::Stopping - ); - ack.send(()).unwrap(); - stopped.await.unwrap(); - assert!(!run.result.is_finished()); - let failed = resumed.is_err(); - run.change.take().unwrap().send(resumed).unwrap(); - if failed { - run.expect("confirm-paused").await; - run.paused.take().unwrap().send(true).unwrap(); - } - let state = run.state.clone(); - assert!(run.finish().await.unwrap_err().contains("changed during")); - let inner = state.lock().unwrap(); - assert_eq!(inner.lease.as_ref().unwrap().phase, Phase::Restoring); - assert!(inner.snapshot().error.is_none()); - } - } - - #[tokio::test] - async fn delayed_pause_confirmation_cannot_publish_to_new_owner() { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide.take().unwrap().send(Ok(())).unwrap(); - run.expect("actor").await; - run.change - .take() - .unwrap() - .send(Err("setup failed".into())) - .unwrap(); - run.expect("confirm-paused").await; - { - let mut inner = run.state.lock().unwrap(); - inner.generation = 2; - let lease = inner.lease.as_mut().unwrap(); - lease.generation = 2; - lease.recording_dir = Some(PathBuf::from("recording-b")); - } - run.paused.take().unwrap().send(true).unwrap(); - let state = run.state.clone(); - assert!(run.finish().await.unwrap_err().contains("changed during")); - assert_eq!( - state.lock().unwrap().lease.as_ref().unwrap().phase, - Phase::Resuming - ); - } - - #[tokio::test] - async fn delayed_main_mapping_rechecks_owner_phase_and_stop() { - for mutation in 0..4 { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.hide - .take() - .unwrap() - .send(Err("hide failed".into())) - .unwrap(); - run.expect("notify").await; - run.expect("queue-main").await; - { - let mut inner = run.state.lock().unwrap(); - match mutation { - 0 => { - inner.generation = 2; - inner.lease.as_mut().unwrap().generation = 2; - } - 1 => { - inner.lease.as_mut().unwrap().recording_dir = - Some(PathBuf::from("recording-b")) - } - 2 => inner.lease.as_mut().unwrap().phase = Phase::Resuming, - _ => inner.lease.as_mut().unwrap().stop_requested = true, - } - } - run.reveal.take().unwrap().send(()).unwrap(); - run.expect("suppress-main").await; - assert!( - run.finish() - .await - .unwrap_err() - .contains("Could not restore controls") - ); - } - } - - #[tokio::test] - async fn ordinary_pause_and_successful_resume_never_use_failure_restore() { - for resume in [false, true] { - let mut run = RunningControl::start(resume); - if resume { - run.expect("hide").await; - run.hide.take().unwrap().send(Ok(())).unwrap(); - } - run.expect("actor").await; - run.change.take().unwrap().send(Ok(())).unwrap(); - run.expect("notify").await; - let state = run.state.clone(); - assert!(run.finish().await.is_ok()); - assert_eq!( - state.lock().unwrap().lease.as_ref().unwrap().phase, - if resume { - Phase::Recording - } else { - Phase::Paused - } - ); - } - } - - #[tokio::test] - async fn stale_hide_completion_never_invokes_actor_or_restores_main() { - for success in [false, true] { - let mut run = RunningControl::start(true); - run.expect("hide").await; - run.state.lock().unwrap().lease.as_mut().unwrap().phase = Phase::Restoring; - run.hide - .take() - .unwrap() - .send(if success { - Ok(()) - } else { - Err("hide failed".into()) - }) - .unwrap(); - assert!(run.finish().await.unwrap_err().contains("changed during")); - } - } - - #[tokio::test] - async fn failed_pause_does_not_run_resume_probe_or_restore() { - let mut run = RunningControl::start(false); - run.expect("actor").await; - run.change - .take() - .unwrap() - .send(Err("pause failed".into())) - .unwrap(); - run.expect("notify").await; - let state = run.state.clone(); - assert_eq!(run.finish().await.unwrap_err(), "pause failed"); - assert_eq!( - state.lock().unwrap().lease.as_ref().unwrap().phase, - Phase::Recording - ); - assert!(!state.lock().unwrap().may_reveal(1, "main")); - } - - #[test] - fn control_error_survives_stop_and_release_but_not_a_new_owner() { - let mut inner = fixture(); - let dir = PathBuf::from("recording-a"); - inner.lease.as_mut().unwrap().recording_dir = Some(dir.clone()); - inner.lease.as_mut().unwrap().phase = Phase::Resuming; - assert!(inner.complete_control( - 1, - &dir, - true, - &ControlOutcome::ActorFailed { - error: "capture setup failed".into(), - paused: false - } - )); - assert_eq!( - inner.snapshot().error.as_deref(), - Some("capture setup failed") - ); - assert!(!inner.queue_stop()); - assert_eq!( - inner.snapshot().error.as_deref(), - Some("capture setup failed") - ); - inner.lease = None; - assert_eq!( - inner.snapshot().error.as_deref(), - Some("capture setup failed") - ); - inner.generation = 2; - assert!(inner.snapshot().error.is_none()); - } -} diff --git a/apps/desktop/src-tauri/src/cli.rs b/apps/desktop/src-tauri/src/cli.rs deleted file mode 100644 index d258d170dd0..00000000000 --- a/apps/desktop/src-tauri/src/cli.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub use cap_cli_install::CliInstallStatus; - -#[tauri::command] -#[specta::specta] -pub fn get_cli_install_status() -> Result { - cap_cli_install::status() -} - -#[tauri::command] -#[specta::specta] -pub fn install_cli() -> Result { - cap_cli_install::install() -} - -#[tauri::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-tauri/src/clip_thumbnails.rs deleted file mode 100644 index 65860a5ad13..00000000000 --- a/apps/desktop/src-tauri/src/clip_thumbnails.rs +++ /dev/null @@ -1,455 +0,0 @@ -use std::{path::Path, sync::LazyLock}; - -use cap_project::{RecordingMetaInner, StudioRecordingMeta}; -use tokio::sync::Semaphore; - -use crate::editor_window::WindowEditorInstance; - -const THUMB_MAX_WIDTH: u32 = 240; -const THUMB_JPEG_QUALITY: u8 = 70; -const SEEK_DECODE_PACKET_LIMIT: usize = 4096; -const SEEK_DECODE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2); - -static THUMBNAIL_SEMAPHORE: LazyLock = LazyLock::new(|| Semaphore::new(4)); - -#[tauri::command] -#[specta::specta] -#[tracing::instrument(skip(editor_instance))] -pub async fn get_clip_thumbnail( - editor_instance: WindowEditorInstance, - recording_segment: u32, - time: f64, -) -> Result { - let project_path = editor_instance.project_path.clone(); - let meta = editor_instance.meta(); - - let RecordingMetaInner::Studio(studio) = &meta.inner else { - return Err("Clip thumbnails are only available for studio recordings".to_string()); - }; - - let display_path = match studio.as_ref() { - StudioRecordingMeta::SingleSegment { segment } => meta.path(&segment.display.path), - StudioRecordingMeta::MultipleSegments { inner } => { - let segment = inner - .segments - .get(recording_segment as usize) - .ok_or_else(|| format!("Recording segment {recording_segment} not found"))?; - meta.path(&segment.display.path) - } - }; - - let time = time.max(0.0); - let cache_path = project_path - .join("thumbnails") - .join("clips-v2") - .join(format!( - "seg{recording_segment}_{}.jpg", - (time * 1000.0).round() as i64 - )); - - if tokio::fs::try_exists(&cache_path).await.unwrap_or(false) { - return Ok(cache_path.to_string_lossy().into_owned()); - } - - let permit = THUMBNAIL_SEMAPHORE - .acquire() - .await - .map_err(|e| format!("Failed to acquire thumbnail permit: {e}"))?; - - if tokio::fs::try_exists(&cache_path).await.unwrap_or(false) { - return Ok(cache_path.to_string_lossy().into_owned()); - } - - let output = cache_path.clone(); - tokio::task::spawn_blocking(move || { - let _permit = permit; - decode_clip_thumbnail(&display_path, time, &output) - }) - .await - .map_err(|e| format!("Thumbnail task failed: {e}"))??; - - Ok(cache_path.to_string_lossy().into_owned()) -} - -fn decode_clip_thumbnail(input: &Path, time: f64, output: &Path) -> Result<(), String> { - decode_clip_thumbnail_with_budget( - input, - time, - output, - SEEK_DECODE_PACKET_LIMIT, - SEEK_DECODE_TIMEOUT, - ) -} - -fn decode_clip_thumbnail_with_budget( - input: &Path, - time: f64, - output: &Path, - packet_limit: usize, - timeout: std::time::Duration, -) -> Result<(), String> { - use ffmpeg::rescale::{Rescale, TIME_BASE}; - - let mut ictx = - ffmpeg::format::input(input).map_err(|e| format!("Failed to open video: {e}"))?; - - let stream = ictx - .streams() - .best(ffmpeg::media::Type::Video) - .ok_or("No video stream found")?; - let stream_index = stream.index(); - let stream_time_base = stream.time_base(); - let stream_start = match stream.start_time() { - ffmpeg::ffi::AV_NOPTS_VALUE => 0, - timestamp => timestamp, - }; - let target_timestamp = ((time * 1_000_000.0) as i64) - .rescale((1, 1_000_000), stream_time_base) - .saturating_add(stream_start); - - let mut decoder = ffmpeg::codec::context::Context::from_parameters(stream.parameters()) - .map_err(|e| e.to_string())? - .decoder() - .video() - .map_err(|e| e.to_string())?; - - let src_width = decoder.width(); - let src_height = decoder.height(); - if src_width == 0 || src_height == 0 { - return Err("Invalid video dimensions".to_string()); - } - - let scale = (THUMB_MAX_WIDTH as f32 / src_width as f32).min(1.0); - let target_width = ((src_width as f32 * scale).round() as u32).max(1); - let target_height = ((src_height as f32 * scale).round() as u32).max(1); - - let mut scaler = ffmpeg::software::scaling::context::Context::get( - decoder.format(), - src_width, - src_height, - ffmpeg::format::Pixel::RGB24, - target_width, - target_height, - ffmpeg::software::scaling::flag::Flags::BILINEAR, - ) - .map_err(|e| e.to_string())?; - - if time > 0.0 { - let position_us = (time * 1_000_000.0) as i64; - let seek_target = target_timestamp.rescale(stream_time_base, TIME_BASE); - decoder.flush(); - ictx.seek(seek_target, ..seek_target) - .map_err(|e| format!("Failed to seek to {position_us}us: {e}"))?; - } - - let mut frame = ffmpeg::frame::Video::empty(); - let mut decoded = ffmpeg::frame::Video::empty(); - let mut got_frame = false; - let mut reached_target = false; - let mut decoder_finished = false; - let mut packets_tried = 0usize; - let decode_started = std::time::Instant::now(); - - 'outer: for (packet_stream, packet) in ictx.packets() { - if decode_started.elapsed() >= timeout { - return Err("Thumbnail decode time budget exhausted".to_string()); - } - if packet_stream.index() != stream_index { - continue; - } - - packets_tried += 1; - - if decoder.send_packet(&packet).is_err() { - if packets_tried >= packet_limit { - return Err("Thumbnail decode packet budget exhausted".to_string()); - } - continue; - } - - loop { - if decode_started.elapsed() >= timeout { - return Err("Thumbnail decode time budget exhausted".to_string()); - } - match decoder.receive_frame(&mut decoded) { - Ok(()) => { - std::mem::swap(&mut frame, &mut decoded); - got_frame = true; - if thumbnail_frame_reaches_target(&frame, target_timestamp) { - reached_target = true; - break 'outer; - } - } - Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::ffi::EAGAIN => break, - Err(ffmpeg::Error::Eof) => { - decoder_finished = true; - break 'outer; - } - Err(e) => { - if packets_tried >= packet_limit { - return Err(format!("Failed to decode frame: {e}")); - } - break; - } - } - } - - if packets_tried >= packet_limit { - return Err("Thumbnail decode packet budget exhausted".to_string()); - } - } - - if !reached_target && !decoder_finished { - decoder - .send_eof() - .map_err(|e| format!("Failed to flush decoder: {e}"))?; - loop { - if decode_started.elapsed() >= timeout { - return Err("Thumbnail decode time budget exhausted".to_string()); - } - match decoder.receive_frame(&mut decoded) { - Ok(()) => { - std::mem::swap(&mut frame, &mut decoded); - got_frame = true; - if thumbnail_frame_reaches_target(&frame, target_timestamp) { - break; - } - } - Err(ffmpeg::Error::Eof) => break, - Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::ffi::EAGAIN => break, - Err(e) => return Err(format!("Failed to flush decoder: {e}")), - } - } - } - - if !got_frame { - return Err("No decodable frames found".to_string()); - } - - let mut rgb_frame = ffmpeg::frame::Video::empty(); - scaler - .run(&frame, &mut rgb_frame) - .map_err(|e| e.to_string())?; - - let width = rgb_frame.width() as usize; - let height = rgb_frame.height() as usize; - let src_stride = rgb_frame.stride(0); - let dst_stride = width * 3; - if src_stride < dst_stride { - return Err(format!( - "Unexpected RGB stride: src_stride={src_stride}, expected >= {dst_stride}" - )); - } - let mut img_buffer = vec![0u8; height * dst_stride]; - for y in 0..height { - let src_slice = &rgb_frame.data(0)[y * src_stride..y * src_stride + dst_stride]; - img_buffer[y * dst_stride..(y + 1) * dst_stride].copy_from_slice(src_slice); - } - - use image::ImageEncoder; - - let mut jpeg_bytes = Vec::new(); - image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_bytes, THUMB_JPEG_QUALITY) - .write_image( - &img_buffer, - width as u32, - height as u32, - image::ExtendedColorType::Rgb8, - ) - .map_err(|e| format!("Failed to encode thumbnail: {e}"))?; - - persist_clip_thumbnail(output, &jpeg_bytes) -} - -fn thumbnail_frame_reaches_target(frame: &ffmpeg::frame::Video, target_timestamp: i64) -> bool { - frame - .timestamp() - .or_else(|| frame.pts()) - .is_none_or(|timestamp| timestamp >= target_timestamp) -} - -fn persist_clip_thumbnail(output: &Path, jpeg_bytes: &[u8]) -> Result<(), String> { - use std::io::Write; - - let parent = output.parent().unwrap_or_else(|| Path::new(".")); - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create thumbnail directory: {e}"))?; - - let mut staged = tempfile::NamedTempFile::new_in(parent) - .map_err(|e| format!("Failed to stage thumbnail: {e}"))?; - staged - .write_all(jpeg_bytes) - .map_err(|e| format!("Failed to write thumbnail: {e}"))?; - staged - .persist(output) - .map_err(|e| format!("Failed to persist thumbnail: {e}"))?; - - Ok(()) -} - -#[cfg(test)] -mod persistence_tests { - use super::*; - use std::sync::{Arc, Barrier}; - - #[test] - fn concurrent_identical_thumbnails_are_all_persisted() { - let root = tempfile::tempdir().unwrap(); - let output = root.path().join("clips").join("segment.jpg"); - let barrier = Arc::new(Barrier::new(4)); - let pixels = [35; 24 * 24 * 3]; - let mut jpeg = Vec::new(); - image::codecs::jpeg::JpegEncoder::new(&mut jpeg) - .encode(&pixels, 24, 24, image::ExtendedColorType::Rgb8) - .unwrap(); - - std::thread::scope(|scope| { - let tasks = (0..4) - .map(|_| { - let output = &output; - let jpeg = &jpeg; - let barrier = barrier.clone(); - scope.spawn(move || { - barrier.wait(); - persist_clip_thumbnail(output, jpeg) - }) - }) - .collect::>(); - - for task in tasks { - task.join().unwrap().unwrap(); - } - }); - - assert_eq!(std::fs::read(&output).unwrap(), jpeg); - assert_eq!(image::open(&output).unwrap().width(), 24); - assert_eq!( - std::fs::read_dir(output.parent().unwrap()).unwrap().count(), - 1 - ); - } - - #[test] - fn replacing_cached_thumbnail_preserves_contents() { - let root = tempfile::tempdir().unwrap(); - let output = root.path().join("segment.jpg"); - persist_clip_thumbnail(&output, b"old thumbnail").unwrap(); - persist_clip_thumbnail(&output, b"replacement thumbnail").unwrap(); - assert_eq!(std::fs::read(&output).unwrap(), b"replacement thumbnail"); - assert_eq!(std::fs::read_dir(root.path()).unwrap().count(), 1); - } -} - -#[cfg(test)] -mod decoding_tests { - use super::*; - use std::{ - path::PathBuf, - sync::{Arc, Barrier}, - }; - - fn fixture() -> (tempfile::TempDir, PathBuf) { - fixture_from_bytes(include_bytes!("../test-data/clip-thumbnail-gop.mp4")) - } - - fn fixture_from_bytes(bytes: &[u8]) -> (tempfile::TempDir, PathBuf) { - ffmpeg::init().unwrap(); - let root = tempfile::tempdir().unwrap(); - let input = root.path().join("gop.mp4"); - std::fs::write(&input, bytes).unwrap(); - (root, input) - } - - #[test] - fn split_thumbnails_use_requested_time_inside_keyframe_interval() { - let (root, input) = fixture(); - for (time, expected_channel) in [(0.0, 0), (1.5, 1), (2.5, 2), (3.0, 2)] { - let output = root.path().join(format!("{time}.jpg")); - decode_clip_thumbnail(&input, time, &output).unwrap(); - let image = image::open(&output).unwrap().to_rgb8(); - let pixel = image.get_pixel(80, 45).0; - assert!( - pixel[expected_channel] > 100, - "requested {time}s, got {pixel:?}" - ); - for (channel, value) in pixel.iter().enumerate() { - if channel != expected_channel { - assert!(*value < 20, "requested {time}s, got {pixel:?}"); - } - } - } - } - - #[test] - fn simultaneous_decodes_of_one_clip_all_produce_valid_thumbnail() { - let (root, input) = fixture(); - let output = root.path().join("same-clip.jpg"); - let barrier = Arc::new(Barrier::new(4)); - - std::thread::scope(|scope| { - let tasks = (0..4) - .map(|_| { - let input = &input; - let output = &output; - let barrier = barrier.clone(); - scope.spawn(move || { - barrier.wait(); - decode_clip_thumbnail(input, 1.5, output) - }) - }) - .collect::>(); - for task in tasks { - task.join().unwrap().unwrap(); - } - }); - - let image = image::open(output).unwrap().to_rgb8(); - let pixel = image.get_pixel(80, 45).0; - assert!(pixel[1] > 100 && pixel[0] < 20 && pixel[2] < 20); - } - - #[test] - fn long_gop_thumbnails_reach_requested_time_and_preserve_eof_fallback() { - let (root, input) = - fixture_from_bytes(include_bytes!("../test-data/clip-thumbnail-long-gop.mp4")); - for time in [8.5, 10.0, 11.0] { - let output = root.path().join(format!("{time}.jpg")); - decode_clip_thumbnail(&input, time, &output).unwrap(); - let pixel = image::open(&output).unwrap().to_rgb8().get_pixel(80, 45).0; - assert!( - pixel[2] > 100 && pixel[1] < 20 && pixel[0] < 20, - "requested {time}s, got {pixel:?}" - ); - } - } - - #[test] - fn exhausted_decode_budget_does_not_write_partial_thumbnail() { - let (root, input) = - fixture_from_bytes(include_bytes!("../test-data/clip-thumbnail-long-gop.mp4")); - let output = root.path().join("thumbnail.jpg"); - for (packet_limit, timeout, expected_error) in [ - (240, std::time::Duration::from_secs(2), "packet budget"), - ( - SEEK_DECODE_PACKET_LIMIT, - std::time::Duration::ZERO, - "time budget", - ), - ] { - let result = - decode_clip_thumbnail_with_budget(&input, 8.5, &output, packet_limit, timeout); - assert!(result.unwrap_err().contains(expected_error)); - assert!(!output.exists()); - } - } - - #[test] - fn frames_without_timestamps_preserve_first_frame_fallback() { - let mut frame = ffmpeg::frame::Video::empty(); - assert!(thumbnail_frame_reaches_target(&frame, 100)); - frame.set_pts(Some(99)); - assert!(!thumbnail_frame_reaches_target(&frame, 100)); - frame.set_pts(Some(100)); - assert!(thumbnail_frame_reaches_target(&frame, 100)); - } -} diff --git a/apps/desktop/src-tauri/src/crash_sentinel.rs b/apps/desktop/src-tauri/src/crash_sentinel.rs deleted file mode 100644 index 8a49480bcb4..00000000000 --- a/apps/desktop/src-tauri/src/crash_sentinel.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Detects sessions that ended without a clean shutdown. -//! -//! A whole class of failures never reaches Sentry's panic hook because the process -//! is killed without panicking: a macOS WindowServer/GPU wedge that soft-restarts the -//! login session, an OOM kill, a force-quit, or power loss. We catch those after the -//! fact: every launch arms a sentinel file with this session's context, and a clean -//! shutdown disarms it. If a launch finds a sentinel left over from a previous run, -//! that run died unexpectedly — report it to Sentry with the captured context. - -use std::{ - path::{Path, PathBuf}, - sync::Mutex, -}; - -use serde::{Deserialize, Serialize}; - -const SENTINEL_FILE: &str = "session-active.json"; - -#[derive(Serialize, Deserialize, Clone)] -struct SessionRecord { - pid: u32, - started_at: String, - app_version: String, - os: String, - arch: String, - #[serde(default)] - liquid_glass: String, - /// True while GPU adapter/device initialisation is in flight. If the process - /// dies inside that window, the next launch treats the death as a - /// graphics-init crash (the only case that justifies software-rendering - /// recovery); a death at any other time says nothing about the GPU. - #[serde(default)] - gpu_init_phase: bool, - /// True when this session was already running in software-graphics recovery - /// mode, so a follow-up crash doesn't chain into recovery forever. - #[serde(default)] - graphics_recovery: bool, - /// True while any camera-background-blur processor is alive (ONNX/DirectML - /// session + dedicated GPU work), and for a linger period after the last - /// one is released — deferred driver/D3D12 faults have been observed - /// killing the process tens of seconds after blur teardown. Native crashes - /// in that stack never reach a panic handler; if the process dies inside - /// this window the next launch attributes the death to blur and disables it. - #[serde(default)] - blur_active: bool, - /// True when this session already ran with blur disabled by crash recovery, - /// for diagnostics on the next-launch report. - #[serde(default)] - blur_recovery: bool, -} - -struct ActiveSession { - path: PathBuf, - record: SessionRecord, - gpu_init_depth: u32, - blur_session_depth: u32, - /// Bumped on every blur enter/exit so a scheduled linger-disarm can tell - /// whether blur activity happened after it was scheduled. - blur_generation: u64, -} - -static SESSION: Mutex> = Mutex::new(None); - -/// How the previous session died, as reconstructed from its surviving sentinel. -/// Only the Windows graphics-recovery path consumes the fields today. -#[derive(Clone, Copy)] -#[cfg_attr(not(target_os = "windows"), allow(dead_code))] -pub struct UnexpectedTermination { - /// The process died while GPU adapter/device initialisation was in flight. - pub during_gpu_init: bool, - /// The session was already running in software-graphics recovery mode. - pub in_graphics_recovery: bool, - /// The process died while a camera-background-blur processor was alive. - pub blur_active: bool, -} - -/// Arm the sentinel for this session and, if a previous session's sentinel survived, -/// report that unexpected termination to Sentry. Call once at startup, after Sentry -/// is initialised. Returns details of the previous session's unexpected termination, -/// or `None` if the previous session shut down cleanly. -pub fn init(logs_dir: Option<&Path>, app_version: &str) -> Option { - let os = format!( - "{} {}", - tauri_plugin_os::platform(), - tauri_plugin_os::version() - ); - let arch = tauri_plugin_os::arch().to_string(); - sentry::configure_scope(|scope| { - scope.set_tag("os.full", &os); - scope.set_tag("arch", &arch); - scope.set_tag("app.version", app_version); - }); - - let path = logs_dir?.join(SENTINEL_FILE); - let mut previous_termination = None; - - if let Ok(contents) = std::fs::read_to_string(&path) { - match serde_json::from_str::(&contents) { - // A live process still owns this sentinel — almost certainly a - // single-instance double launch (this init runs before the - // single-instance plugin loads), not a crash. Leave it untouched and - // don't arm a competing sentinel for this about-to-exit instance. - Ok(prev) if process_is_running(prev.pid) => return None, - Ok(prev) => { - report_unexpected_termination(&prev); - previous_termination = Some(UnexpectedTermination { - during_gpu_init: prev.gpu_init_phase, - in_graphics_recovery: prev.graphics_recovery, - blur_active: prev.blur_active, - }); - } - Err(error) => { - tracing::warn!(%error, "Found unreadable crash sentinel from previous session") - } - } - let _ = std::fs::remove_file(&path); - } - - let record = SessionRecord { - pid: std::process::id(), - started_at: chrono::Utc::now().to_rfc3339(), - app_version: app_version.to_string(), - os: os.clone(), - arch: arch.clone(), - liquid_glass: "unknown".to_string(), - gpu_init_phase: false, - graphics_recovery: false, - blur_active: false, - blur_recovery: false, - }; - - write_record(&path, &record); - - *SESSION.lock().unwrap() = Some(ActiveSession { - path, - record, - gpu_init_depth: 0, - blur_session_depth: 0, - blur_generation: 0, - }); - - previous_termination -} - -/// Arm the GPU-init marker while adapter/device initialisation runs. If the process -/// dies inside this window the next launch sees `during_gpu_init` and can engage -/// graphics recovery. Nestable; the marker persists until the last exit call. -pub fn enter_gpu_init_phase() { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - { - session.gpu_init_depth += 1; - if !session.record.gpu_init_phase { - session.record.gpu_init_phase = true; - write_record(&session.path, &session.record); - } - } -} - -/// Disarm the GPU-init marker once initialisation finished (successfully or not — -/// a survivable failure is not a crash). -pub fn exit_gpu_init_phase() { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - { - session.gpu_init_depth = session.gpu_init_depth.saturating_sub(1); - if session.gpu_init_depth == 0 && session.record.gpu_init_phase { - session.record.gpu_init_phase = false; - write_record(&session.path, &session.record); - } - } -} - -/// How long `blur_active` stays armed after the last blur processor is -/// released. Deferred D3D12/DML/driver faults have been observed killing the -/// process up to ~50s after blur teardown finished (the fault surfaces -/// asynchronously); disarming immediately would miss exactly those deaths and -/// leave the crash loop unbroken. -const BLUR_DISARM_LINGER: std::time::Duration = std::time::Duration::from_secs(120); - -/// Arm the blur marker while a camera-background-blur processor is alive. If the -/// process dies inside this window the next launch sees `blur_active` and can -/// disable blur. Nestable (editor render + camera preview can overlap); the -/// marker persists until a linger period after the last exit call. -pub fn enter_blur_session() { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - { - session.blur_session_depth += 1; - session.blur_generation = session.blur_generation.wrapping_add(1); - if !session.record.blur_active { - session.record.blur_active = true; - write_record(&session.path, &session.record); - } - } -} - -/// Schedule the blur marker to disarm once the linger window passes with no new -/// blur session. The generation check makes a stale disarm from an earlier -/// exit a no-op if blur was re-armed in the meantime. -pub fn exit_blur_session() { - let scheduled_generation = { - let Ok(mut guard) = SESSION.lock() else { - return; - }; - let Some(session) = guard.as_mut() else { - return; - }; - session.blur_session_depth = session.blur_session_depth.saturating_sub(1); - session.blur_generation = session.blur_generation.wrapping_add(1); - if session.blur_session_depth != 0 || !session.record.blur_active { - return; - } - session.blur_generation - }; - - std::thread::spawn(move || { - std::thread::sleep(BLUR_DISARM_LINGER); - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - && session.blur_generation == scheduled_generation - && session.blur_session_depth == 0 - && session.record.blur_active - { - session.record.blur_active = false; - write_record(&session.path, &session.record); - } - }); -} - -/// Record that this session is running with camera background blur disabled by -/// crash recovery, for diagnostics on a follow-up unexpected-termination report. -pub fn mark_blur_recovery() { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - && !session.record.blur_recovery - { - session.record.blur_recovery = true; - write_record(&session.path, &session.record); - } - - sentry::configure_scope(|scope| { - scope.set_tag("camera_blur_recovery", "true"); - }); -} - -/// Record that this session is running in software-graphics recovery mode, so an -/// unexpected termination of *this* session doesn't chain into recovery again. -#[cfg_attr(not(target_os = "windows"), allow(dead_code))] -pub fn mark_graphics_recovery() { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - && !session.record.graphics_recovery - { - session.record.graphics_recovery = true; - write_record(&session.path, &session.record); - } - - sentry::configure_scope(|scope| { - scope.set_tag("graphics_recovery", "true"); - }); -} - -/// Record the result of the macOS Liquid Glass material attempt so that, if this -/// session dies unexpectedly, the next-launch report names which cohort it was in. -/// `outcome` is one of "applied", "fallback", or "unsupported". -#[cfg(target_os = "macos")] -pub fn set_liquid_glass_outcome(outcome: &str) { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.as_mut() - && session.record.liquid_glass != outcome - { - session.record.liquid_glass = outcome.to_string(); - write_record(&session.path, &session.record); - } - - sentry::configure_scope(|scope| { - scope.set_tag("macos_liquid_glass", outcome); - }); -} - -/// Disarm the sentinel after a fully graceful shutdown. If this is never reached (the -/// process was killed, or shutdown hung past the watchdog) the surviving sentinel is -/// what the next launch reports. -pub fn mark_clean_exit() { - if let Ok(mut guard) = SESSION.lock() - && let Some(session) = guard.take() - { - let _ = std::fs::remove_file(&session.path); - } -} - -fn report_unexpected_termination(prev: &SessionRecord) { - tracing::error!( - prev_pid = prev.pid, - prev_started_at = %prev.started_at, - prev_os = %prev.os, - prev_liquid_glass = %prev.liquid_glass, - prev_gpu_init_phase = prev.gpu_init_phase, - prev_graphics_recovery = prev.graphics_recovery, - prev_blur_active = prev.blur_active, - prev_blur_recovery = prev.blur_recovery, - "Previous Cap session terminated without a clean shutdown" - ); - - sentry::with_scope( - |scope| { - scope.set_tag("unexpected_termination", "true"); - scope.set_tag("prev.os", &prev.os); - scope.set_tag("prev.arch", &prev.arch); - scope.set_tag("prev.app_version", &prev.app_version); - scope.set_tag("prev.macos_liquid_glass", &prev.liquid_glass); - scope.set_tag("prev.gpu_init_phase", prev.gpu_init_phase.to_string()); - scope.set_tag("prev.graphics_recovery", prev.graphics_recovery.to_string()); - scope.set_tag("prev.blur_active", prev.blur_active.to_string()); - scope.set_tag("prev.blur_recovery", prev.blur_recovery.to_string()); - scope.set_extra("prev.pid", prev.pid.into()); - scope.set_extra("prev.started_at", prev.started_at.clone().into()); - }, - || { - sentry::capture_message( - "Cap session terminated unexpectedly (no clean shutdown)", - sentry::Level::Error, - ); - }, - ); -} - -fn process_is_running(pid: u32) -> bool { - let pid = sysinfo::Pid::from_u32(pid); - let mut system = sysinfo::System::new(); - system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true); - system.process(pid).is_some() -} - -fn write_record(path: &Path, record: &SessionRecord) { - match serde_json::to_string(record) { - Ok(json) => { - if let Err(error) = std::fs::write(path, json) { - tracing::warn!(%error, "Failed to write crash sentinel"); - } - } - Err(error) => tracing::warn!(%error, "Failed to serialize crash sentinel"), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sentinel_is_disabled_without_a_directory_and_works_when_available() { - assert!(init(None, "test").is_none()); - assert!(SESSION.lock().unwrap().is_none()); - enter_gpu_init_phase(); - exit_gpu_init_phase(); - enter_blur_session(); - exit_blur_session(); - mark_clean_exit(); - assert!(SESSION.lock().unwrap().is_none()); - - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join(SENTINEL_FILE); - assert!(init(Some(directory.path()), "test").is_none()); - let read_record = || { - serde_json::from_str::(&std::fs::read_to_string(&path).unwrap()).unwrap() - }; - assert_eq!(read_record().app_version, "test"); - enter_gpu_init_phase(); - assert!(read_record().gpu_init_phase); - exit_gpu_init_phase(); - assert!(!read_record().gpu_init_phase); - mark_clean_exit(); - assert!(!path.exists()); - assert!(SESSION.lock().unwrap().is_none()); - } -} diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs deleted file mode 100644 index 5372d58785f..00000000000 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ /dev/null @@ -1,558 +0,0 @@ -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; - -#[cfg(debug_assertions)] -use crate::camera::CameraPreviewState; - -#[cfg(debug_assertions)] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct CaptureArea { - screen: String, - x: f64, - y: f64, - width: f64, - height: f64, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum CaptureMode { - Screen(String), - Window(String), - #[cfg(debug_assertions)] - Area(Box), - #[cfg(debug_assertions)] - CameraOnly, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum DeepLinkAction { - StartRecording { - capture_mode: CaptureMode, - camera: Option, - mic_label: Option, - capture_system_audio: bool, - mode: RecordingMode, - }, - StopRecording, - #[cfg(debug_assertions)] - PauseRecording, - #[cfg(debug_assertions)] - ResumeRecording, - #[cfg(debug_assertions)] - OpenCamera { - camera: DeviceOrModelID, - }, - #[cfg(debug_assertions)] - SetCameraPreviewState { - state: CameraPreviewState, - }, - #[cfg(debug_assertions)] - SetRecordingInputs { - camera: Option, - mic_label: Option, - }, - #[cfg(debug_assertions)] - SetEditorPlayback { - project_path: PathBuf, - playing: bool, - }, - #[cfg(debug_assertions)] - CloseEditor { - project_path: PathBuf, - }, - OpenEditor { - project_path: PathBuf, - }, - OpenSettings { - page: Option, - }, -} - -pub struct DeepLinkActionExecutor { - tx: std::sync::mpsc::Sender, -} - -impl DeepLinkActionExecutor { - pub fn new(app: &AppHandle) -> Self { - let (tx, rx) = std::sync::mpsc::channel::(); - let app_handle = app.clone(); - let runtime = tokio::runtime::Handle::current(); - - trace!("Starting deep link action executor"); - let thread = std::thread::Builder::new().name("deep-link-action-executor".to_string()); - // Debug recording futures overflow the default thread stack; match Tauri's worker stack. - #[cfg(debug_assertions)] - let thread = thread.stack_size(16 * 1024 * 1024); - let thread_result = thread.spawn(move || { - trace!("Deep link action executor started"); - for action in rx { - trace!(?action, "Executing deep link action"); - if let Err(err) = runtime.block_on(action.execute(&app_handle)) { - eprintln!("Failed to handle deep link action: {err}"); - } - } - }); - - if let Err(err) = thread_result { - eprintln!("Failed to start deep link action executor: {err}"); - } - - Self { tx } - } - - fn dispatch( - &self, - action: DeepLinkAction, - ) -> Result<(), std::sync::mpsc::SendError> { - self.tx.send(action) - } -} - -pub fn handle(app_handle: &AppHandle, urls: Vec) { - trace!("Handling deep actions for: {:?}", &urls); - - let actions: Vec<_> = urls - .into_iter() - .filter(|url| !url.as_str().is_empty()) - .filter_map(|url| { - DeepLinkAction::try_from(&url) - .map_err(|e| match e { - ActionParseFromUrlError::ParseFailed(msg) => { - eprintln!("Failed to parse deep link \"{}\": {}", &url, msg) - } - ActionParseFromUrlError::Invalid => { - eprintln!("Invalid deep link format \"{}\"", &url) - } - // Likely login action, not handled here. - ActionParseFromUrlError::NotAction => {} - }) - .ok() - }) - .collect(); - - trace!(action_count = actions.len(), "Parsed deep link actions"); - - if actions.is_empty() { - return; - } - - let Some(executor) = app_handle.try_state::() else { - eprintln!("Deep link action executor unavailable"); - return; - }; - - for action in actions { - trace!(?action, "Queueing deep link action"); - if let Err(err) = executor.dispatch(action) { - eprintln!("Failed to queue deep link action: {err}"); - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ActionParseFromUrlError { - ParseFailed(String), - Invalid, - NotAction, -} - -impl TryFrom<&Url> for DeepLinkAction { - type Error = ActionParseFromUrlError; - - fn try_from(url: &Url) -> Result { - #[cfg(target_os = "macos")] - if url.scheme() == "file" { - return url - .to_file_path() - .map(|project_path| Self::OpenEditor { project_path }) - .map_err(|_| ActionParseFromUrlError::Invalid); - } - - match url.domain() { - Some("action") => {} - Some(_) => return Err(ActionParseFromUrlError::NotAction), - None => return Err(ActionParseFromUrlError::Invalid), - } - - let params = url - .query_pairs() - .collect::>(); - let json_value = params - .get("value") - .ok_or(ActionParseFromUrlError::Invalid)?; - let action: Self = serde_json::from_str(json_value) - .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?; - Ok(action) - } -} - -impl DeepLinkAction { - pub async fn execute(self, app: &AppHandle) -> Result<(), String> { - match self { - DeepLinkAction::StartRecording { - capture_mode, - camera, - mic_label, - capture_system_audio, - mode, - } => { - let state = app.state::>(); - - crate::set_camera_input(app.clone(), state.clone(), camera, None).await?; - crate::set_mic_input(app.clone(), state.clone(), mic_label).await?; - - let capture_target: ScreenCaptureTarget = match capture_mode { - CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() - .into_iter() - .find(|(s, _)| s.name == name) - .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", &name))?, - CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() - .into_iter() - .find(|(w, _)| w.name == name) - .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) - .ok_or(format!("No window with name \"{}\"", &name))?, - #[cfg(debug_assertions)] - CaptureMode::Area(area) => { - if area.width <= 0.0 || area.height <= 0.0 { - return Err("Area width and height must be positive".to_string()); - } - let screen = cap_recording::screen_capture::list_displays() - .into_iter() - .find(|(display, _)| display.name == area.screen) - .map(|(display, _)| display.id) - .ok_or(format!("No screen with name \"{}\"", &area.screen))?; - ScreenCaptureTarget::Area { - screen, - bounds: scap_targets::bounds::LogicalBounds::new( - scap_targets::bounds::LogicalPosition::new(area.x, area.y), - scap_targets::bounds::LogicalSize::new(area.width, area.height), - ), - } - } - #[cfg(debug_assertions)] - CaptureMode::CameraOnly => ScreenCaptureTarget::CameraOnly, - }; - - let inputs = StartRecordingInputs { - mode, - capture_target, - capture_system_audio, - organization_id: None, - }; - - crate::recording::start_recording(app.clone(), state, inputs) - .await - .map(|_| ()) - } - DeepLinkAction::StopRecording => { - crate::recording::stop_recording(app.clone(), app.state()).await - } - #[cfg(debug_assertions)] - DeepLinkAction::PauseRecording => { - crate::recording::pause_recording(app.clone(), app.state()).await - } - #[cfg(debug_assertions)] - DeepLinkAction::ResumeRecording => { - crate::recording::resume_recording(app.clone(), app.state()).await - } - #[cfg(debug_assertions)] - DeepLinkAction::OpenCamera { camera } => { - crate::set_camera_input( - app.clone(), - app.state::>(), - Some(camera), - None, - ) - .await?; - - if crate::general_settings::GeneralSettingsStore::native_camera_preview_enabled(app) - { - crate::set_native_camera_preview_enabled( - app.clone(), - app.state::>(), - true, - ) - .await?; - } - - app.emit("instant-mode-harness-camera-opened", ()) - .map_err(|err| err.to_string())?; - for delay_ms in [250, 750, 1500] { - tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; - app.emit("instant-mode-harness-camera-opened", ()) - .map_err(|err| err.to_string())?; - } - - Ok(()) - } - #[cfg(debug_assertions)] - DeepLinkAction::SetCameraPreviewState { state } => { - crate::set_camera_preview_state(app.state(), state).await - } - #[cfg(debug_assertions)] - DeepLinkAction::SetRecordingInputs { camera, mic_label } => { - crate::set_camera_input(app.clone(), app.state(), camera, None).await?; - crate::set_mic_input(app.clone(), app.state(), mic_label).await - } - #[cfg(debug_assertions)] - DeepLinkAction::SetEditorPlayback { - project_path, - playing, - } => { - let window = crate::windows::editor_window_for_path(app, &project_path) - .ok_or("Editor window is not open")?; - window - .emit_to( - tauri::EventTarget::WebviewWindow { - label: window.label().to_string(), - }, - "cap-dev-set-editor-playback", - playing, - ) - .map_err(|error| error.to_string()) - } - #[cfg(debug_assertions)] - DeepLinkAction::CloseEditor { project_path } => { - let window = crate::windows::editor_window_for_path(app, &project_path) - .ok_or("Editor window is not open")?; - window.close().map_err(|error| error.to_string()) - } - DeepLinkAction::OpenEditor { project_path } => { - crate::open_project_from_path(Path::new(&project_path), app.clone()) - } - DeepLinkAction::OpenSettings { page } => { - crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_stop_recording_action_url() { - let url = Url::parse("cap-desktop://action?value=%22stop_recording%22").unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&url), - Ok(DeepLinkAction::StopRecording) - ); - } - - #[cfg(debug_assertions)] - #[test] - fn parses_open_camera_action_url() { - let value = serde_json::json!({ - "open_camera": { - "camera": { "DeviceID": "camera-1" } - } - }) - .to_string(); - let url = Url::parse_with_params("cap-desktop://action", &[("value", value)]).unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&url), - Ok(DeepLinkAction::OpenCamera { - camera: DeviceOrModelID::DeviceID("camera-1".to_string()) - }) - ); - } - - #[cfg(debug_assertions)] - #[test] - fn parses_camera_preview_state_action_url() { - let value = serde_json::json!({ - "set_camera_preview_state": { - "state": { - "size": 400.0, - "shape": "full", - "mirrored": true, - "background_blur": "heavy" - } - } - }) - .to_string(); - let url = Url::parse_with_params("cap-desktop://action", &[("value", value)]).unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&url), - Ok(DeepLinkAction::SetCameraPreviewState { - state: CameraPreviewState { - size: 400.0, - shape: crate::camera::CameraPreviewShape::Full, - mirrored: true, - background_blur: cap_project::BackgroundBlurMode::Heavy, - } - }) - ); - } - - #[cfg(debug_assertions)] - #[test] - fn parses_pause_and_resume_action_urls() { - let pause_url = Url::parse("cap-desktop://action?value=%22pause_recording%22").unwrap(); - let resume_url = Url::parse("cap-desktop://action?value=%22resume_recording%22").unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&pause_url), - Ok(DeepLinkAction::PauseRecording) - ); - assert_eq!( - DeepLinkAction::try_from(&resume_url), - Ok(DeepLinkAction::ResumeRecording) - ); - } - - #[cfg(debug_assertions)] - #[test] - fn parses_area_recording_action_url() { - let value = serde_json::json!({ - "start_recording": { - "capture_mode": { - "area": { - "screen": "Built-in Retina Display", - "x": 10.0, - "y": 20.0, - "width": 800.0, - "height": 600.0 - } - }, - "camera": { "DeviceID": "camera-1" }, - "mic_label": "microphone-1", - "capture_system_audio": false, - "mode": "instant" - } - }) - .to_string(); - let url = Url::parse_with_params("cap-desktop://action", &[("value", value)]).unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&url), - Ok(DeepLinkAction::StartRecording { - capture_mode: CaptureMode::Area(Box::new(CaptureArea { - screen: "Built-in Retina Display".to_string(), - x: 10.0, - y: 20.0, - width: 800.0, - height: 600.0, - })), - camera: Some(DeviceOrModelID::DeviceID("camera-1".to_string())), - mic_label: Some("microphone-1".to_string()), - capture_system_audio: false, - mode: RecordingMode::Instant, - }) - ); - } - - #[cfg(debug_assertions)] - #[test] - fn parses_camera_only_recording_action_url() { - let value = serde_json::json!({ - "start_recording": { - "capture_mode": "camera_only", - "camera": { "DeviceID": "camera-1" }, - "mic_label": "microphone-1", - "capture_system_audio": false, - "mode": "studio" - } - }) - .to_string(); - let url = Url::parse_with_params("cap-desktop://action", &[("value", value)]).unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&url), - Ok(DeepLinkAction::StartRecording { - capture_mode: CaptureMode::CameraOnly, - camera: Some(DeviceOrModelID::DeviceID("camera-1".to_string())), - mic_label: Some("microphone-1".to_string()), - capture_system_audio: false, - mode: RecordingMode::Studio, - }) - ); - } - - #[test] - fn parses_start_recording_action_url() { - let url = Url::parse( - "cap-desktop://action?value=%7B%22start_recording%22%3A%7B%22capture_mode%22%3A%7B%22screen%22%3A%22Odyssey%20G93SC%22%7D%2C%22camera%22%3Anull%2C%22mic_label%22%3A%22Shure%20MV7%2B%22%2C%22capture_system_audio%22%3Atrue%2C%22mode%22%3A%22studio%22%7D%7D", - ) - .unwrap(); - - let Ok(DeepLinkAction::StartRecording { - capture_mode, - camera, - mic_label, - capture_system_audio, - mode, - }) = DeepLinkAction::try_from(&url) - else { - panic!("expected start recording action"); - }; - - assert_eq!( - capture_mode, - CaptureMode::Screen("Odyssey G93SC".to_string()) - ); - assert_eq!(camera, None); - assert_eq!(mic_label.as_deref(), Some("Shure MV7+")); - assert!(capture_system_audio); - assert_eq!(mode, RecordingMode::Studio); - } - - #[test] - fn parses_start_recording_action_with_camera_device_id() { - let value = serde_json::json!({ - "start_recording": { - "capture_mode": { "screen": "Odyssey G93SC" }, - "camera": { "DeviceID": "camera-1" }, - "mic_label": "Shure MV7+", - "capture_system_audio": true, - "mode": "studio" - } - }) - .to_string(); - let url = Url::parse_with_params("cap-desktop://action", &[("value", value)]).unwrap(); - - let Ok(DeepLinkAction::StartRecording { - camera, - mic_label, - capture_system_audio, - .. - }) = DeepLinkAction::try_from(&url) - else { - panic!("expected start recording action"); - }; - - assert_eq!( - camera, - Some(DeviceOrModelID::DeviceID("camera-1".to_string())) - ); - assert_eq!(mic_label.as_deref(), Some("Shure MV7+")); - assert!(capture_system_audio); - } - - #[test] - fn rejects_non_action_host() { - let url = Url::parse("cap-desktop://login?value=%22stop_recording%22").unwrap(); - - assert_eq!( - DeepLinkAction::try_from(&url), - Err(ActionParseFromUrlError::NotAction) - ); - } -} diff --git a/apps/desktop/src-tauri/src/diagnostics.rs b/apps/desktop/src-tauri/src/diagnostics.rs deleted file mode 100644 index 102c735e423..00000000000 --- a/apps/desktop/src-tauri/src/diagnostics.rs +++ /dev/null @@ -1,787 +0,0 @@ -use crate::{ArcLock, general_settings::GeneralSettingsStore, permissions}; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::{ - path::{Path, PathBuf}, - sync::atomic::{AtomicBool, Ordering}, -}; -use tauri::{AppHandle, Manager}; -use tauri_specta::Event; -use tokio::io::AsyncBufReadExt; -use tracing::{error, info, warn}; - -/// Reports are written next to the logs so support can ask for them by name. -const REPORTS_DIR_NAME: &str = "diagnostics"; -const REPORTS_TO_KEEP: usize = 5; -const SYNC_TEST_STDERR_TAIL_LIMIT: usize = 40; -/// The full report always lands on disk; only the copy handed back to the -/// webview is trimmed, so a pathological run can't wedge the settings window. -const MAX_INLINE_REPORT_BYTES: usize = 2 * 1024 * 1024; -const MIN_SYNC_TEST_SECS: u32 = 14; -const MAX_SYNC_TEST_SECS: u32 = 120; - -/// Only one diagnostic can run at a time: the sync test takes over the screen -/// and the audio device, so a second run would measure the first one. -static DIAGNOSTIC_RUNNING: AtomicBool = AtomicBool::new(false); - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct DiagnosticOptions { - pub include_sync_test: bool, - /// `studio`, `instant` or `both`. - pub mode: String, - pub duration_secs: Option, - pub include_microphone: bool, - pub mic_name: Option, - pub skip_export: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct DiagnosticRunResult { - pub report_path: String, - pub verdict: Option, - pub summary: Option, - pub sync_test_error: Option, - pub report_json: String, -} - -/// `phase` is `sync-test`, `collecting` or `done`; `stage`/`mode` are only set -/// for `sync-test` and carry the CLI's stage names verbatim. -#[derive(Debug, Clone, Serialize, Type, Event)] -#[serde(rename_all = "camelCase")] -pub struct DiagnosticProgress { - pub phase: String, - pub stage: Option, - pub mode: Option, -} - -impl DiagnosticProgress { - fn phase(phase: &str) -> Self { - Self { - phase: phase.to_string(), - stage: None, - mode: None, - } - } -} - -/// The subset of `cap selftest av-sync --progress-json` NDJSON we act on. -/// Lines with an unknown `type` (or no `type` at all) are ignored so the CLI -/// can add message kinds without breaking older apps. -#[derive(Debug, Deserialize, PartialEq)] -#[serde(tag = "type")] -enum SyncTestMessage { - Stage { - stage: String, - #[serde(default)] - mode: Option, - }, - Report { - report: serde_json::Value, - }, - Error { - error: String, - }, -} - -fn parse_sync_test_line(line: &str) -> Option { - let line = line.trim(); - if line.is_empty() { - return None; - } - serde_json::from_str(line).ok() -} - -fn normalize_mode(mode: &str) -> Result<&'static str, String> { - match mode { - "studio" => Ok("studio"), - "instant" => Ok("instant"), - "both" => Ok("both"), - other => Err(format!("Unknown sync test mode '{other}'")), - } -} - -struct RunGuard; - -impl RunGuard { - fn acquire() -> Result { - DIAGNOSTIC_RUNNING - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .map(|_| Self) - .map_err(|_| "A diagnostic is already running".to_string()) - } -} - -impl Drop for RunGuard { - fn drop(&mut self) { - DIAGNOSTIC_RUNNING.store(false, Ordering::Release); - } -} - -/// The argv for one sync-test run, without the binary itself. -/// -/// `--discard-recordings` is not optional here: the app surfaces the report and -/// never the recording, and the CLI otherwise keeps the recorded project for -/// every non-pass verdict -- including the common `inconclusive` one -- with -/// nothing on this side to reveal or delete it. -fn sync_test_args(options: &DiagnosticOptions, mode: &str) -> Vec { - let mut args = vec![ - "selftest".to_string(), - "av-sync".to_string(), - "--mode".to_string(), - mode.to_string(), - "--progress-json".to_string(), - "--discard-recordings".to_string(), - ]; - - if let Some(duration) = options.duration_secs { - args.push("--duration".to_string()); - args.push( - duration - .clamp(MIN_SYNC_TEST_SECS, MAX_SYNC_TEST_SECS) - .to_string(), - ); - } - - if options.include_microphone { - args.push("--mic".to_string()); - if let Some(name) = options.mic_name.as_deref().filter(|n| !n.is_empty()) { - args.push("--mic-name".to_string()); - args.push(name.to_string()); - } - } - - if options.skip_export { - args.push("--skip-export".to_string()); - } - - args -} - -async fn collect_sync_test_stderr_tail(stderr: tokio::process::ChildStderr) -> Vec { - let mut lines = tokio::io::BufReader::new(stderr).lines(); - let mut tail = Vec::new(); - - loop { - match lines.next_line().await { - Ok(Some(line)) => { - if cfg!(debug_assertions) { - info!(line = %line, "Sync test stderr"); - } - tail.push(line); - if tail.len() > SYNC_TEST_STDERR_TAIL_LIMIT { - tail.remove(0); - } - } - Ok(None) => break, - Err(e) => { - tail.push(format!("failed reading stderr: {e}")); - break; - } - } - } - - tail -} - -/// Drives `cap selftest av-sync` in the exporter sidecar. The verdict lives in -/// the `Report` message, never in the exit code: the CLI exits non-zero for -/// fail/inconclusive runs that still produced a full report. -async fn run_sync_test( - app: &AppHandle, - options: &DiagnosticOptions, -) -> Result { - let mode = normalize_mode(&options.mode)?; - // The sync test lives in the `cap` CLI, shipped as the `cap-exporter` - // sidecar; a dev build without the sidecar has no way to run it. - let bin_path = crate::export::resolve_exporter_binary() - .map_err(|e| format!("Sync test helper unavailable: {e}"))?; - - let mut command = tokio::process::Command::new(&bin_path); - command - .args(sync_test_args(options, mode)) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - // Every early return below (a stdout read error, most of all) would - // otherwise orphan a process that owns the screen with a fullscreen - // flashing window and is midway through a recording. - .kill_on_drop(true); - crate::export::configure_exporter_command(&mut command); - - let mut child = command - .spawn() - .map_err(|e| format!("Failed to start sync test '{}': {e}", bin_path.display()))?; - - let stdout = child - .stdout - .take() - .ok_or_else(|| "Sync test stdout was not captured".to_string())?; - let stderr = child - .stderr - .take() - .ok_or_else(|| "Sync test stderr was not captured".to_string())?; - let stderr_task = tokio::spawn(collect_sync_test_stderr_tail(stderr)); - - let mut report = None; - let mut reported_error = None; - let mut stdout_lines = tokio::io::BufReader::new(stdout).lines(); - - while let Some(line) = stdout_lines - .next_line() - .await - .map_err(|e| format!("Failed reading sync test stdout: {e}"))? - { - match parse_sync_test_line(&line) { - Some(SyncTestMessage::Stage { stage, mode }) => { - DiagnosticProgress { - phase: "sync-test".to_string(), - stage: Some(stage), - mode, - } - .emit(app) - .ok(); - } - Some(SyncTestMessage::Report { report: value }) => report = Some(value), - Some(SyncTestMessage::Error { error }) => reported_error = Some(error), - None => {} - } - } - - let status = child - .wait() - .await - .map_err(|e| format!("Failed waiting for sync test: {e}"))?; - let stderr_tail = stderr_task.await.unwrap_or_default(); - - if let Some(report) = report { - return Ok(report); - } - - if let Some(error) = reported_error { - return Err(error); - } - - let tail = stderr_tail.join("\n"); - Err(format!( - "Sync test exited with status {status} without producing a report.\n{tail}" - )) -} - -/// Strip credentials from a configured server URL without hiding the host, -/// which is the part support needs to know about. -fn redact_url_credentials(url: &str) -> String { - let Some((scheme, rest)) = url.split_once("://") else { - return url.to_string(); - }; - let authority_end = rest.find('/').unwrap_or(rest.len()); - let (authority, path) = rest.split_at(authority_end); - match authority.rsplit_once('@') { - Some((_, host)) => format!("{scheme}://***@{host}{path}"), - None => url.to_string(), - } -} - -fn settings_snapshot(settings: &GeneralSettingsStore) -> serde_json::Value { - use cap_recording::diagnostics::redact_home_paths; - - serde_json::json!({ - "maxFps": settings.max_fps, - "instantModeMaxResolution": settings.instant_mode_max_resolution, - "studioRecordingQuality": settings.studio_recording_quality, - "customCursorCapture": settings.custom_cursor_capture, - "outOfProcessMuxer": settings.out_of_process_muxer, - "crashRecoveryRecording": settings.crash_recovery_recording, - "enableNativeCameraPreview": settings.enable_native_camera_preview, - "recordingsPath": settings - .recordings_path - .as_deref() - .map(redact_home_paths), - "previousRecordingsPaths": settings - .previous_recordings_paths - .iter() - .map(|p| redact_home_paths(p)) - .collect::>(), - "updateChannel": settings.update_channel, - "serverUrl": redact_url_credentials(&settings.server_url), - "instanceId": settings.instance_id, - "enableTelemetry": settings.enable_telemetry, - "editorPreviewQuality": settings.editor_preview_quality, - "captureKeyboardEvents": settings.capture_keyboard_events, - }) -} - -fn permissions_snapshot() -> serde_json::Value { - let check = permissions::do_permissions_check(false); - serde_json::json!({ - "screenRecording": crate::logging::permission_status_str(&check.screen_recording), - "camera": crate::logging::permission_status_str(&check.camera), - "microphone": crate::logging::permission_status_str(&check.microphone), - "accessibility": crate::logging::permission_status_str(&check.accessibility), - }) -} - -fn reports_dir(app: &AppHandle) -> Result { - let dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Failed to get app data dir: {e}"))? - .join(REPORTS_DIR_NAME); - std::fs::create_dir_all(&dir) - .map_err(|e| format!("Failed to create diagnostics folder: {e}"))?; - Ok(dir) -} - -/// Keep the newest `keep` reports. Ties on modification time fall back to the -/// (timestamped) file name so the order is deterministic. -fn prune_reports(dir: &Path, keep: usize) -> Result<(), String> { - let entries = - std::fs::read_dir(dir).map_err(|e| format!("Failed to read diagnostics folder: {e}"))?; - - let mut reports: Vec<(std::time::SystemTime, std::ffi::OsString, PathBuf)> = entries - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - let name = path.file_name()?.to_owned(); - let name_str = name.to_str()?; - if !path.is_file() - || !name_str.starts_with("cap-diagnostic-") - || !name_str.ends_with(".json") - { - return None; - } - let modified = std::fs::metadata(&path).ok()?.modified().ok()?; - Some((modified, name, path)) - }) - .collect(); - - reports.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| b.1.cmp(&a.1))); - - for (_, _, path) in reports.into_iter().skip(keep) { - if let Err(e) = std::fs::remove_file(&path) { - warn!(path = %path.display(), error = %e, "Failed to prune old diagnostic report"); - } - } - - Ok(()) -} - -/// The webview only ever renders a preview, so the two unbounded arrays are the -/// first thing to go when a report is huge. -fn truncate_for_ui(report: &serde_json::Value) -> serde_json::Value { - let mut trimmed = report.clone(); - - if let Some(obj) = trimmed.as_object_mut() { - obj.insert( - "recentRecordings".to_string(), - serde_json::Value::Array(Vec::new()), - ); - - if let Some(sync_test) = obj.get_mut("syncTest").and_then(|v| v.as_object_mut()) { - for key in ["recording", "microphone", "export", "instant"] { - if let Some(measurement) = sync_test.get_mut(key).and_then(|v| v.as_object_mut()) { - measurement.insert("events".to_string(), serde_json::Value::Array(Vec::new())); - } - } - } - - obj.insert( - "truncatedForPreview".to_string(), - serde_json::Value::Bool(true), - ); - } - - trimmed -} - -fn resolve_within(dir: &Path, candidate: &Path) -> Result { - let dir = dir - .canonicalize() - .map_err(|e| format!("Diagnostics folder unavailable: {e}"))?; - let candidate = candidate - .canonicalize() - .map_err(|e| format!("Diagnostic report not found: {e}"))?; - - if !candidate.starts_with(&dir) { - return Err("Diagnostic report is outside the diagnostics folder".to_string()); - } - - Ok(candidate) -} - -fn validate_report_path(app: &AppHandle, report_path: &str) -> Result { - resolve_within(&reports_dir(app)?, Path::new(report_path)) -} - -#[tauri::command] -#[specta::specta] -pub async fn run_diagnostic( - app: AppHandle, - options: DiagnosticOptions, -) -> Result { - if options.include_sync_test { - // Validate before taking the guard so a bad mode is a plain error. - normalize_mode(&options.mode)?; - } - - let _guard = RunGuard::acquire()?; - - { - let app_lock = app.state::>(); - let state = app_lock.read().await; - if matches!( - state.recording_state, - crate::RecordingState::Active(_) | crate::RecordingState::Pending { .. } - ) { - return Err("Stop the current recording before running a diagnostic".to_string()); - } - } - - let mut sync_test = None; - let mut sync_test_error = None; - - if options.include_sync_test { - match run_sync_test(&app, &options).await { - Ok(report) => sync_test = Some(report), - Err(e) => { - // An unrunnable sync test still leaves a useful environment - // report, so the run continues with the reason recorded. - error!(error = %e, "Sync test failed; continuing with environment report"); - sync_test_error = Some(e); - } - } - } - - DiagnosticProgress::phase("collecting").emit(&app).ok(); - - let settings = GeneralSettingsStore::get(&app) - .map_err(|e| warn!("Failed to read general settings for diagnostic: {e}")) - .ok() - .flatten(); - let recordings_dir = GeneralSettingsStore::recordings_dir(&app); - let configured_max_fps = settings.as_ref().map(|s| s.max_fps); - let fragmented_recording = settings - .as_ref() - .map(|s| s.crash_recovery_recording) - .unwrap_or(cap_recording::DEFAULT_CRASH_RECOVERY_RECORDING); - let settings_json = settings.as_ref().map(settings_snapshot); - let app_version = env!("CARGO_PKG_VERSION").to_string(); - let sync_test_error_for_report = sync_test_error.clone(); - - // `collect_report` probes displays and disk mounts; both can block for tens - // of seconds, so it never runs on the async runtime's cooperative threads. - let report = tokio::task::spawn_blocking(move || { - cap_recording::diagnostics::collect_report( - cap_recording::diagnostics::DiagnosticReportArgs { - flavor: "tauri", - app_version: &app_version, - settings: settings_json, - permissions: Some(permissions_snapshot()), - recordings_dir: Some(recordings_dir.as_path()), - configured_max_fps, - fragmented_recording, - sync_test, - sync_test_error: sync_test_error_for_report, - }, - ) - }) - .await - .map_err(|e| format!("Failed to collect diagnostic report: {e}"))?; - - let verdict = report - .sync_test - .as_ref() - .and_then(|v| v.get("verdict")) - .and_then(|v| v.as_str()) - .map(str::to_string); - let summary = report - .sync_test - .as_ref() - .and_then(|v| v.get("summary")) - .and_then(|v| v.as_str()) - .map(str::to_string); - - let report_value = serde_json::to_value(&report) - .map_err(|e| format!("Failed to serialize diagnostic report: {e}"))?; - let report_json = serde_json::to_string_pretty(&report_value) - .map_err(|e| format!("Failed to serialize diagnostic report: {e}"))?; - - let dir = reports_dir(&app)?; - let report_path = dir.join(format!( - "cap-diagnostic-{}.json", - chrono::Local::now().format("%Y%m%d-%H%M%S") - )); - std::fs::write(&report_path, &report_json) - .map_err(|e| format!("Failed to write diagnostic report: {e}"))?; - prune_reports(&dir, REPORTS_TO_KEEP).ok(); - - let report_json = if report_json.len() > MAX_INLINE_REPORT_BYTES { - serde_json::to_string_pretty(&truncate_for_ui(&report_value)).unwrap_or(report_json) - } else { - report_json - }; - - DiagnosticProgress::phase("done").emit(&app).ok(); - - Ok(DiagnosticRunResult { - report_path: report_path.display().to_string(), - verdict, - summary, - sync_test_error, - report_json, - }) -} - -#[tauri::command] -#[specta::specta] -pub async fn upload_diagnostic_report(app: AppHandle, report_path: String) -> Result<(), String> { - let path = validate_report_path(&app, &report_path)?; - let report = std::fs::read_to_string(&path) - .map_err(|e| format!("Failed to read diagnostic report: {e}"))?; - - crate::logging::upload_log_file_inner(&app, Some(report)).await -} - -#[tauri::command] -#[specta::specta] -pub async fn reveal_diagnostic_report(app: AppHandle, report_path: String) -> Result<(), String> { - let path = validate_report_path(&app, &report_path)?; - - crate::reveal_in_dir(app, path) - .await - .map_err(|e| format!("Failed to reveal diagnostic report: {e}")) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn temp_dir() -> PathBuf { - let dir = std::env::temp_dir().join(format!("cap-diagnostics-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - - #[test] - fn parses_stage_lines() { - assert_eq!( - parse_sync_test_line(r#"{"type":"Stage","stage":"recording","mode":"studio"}"#), - Some(SyncTestMessage::Stage { - stage: "recording".to_string(), - mode: Some("studio".to_string()), - }) - ); - assert_eq!( - parse_sync_test_line(r#"{"type":"Stage","stage":"done","mode":null}"#), - Some(SyncTestMessage::Stage { - stage: "done".to_string(), - mode: None, - }) - ); - } - - #[test] - fn parses_report_and_error_lines() { - let report = parse_sync_test_line(r#"{"type":"Report","report":{"verdict":"pass"}}"#); - let Some(SyncTestMessage::Report { report }) = report else { - panic!("expected a report message"); - }; - assert_eq!(report["verdict"], "pass"); - - assert_eq!( - parse_sync_test_line(r#"{"type":"Error","error":"no display"}"#), - Some(SyncTestMessage::Error { - error: "no display".to_string() - }) - ); - } - - #[test] - fn ignores_unknown_and_malformed_lines() { - assert_eq!(parse_sync_test_line(r#"{"type":"Heartbeat","at":1}"#), None); - assert_eq!(parse_sync_test_line("not json at all"), None); - assert_eq!(parse_sync_test_line("{}"), None); - assert_eq!(parse_sync_test_line(" "), None); - // A stage message missing its required field is dropped, not fatal. - assert_eq!(parse_sync_test_line(r#"{"type":"Stage"}"#), None); - } - - fn options(mode: &str) -> DiagnosticOptions { - DiagnosticOptions { - include_sync_test: true, - mode: mode.to_string(), - duration_secs: None, - include_microphone: false, - mic_name: None, - skip_export: false, - } - } - - /// The app never surfaces the recorded project, so it always asks the CLI - /// to discard it -- otherwise every non-pass verdict leaves a full screen - /// recording in temp that nothing here ever cleans up. - #[test] - fn the_sync_test_always_discards_recordings() { - assert_eq!( - sync_test_args(&options("both"), "both"), - [ - "selftest", - "av-sync", - "--mode", - "both", - "--progress-json", - "--discard-recordings", - ] - ); - - let full = DiagnosticOptions { - duration_secs: Some(500), - include_microphone: true, - mic_name: Some("Studio Mic".to_string()), - skip_export: true, - ..options("studio") - }; - assert_eq!( - sync_test_args(&full, "studio"), - [ - "selftest", - "av-sync", - "--mode", - "studio", - "--progress-json", - "--discard-recordings", - // Out-of-range durations are clamped, as they always were. - "--duration", - "120", - "--mic", - "--mic-name", - "Studio Mic", - "--skip-export", - ] - ); - - // An empty mic name is not passed as a flag value. - let empty_name = DiagnosticOptions { - include_microphone: true, - mic_name: Some(String::new()), - ..options("instant") - }; - assert!( - !sync_test_args(&empty_name, "instant") - .iter() - .any(|arg| arg == "--mic-name") - ); - } - - #[test] - fn normalizes_known_modes_only() { - assert_eq!(normalize_mode("both"), Ok("both")); - assert_eq!(normalize_mode("studio"), Ok("studio")); - assert_eq!(normalize_mode("instant"), Ok("instant")); - assert!(normalize_mode("Studio").is_err()); - assert!(normalize_mode("").is_err()); - } - - #[test] - fn resolve_within_accepts_files_in_the_dir() { - let root = temp_dir(); - let dir = root.join("diagnostics"); - std::fs::create_dir_all(&dir).unwrap(); - let file = dir.join("cap-diagnostic-1.json"); - std::fs::write(&file, "{}").unwrap(); - - let resolved = resolve_within(&dir, &file).unwrap(); - assert_eq!(resolved, file.canonicalize().unwrap()); - - std::fs::remove_dir_all(&root).ok(); - } - - #[test] - fn resolve_within_rejects_escapes() { - let root = temp_dir(); - let dir = root.join("diagnostics"); - std::fs::create_dir_all(&dir).unwrap(); - let outside = root.join("secret.json"); - std::fs::write(&outside, "{}").unwrap(); - - // Traversal out of the diagnostics folder. - assert!(resolve_within(&dir, &dir.join("../secret.json")).is_err()); - // An absolute path elsewhere on disk. - assert!(resolve_within(&dir, &outside).is_err()); - // A file that does not exist at all. - assert!(resolve_within(&dir, &dir.join("missing.json")).is_err()); - - std::fs::remove_dir_all(&root).ok(); - } - - #[test] - fn prunes_to_the_newest_reports() { - let dir = temp_dir(); - for i in 0..8 { - std::fs::write( - dir.join(format!("cap-diagnostic-2026010{i}-120000.json")), - "{}", - ) - .unwrap(); - } - // Unrelated files are never touched. - std::fs::write(dir.join("notes.txt"), "keep me").unwrap(); - std::fs::write(dir.join("cap-diagnostic-partial.txt"), "keep me").unwrap(); - - prune_reports(&dir, 5).unwrap(); - - let mut remaining: Vec = std::fs::read_dir(&dir) - .unwrap() - .map(|e| e.unwrap().file_name().to_string_lossy().into_owned()) - .filter(|n| n.starts_with("cap-diagnostic-") && n.ends_with(".json")) - .collect(); - remaining.sort(); - - assert_eq!(remaining.len(), 5); - assert!(remaining.contains(&"cap-diagnostic-20260107-120000.json".to_string())); - assert!(!remaining.contains(&"cap-diagnostic-20260100-120000.json".to_string())); - assert!(dir.join("notes.txt").exists()); - assert!(dir.join("cap-diagnostic-partial.txt").exists()); - - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn redacts_only_url_credentials() { - assert_eq!( - redact_url_credentials("https://cap.so"), - "https://cap.so".to_string() - ); - assert_eq!( - redact_url_credentials("https://user:pass@self.hosted/cap"), - "https://***@self.hosted/cap".to_string() - ); - assert_eq!(redact_url_credentials("not a url"), "not a url".to_string()); - } - - #[test] - fn truncate_for_ui_drops_unbounded_arrays() { - let report = serde_json::json!({ - "recentRecordings": [{ "id": "a" }], - "syncTest": { - "verdict": "pass", - "recording": { "medianOffsetMs": 1.0, "events": [1, 2, 3] }, - "studioError": null, - }, - }); - - let trimmed = truncate_for_ui(&report); - - assert_eq!(trimmed["recentRecordings"].as_array().unwrap().len(), 0); - assert_eq!( - trimmed["syncTest"]["recording"]["events"] - .as_array() - .unwrap() - .len(), - 0 - ); - assert_eq!(trimmed["syncTest"]["verdict"], "pass"); - assert_eq!(trimmed["syncTest"]["recording"]["medianOffsetMs"], 1.0); - assert_eq!(trimmed["truncatedForPreview"], true); - } -} diff --git a/apps/desktop/src-tauri/src/editor_preparing.rs b/apps/desktop/src-tauri/src/editor_preparing.rs deleted file mode 100644 index 74a236e1348..00000000000 --- a/apps/desktop/src-tauri/src/editor_preparing.rs +++ /dev/null @@ -1,1398 +0,0 @@ -use std::{ - collections::HashMap, - io::{self, BufReader, Read}, - panic::AssertUnwindSafe, - path::{Path, PathBuf}, - str::FromStr, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; - -use cap_editor::{ - EditorFrameFormat, PreparingEditorProgress, PreparingPlaybackController, PreparingPlaybackExit, - PreparingPlaybackOptions, PreparingPlaybackSession, PreparingPlaybackSnapshot, - PreparingPlaybackState, PreparingPlaybackStopHandle, PreparingPreviewInput, - PreparingPreviewOptions, PreparingPreviewSegment, -}; -use cap_project::{CursorEvents, ProjectConfiguration}; -use cap_recording::recovery::{ - PreparingSidecarKind, PreparingStudioSources, PreparingStudioState, PreparingVideoTrack, -}; -use cap_rendering::{FrozenRecordedCursorAssets, ManagedSegmentVideoInput, ManagedVideoTrackInput}; -use futures::{ - FutureExt, - future::{BoxFuture, Shared}, -}; -use serde::Serialize; -use specta::Type; -use tauri::{AppHandle, Manager, Window}; -use tauri_specta::Event; -use tokio::sync::{oneshot, watch}; - -use crate::frame_ws::{OwnedWatchFrameWs, WSFrame}; -use crate::{ - FinalizationProject, FinalizingRecordings, - preparing_finalization::FinalizationPreparing, - windows::{CapWindowId, EditorWindowIds}, -}; - -mod audio; - -const MAX_CURSOR_BYTES: u64 = 256 * 1024 * 1024; - -#[derive(Clone, Debug, Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub(crate) struct PreparingEditorSeed { - title: String, - tracks: Vec, -} - -#[derive(Clone, Debug, Serialize, Type, tauri_specta::Event)] -#[serde(rename_all = "camelCase")] -pub(crate) struct PreparingEditorChanged { - request_epoch: u32, - job_id: String, - sequence: u32, - fps: u32, - progress: PreparingEditorProgress, - playback: PreparingPlaybackState, - seed: PreparingEditorSeed, -} - -#[derive(Default)] -struct PlaybackBinding { - latest: Option, - controller: Option, - audio_output: Option>, - exit: Option, - handoff: Option, -} - -#[derive(Clone)] -struct RetainedJoin(Shared>>); - -impl RetainedJoin { - fn new(task: tokio::task::JoinHandle>) -> Self { - let completion = async move { - task.await - .map_err(|error| format!("Preparing cleanup task failed: {error}"))? - } - .boxed() - .shared(); - let retained = completion.clone(); - drop(tokio::spawn(async move { - let _ = retained.await; - })); - Self(completion) - } - - async fn wait(&self) -> Result<(), String> { - self.0.clone().await - } - fn succeeded(&self) -> bool { - self.0.peek().is_some_and(Result::is_ok) - } -} - -struct ConsumerControl { - window_ids: EditorWindowIds, - window_id: u32, - request_epoch: u32, - path: PathBuf, - finalization: FinalizationPreparing, - cancelled: watch::Sender, - accepts_frames: AtomicBool, - playback: Mutex, - publish: Option>, -} - -impl ConsumerControl { - fn cancel(&self) { - if let Some(handoff) = &self.playback.lock().unwrap().handoff { - handoff.cancel(); - } - self.accepts_frames.store(false, Ordering::Release); - self.cancelled.send_replace(true); - } - - fn is_active(&self) -> bool { - !*self.cancelled.borrow() - && self - .window_ids - .ids - .lock() - .unwrap() - .iter() - .any(|(path, id)| *id == self.window_id && path == &self.path) - && (self.finalization.is_pending() || self.finalization.allows_preparing_continuation()) - } - - fn publish_snapshot(&self, snapshot: &PreparingPlaybackSnapshot) { - let update = { - let mut binding = self.playback.lock().unwrap(); - let Some(latest) = binding.latest.as_mut() else { - return; - }; - latest.sequence = latest.sequence.saturating_add(1); - latest.progress = snapshot.progress.clone(); - latest.playback = snapshot.playback; - latest.clone() - }; - if let Some(publish) = &self.publish { - publish(update); - } - } - - fn accepts(&self) -> bool { - self.accepts_frames.load(Ordering::Acquire) && self.is_active() - } - - fn retire_playback(&self) { - let output = { - let mut binding = self.playback.lock().unwrap(); - binding.controller = None; - binding.exit = None; - if let Some(handoff) = binding.handoff.take() { - handoff.cancel(); - } - binding.audio_output.take() - }; - if let Some(output) = output { - output.shutdown(); - } - } -} - -struct Entry { - control: Arc, - joined: RetainedJoin, -} - -#[derive(Default)] -struct ProjectConsumers { - ordinary_loading: bool, - entries: Vec, -} - -#[derive(Default)] -struct Registry { - projects: HashMap, - epochs: HashMap, -} - -#[derive(Clone, Default)] -pub(crate) struct PreparingConsumers(Arc>); - -impl PreparingConsumers { - pub(crate) async fn dispose_all(&self) -> Result<(), String> { - let paths = { - let registry = self.0.lock().unwrap(); - for project in registry.projects.values() { - for entry in &project.entries { - entry.control.cancel(); - } - } - registry.projects.keys().cloned().collect::>() - }; - let mut failure = None; - for path in paths { - if let Err(error) = self.before_ordinary_loading(&path).await { - failure.get_or_insert(error); - } - } - for project in self.0.lock().unwrap().projects.values() { - for entry in &project.entries { - entry.control.retire_playback(); - } - } - failure.map_or(Ok(()), Err) - } - - pub(crate) fn cancel_window(&self, window_id: u32, epoch: Option) { - let registry = self.0.lock().unwrap(); - let mut closed = Vec::new(); - for project in registry.projects.values() { - for entry in &project.entries { - if entry.control.window_id == window_id - && epoch.is_none_or(|epoch| entry.control.request_epoch == epoch) - { - let adopted = epoch.is_some() - && entry - .control - .playback - .lock() - .unwrap() - .handoff - .as_ref() - .is_some_and(|handoff| handoff.committed()); - if adopted { - entry.control.accepts_frames.store(false, Ordering::Release); - entry.control.cancelled.send_replace(true); - } else { - entry.control.cancel(); - } - if epoch.is_none() { - closed.push((entry.control.clone(), entry.joined.clone())); - } - } - } - } - drop(registry); - if !closed.is_empty() { - let registry = self.clone(); - drop(tokio::spawn(async move { - for (control, joined) in closed { - let _ = joined.wait().await; - control.retire_playback(); - } - for project in registry.0.lock().unwrap().projects.values_mut() { - project.entries.retain(|entry| { - entry.control.window_id != window_id || !entry.joined.succeeded() - }); - } - })); - } - } - - fn control_for_window( - &self, - window_id: u32, - epoch: Option, - ) -> Option> { - self.0 - .lock() - .unwrap() - .projects - .values() - .flat_map(|project| &project.entries) - .filter(|entry| { - entry.control.window_id == window_id - && epoch.is_none_or(|epoch| entry.control.request_epoch == epoch) - }) - .max_by_key(|entry| entry.control.request_epoch) - .map(|entry| entry.control.clone()) - } - - pub(crate) fn snapshot_for_window(&self, window_id: u32) -> Option { - self.control_for_window(window_id, None)? - .playback - .lock() - .unwrap() - .latest - .clone() - } - - pub(crate) async fn take_startup( - &self, - path: &Path, - ) -> Result< - ( - Arc, - cap_editor::EditorStartupInputs, - Option, - ), - String, - > { - let latest = { - let registry = self.0.lock().unwrap(); - registry.projects.get(path).and_then(|project| { - project - .entries - .iter() - .filter(|entry| { - entry.joined.succeeded() - || (entry.control.is_active() - && entry.control.finalization.allows_preparing_continuation()) - }) - .max_by_key(|entry| entry.control.request_epoch) - .map(|entry| (entry.control.clone(), entry.joined.clone())) - }) - }; - if let Some((control, joined)) = latest { - let handoff = control.playback.lock().unwrap().handoff.clone(); - let mut completed_audio = None; - let mut continuing = None; - if let Some(handoff) = handoff { - match handoff.take_completed_audio().await { - Ok(audio) => { - completed_audio = Some(audio); - continuing = Some(handoff); - } - Err(_) => { - control.cancel(); - joined.wait().await?; - } - } - } - let mut binding = control.playback.lock().unwrap(); - if completed_audio.is_none() { - completed_audio = binding - .exit - .take() - .and_then(|exit| exit.take_completed_audio()); - } - let output = binding - .audio_output - .take() - .unwrap_or_else(|| Arc::new(cap_editor::AudioOutput::new())); - return Ok(( - output, - cap_editor::EditorStartupInputs { - recordings: None, - completed_audio, - }, - continuing, - )); - } - Ok(( - Arc::new(cap_editor::AudioOutput::new()), - cap_editor::EditorStartupInputs::default(), - None, - )) - } - - pub(crate) async fn before_ordinary_loading(&self, path: &Path) -> Result<(), String> { - let pending = { - let mut registry = self.0.lock().unwrap(); - let project = registry.projects.entry(path.to_path_buf()).or_default(); - project.ordinary_loading = true; - project - .entries - .iter() - .filter_map(|entry| { - if entry.control.is_active() - && entry.control.finalization.allows_preparing_continuation() - && entry.control.playback.lock().unwrap().handoff.is_some() - { - return None; - } - entry.control.cancel(); - Some(entry.joined.clone()) - }) - .collect::>() - }; - let mut failure = None; - for joined in pending { - if let Err(error) = joined.wait().await { - failure.get_or_insert(error); - } - } - failure.map_or(Ok(()), Err) - } -} - -#[tauri::command] -#[specta::specta] -pub(crate) async fn create_preparing_editor_frame( - window: Window, - request_epoch: u32, -) -> Result, String> { - let CapWindowId::Editor { id } = - CapWindowId::from_str(window.label()).map_err(|error| error.to_string())? - else { - return Err("Invalid editor window".into()); - }; - let app = window.app_handle(); - let window_ids = EditorWindowIds::get(app); - let path = window_ids - .ids - .lock() - .unwrap() - .iter() - .find(|(_, current)| *current == id) - .map(|(path, _)| path.clone()) - .ok_or("Editor window is closed")?; - let project = FinalizationProject::observe(path.clone()).await?; - if path != project.work_path() { - return Ok(None); - } - let Some(finalization) = app - .state::() - .preparing_for_project(&project) - else { - return Ok(None); - }; - let registry = app.state::().inner().clone(); - let (response, result) = oneshot::channel(); - { - let ids = window_ids.ids.lock().unwrap(); - if !ids - .iter() - .any(|(registered, current)| *current == id && registered == &path) - { - return Ok(None); - } - let mut registry = registry.0.lock().unwrap(); - if request_epoch == 0 - || registry - .epochs - .get(&id) - .is_some_and(|epoch| *epoch >= request_epoch) - { - return Ok(None); - } - registry.epochs.insert(id, request_epoch); - let project = registry.projects.entry(path.clone()).or_default(); - if project.ordinary_loading || !finalization.is_pending() { - return Ok(None); - } - project.entries.retain(|entry| !entry.joined.succeeded()); - let previous = project - .entries - .iter() - .map(|entry| { - entry.control.cancel(); - (entry.joined.clone(), entry.control.clone()) - }) - .collect(); - let control = Arc::new(ConsumerControl { - window_ids: window_ids.clone(), - window_id: id, - request_epoch, - path, - finalization, - cancelled: watch::channel(false).0, - accepts_frames: AtomicBool::new(false), - playback: Mutex::default(), - publish: Some(Box::new({ - let window = window.clone(); - move |snapshot| { - let _ = snapshot.emit_to(&window, window.label()); - } - })), - }); - let runner = Runner { - control: control.clone(), - previous, - response: Some(response), - transport: None, - stop: None, - frames: watch::channel(None).0, - }; - let joined = RetainedJoin::new(tokio::spawn(runner.run())); - project.entries.push(Entry { control, joined }); - } - result - .await - .map_err(|error| format!("Preparing frame admission failed: {error}")) -} - -#[tauri::command] -#[specta::specta] -pub(crate) async fn stop_preparing_editor_frame( - window: Window, - request_epoch: u32, -) -> Result<(), String> { - let CapWindowId::Editor { id } = - CapWindowId::from_str(window.label()).map_err(|error| error.to_string())? - else { - return Err("Invalid editor window".into()); - }; - window - .app_handle() - .state::() - .cancel_window(id, Some(request_epoch)); - Ok(()) -} - -fn editor_window_id(window: &Window) -> Result { - match CapWindowId::from_str(window.label()).map_err(|error| error.to_string())? { - CapWindowId::Editor { id } => Ok(id), - _ => Err("Invalid editor window".into()), - } -} - -#[tauri::command] -#[specta::specta] -pub(crate) fn get_preparing_editor_state( - window: Window, - request_epoch: u32, -) -> Result, String> { - let id = editor_window_id(&window)?; - let state = window.state::(); - Ok(state - .control_for_window(id, Some(request_epoch)) - .and_then(|control| control.playback.lock().unwrap().latest.clone())) -} - -fn playback_controller( - window: &Window, - request_epoch: u32, - job_id: &str, -) -> Result { - let id = editor_window_id(window)?; - let control = window - .state::() - .control_for_window(id, Some(request_epoch)) - .ok_or("Preparing editor is closed")?; - { - let binding = control.playback.lock().unwrap(); - if binding - .latest - .as_ref() - .is_none_or(|latest| latest.job_id != job_id) - { - return Err("Preparing editor identity changed".into()); - } - if binding - .handoff - .as_ref() - .is_some_and(|handoff| handoff.committed()) - { - return Err("Preparing playback has been adopted".into()); - } - } - if !control.is_active() { - return Err("Preparing editor has ended".into()); - } - let binding = control.playback.lock().unwrap(); - binding - .controller - .clone() - .ok_or_else(|| "Preparing playback is not available".into()) -} - -#[tauri::command] -#[specta::specta] -pub(crate) async fn seek_preparing_editor( - window: Window, - request_epoch: u32, - job_id: String, - seconds: f64, -) -> Result<(), String> { - playback_controller(&window, request_epoch, &job_id)? - .seek(seconds) - .await -} - -#[tauri::command] -#[specta::specta] -pub(crate) async fn set_preparing_editor_playing( - window: Window, - request_epoch: u32, - job_id: String, - playing: bool, -) -> Result<(), String> { - playback_controller(&window, request_epoch, &job_id)? - .set_playing(playing) - .await -} - -pub(crate) async fn join_before_ordinary(app: &AppHandle, path: &Path) -> Result<(), String> { - app.state::() - .inner() - .clone() - .before_ordinary_loading(path) - .await -} - -struct Runner { - control: Arc, - previous: Vec<(RetainedJoin, Arc)>, - response: Option>>, - transport: Option, - stop: Option, - frames: watch::Sender>>, -} - -impl Runner { - async fn run(mut self) -> Result<(), String> { - let result = AssertUnwindSafe(self.run_inner()).catch_unwind().await; - let adopted = matches!(&result, Ok(Ok(()))) - && self - .control - .playback - .lock() - .unwrap() - .handoff - .as_ref() - .is_some_and(|handoff| handoff.committed()); - if adopted { - self.control.accepts_frames.store(false, Ordering::Release); - self.control.cancelled.send_replace(true); - self.stop = None; - } else { - self.control.cancel(); - } - self.frames.send_replace(None); - if let Some(response) = self.response.take() { - let _ = response.send(None); - } - let mut failure = match result { - Ok(Ok(())) => None, - Ok(Err(error)) => { - tracing::debug!(%error, "Tauri preparing preview declined"); - None - } - Err(_) => { - tracing::warn!("Tauri preparing adapter panicked"); - None - } - }; - if let Some(stop) = &self.stop { - stop.cancel(); - } - if let Some(transport) = self.transport.take() - && let Err(error) = transport.stop_and_wait().await - { - failure.get_or_insert(error); - } - if let Some(stop) = self.stop.take() { - let exit = stop.stop_and_wait().await; - self.control.publish_snapshot(&exit.snapshot); - if exit.cleanup_failed { - failure.get_or_insert_with(|| { - exit.error - .clone() - .unwrap_or_else(|| "Preparing media cleanup failed".into()) - }); - } - let mut binding = self.control.playback.lock().unwrap(); - binding.controller = None; - binding.exit = Some(exit); - } - failure.map_or(Ok(()), Err) - } - - async fn run_inner(&mut self) -> Result<(), String> { - for (previous, control) in &self.previous { - let result = previous.wait().await; - control.retire_playback(); - result?; - } - if !self.control.is_active() { - return Ok(()); - } - let control = self.control.clone(); - let transport = crate::frame_ws::create_owned_watch_frame_ws( - self.frames.subscribe(), - Arc::new(move || control.accepts()), - ) - .await?; - let url = transport.url.clone(); - self.transport = Some(transport); - if let Some(response) = self.response.take() - && response.send(Some(url)).is_err() - { - return Ok(()); - } - let mut cancel = self.control.cancelled.subscribe(); - if *cancel.borrow() { - return Ok(()); - } - let observer = tokio::select! { - biased; - _ = cancel.changed() => return Ok(()), - observer = self.control.finalization.wait_for_preparing() => observer, - }; - let Some(mut observer) = observer else { - return Ok(()); - }; - let presentation = tokio::select! { - biased; - _ = cancel.changed() => return Ok(()), - presentation = self.control.finalization.wait_for_presentation() => presentation, - }; - let Some(presentation) = presentation else { - return Ok(()); - }; - let sources = loop { - match observer.latest() { - PreparingStudioState::Available(sources) => break sources, - PreparingStudioState::Unavailable(error) => return Err(error), - PreparingStudioState::Ended => return Ok(()), - PreparingStudioState::Waiting => {} - } - tokio::select! { - biased; - _ = cancel.changed() => return Ok(()), - _ = observer.changed() => {}, - } - }; - let control = self.control.clone(); - let job_id = sources.identity().job_id().to_string(); - let (input, audio) = - tokio::task::spawn_blocking(move || adapt(sources, &control, &presentation)) - .await - .map_err(|error| format!("Preparing adapter join failed: {error}"))??; - if !self.control.is_active() - || !matches!(observer.latest(), PreparingStudioState::Available(_)) - { - return Ok(()); - } - let control = self.control.clone(); - let frame_observer = observer.clone(); - let frames = self.frames.clone(); - let mut tracks = vec!["display".to_string()]; - if let Some(cap_project::StudioRecordingMeta::MultipleSegments { inner }) = - input.recording_meta.studio_meta() - { - if inner - .segments - .iter() - .any(|segment| segment.camera.is_some()) - { - tracks.push("camera".into()); - } - if inner.segments.iter().any(|segment| segment.mic.is_some()) { - tracks.push("microphone".into()); - } - if inner - .segments - .iter() - .any(|segment| segment.system_audio.is_some()) - { - tracks.push("systemAudio".into()); - } - } - let audio_output = Arc::new(cap_editor::AudioOutput::new()); - { - let mut binding = self.control.playback.lock().unwrap(); - binding.audio_output = Some(audio_output.clone()); - binding.latest = Some(PreparingEditorChanged { - request_epoch: self.control.request_epoch, - job_id, - sequence: 0, - fps: crate::EDITOR_PREVIEW_FPS, - progress: PreparingEditorProgress::default(), - playback: PreparingPlaybackState::default(), - seed: PreparingEditorSeed { - title: input.recording_meta.pretty_name.clone(), - tracks, - }, - }); - } - self.control.accepts_frames.store(true, Ordering::Release); - let session = PreparingPlaybackSession::spawn_with_expected_metadata( - input, - audio.tracks, - PreparingPlaybackOptions { - preview: PreparingPreviewOptions { - use_hardware_decoding: !cfg!(target_os = "windows"), - frame_format: EditorFrameFormat::Rgba, - shared_device: None, - }, - fps: crate::EDITOR_PREVIEW_FPS, - resolution: crate::default_editor_preview_resolution(), - }, - audio_output, - Box::new(move |_, output, _| { - if control.accepts() - && (matches!(frame_observer.latest(), PreparingStudioState::Available(_)) - || control.finalization.allows_preparing_continuation()) - && let Some(frame) = crate::editor_window::frame_for_websocket(output) - { - #[cfg(debug_assertions)] - crate::stop_editor_benchmark::capture_ws_frame( - crate::stop_editor_benchmark::CaptureKind::Preparing, - &control.path, - &frame, - ); - frames.send_replace(Some(Arc::new(frame))); - } - }), - audio.expected_metadata, - )?; - self.stop = Some(session.stop_handle()); - let handoff = session.handoff_handle(); - { - let mut binding = self.control.playback.lock().unwrap(); - binding.controller = Some(session.controller()); - binding.handoff = Some(handoff.clone()); - } - let mut updates = session.updates(); - self.control - .publish_snapshot(&updates.borrow_and_update().clone()); - let mut health = tokio::time::interval(Duration::from_millis(50)); - let mut observing = true; - loop { - tokio::select! { - biased; - _ = cancel.changed() => return Ok(()), - _ = handoff.wait_committed() => return Ok(()), - state = observer.changed(), if observing => { - if !matches!(state, PreparingStudioState::Available(_)) { - if !self.control.finalization.allows_preparing_continuation() { return Ok(()); } - observing = false; - } - }, - changed = updates.changed() => { - if changed.is_err() { return Ok(()); } - let snapshot = updates.borrow_and_update().clone(); - self.control.publish_snapshot(&snapshot); - if snapshot.progress.phase != cap_editor::PreparingEditorPhase::Preparing { - return Ok(()); - } - }, - _ = health.tick() => { - if !self.control.is_active() { return Ok(()); } - if self.transport.as_ref().is_some_and(OwnedWatchFrameWs::is_finished) { - return Ok(()); - } - - }, - } - } - } -} - -fn ensure_preparing_stopped_layout( - timeline: &cap_project::TimelineConfiguration, - studio: &cap_project::StudioRecordingMeta, -) -> Result<(), String> { - if timeline.segments.len() != 1 || studio.display_notch().is_some() { - Err("Tauri preparing requires one screen segment without a recorded notch".into()) - } else { - Ok(()) - } -} - -pub(crate) fn project_from_preparing_presentation( - presentation: &ProjectConfiguration, - stopped_timeline: &cap_project::TimelineConfiguration, -) -> ProjectConfiguration { - let mut project = presentation.clone(); - project.timeline = Some(crate::recording::recording_timeline( - stopped_timeline.segments.clone(), - Vec::new(), - )); - project -} - -fn ensure_preparing_track_presentation<'a>( - tracks: impl IntoIterator, Option<&'a Path>)>, -) -> Result<(), String> { - if tracks - .into_iter() - .any(|(camera, keyboard)| camera.is_some() || keyboard.is_some()) - { - Err("Camera and keyboard presentation require ordinary editor loading".into()) - } else { - Ok(()) - } -} - -fn ensure_preparing_cursor_presentation(cursor: &CursorEvents) -> Result<(), String> { - if cursor.clicks.is_empty() { - Ok(()) - } else { - Err("Recorded clicks require late ordinary auto-zoom settings".into()) - } -} - -fn adapt( - sources: Arc, - control: &Arc, - presentation: &ProjectConfiguration, -) -> Result<(PreparingPreviewInput, audio::AudioAdaptation), String> { - let ended = || "Preparing sources ended".to_string(); - let live = sources.live().ok_or_else(ended)?; - let recording_meta = live.metadata().ok_or_else(ended)?.clone(); - let studio = recording_meta.studio_meta().ok_or_else(ended)?; - - let timeline = live - .configuration() - .ok_or_else(ended)? - .timeline - .as_ref() - .ok_or("Missing stopped timeline")?; - ensure_preparing_stopped_layout(timeline, studio)?; - let mut project = project_from_preparing_presentation(presentation, timeline); - if project.clips.is_empty() { - project.clips = - cap_editor::initial_clip_configuration(&recording_meta.project_path, studio); - } - let descriptors = live.segments().ok_or_else(ended)?; - ensure_preparing_track_presentation(descriptors.iter().map(|segment| { - ( - segment - .camera() - .map(cap_recording::recovery::PreparingVideoInput::metadata), - segment.keyboard_path(), - ) - }))?; - let pointer_ids = studio.pointer_cursor_ids(); - let mut segments = Vec::with_capacity(descriptors.len()); - let mut cursor_budget = MAX_CURSOR_BYTES; - for descriptor in descriptors { - if *control.cancelled.borrow() || sources.live().is_none() { - return Err(ended()); - } - let display = live - .video(descriptor.index(), PreparingVideoTrack::Display) - .ok_or_else(ended)?; - let (source, paths) = display.input().ok_or_else(ended)?; - let display = ManagedVideoTrackInput::new(source.clone(), paths.to_vec()) - .map_err(|error| error.to_string())?; - let camera = if descriptor.camera().is_some() { - let camera = live - .video(descriptor.index(), PreparingVideoTrack::Camera) - .ok_or_else(ended)?; - let (source, paths) = camera.input().ok_or_else(ended)?; - Some( - ManagedVideoTrackInput::new(source.clone(), paths.to_vec()) - .map_err(|error| error.to_string())?, - ) - } else { - None - }; - let mut cursor = if descriptor.cursor_path().is_some() { - let cursor = live - .sidecar(descriptor.index(), PreparingSidecarKind::Cursor) - .ok_or_else(ended)?; - let (source, path) = cursor.input().ok_or_else(ended)?; - let reader = source.reader(path).map_err(|error| error.to_string())?; - let mut reader = BufReader::new(CheckedCursorReader { - reader, - is_live: || !*control.cancelled.borrow() && sources.live().is_some(), - remaining: cursor_budget, - }); - let cursor = CursorEvents::load_from_reader(&mut reader)?; - cursor_budget = reader.get_ref().remaining; - cursor - } else { - CursorEvents::default() - }; - ensure_preparing_cursor_presentation(&cursor)?; - cursor.stabilize_short_lived_cursor_shapes( - (!pointer_ids.is_empty()).then_some(&pointer_ids), - cap_project::cursor::SHORT_CURSOR_SHAPE_DEBOUNCE_MS, - ); - segments.push(PreparingPreviewSegment { - video: ManagedSegmentVideoInput::new( - descriptor.index() as usize, - studio, - display, - camera, - ) - .map_err(|error| error.to_string())?, - cursor: Arc::new(cursor), - }); - } - let images = descriptors - .first() - .ok_or_else(ended)? - .cursor_images() - .iter() - .map(|asset| { - ( - asset.id().to_string(), - asset.metadata().clone(), - Arc::<[u8]>::from(asset.bytes()), - ) - }); - let cursor_assets = - FrozenRecordedCursorAssets::new(images).map_err(|error| error.to_string())?; - if *control.cancelled.borrow() || sources.live().is_none() { - return Err(ended()); - } - let audio = audio::adapt_audio(&sources, &recording_meta, control)?; - Ok(( - PreparingPreviewInput { - recording_meta, - project, - segments, - cursor_assets, - }, - audio, - )) -} - -struct CheckedCursorReader { - reader: R, - is_live: F, - remaining: u64, -} - -impl bool> Read for CheckedCursorReader { - fn read(&mut self, output: &mut [u8]) -> io::Result { - if output.is_empty() { - return Ok(0); - } - if !(self.is_live)() { - return Err(io::Error::other("Preparing sources ended")); - } - if self.remaining == 0 { - let mut next = [0]; - return if self.reader.read(&mut next)? == 0 { - Ok(0) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Cursor sidecar exceeds preparing limit", - )) - }; - } - let length = output.len().min(self.remaining as usize); - let count = self.reader.read(&mut output[..length])?; - self.remaining -= count as u64; - Ok(count) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::FinalizationAccess; - - fn control( - path: &Path, - window_id: u32, - epoch: u32, - ) -> (crate::FinalizationToken, Arc) { - let project = - FinalizationProject::capture(path.to_path_buf(), FinalizationAccess::Write).unwrap(); - let finalizations = FinalizingRecordings::default(); - let token = finalizations.start_finalizing(project).unwrap(); - let window_ids = EditorWindowIds::default(); - window_ids - .ids - .lock() - .unwrap() - .push((path.to_path_buf(), window_id)); - let control = Arc::new(ConsumerControl { - window_ids, - window_id, - request_epoch: epoch, - path: path.to_path_buf(), - finalization: token.preparing(), - cancelled: watch::channel(false).0, - accepts_frames: AtomicBool::new(true), - playback: Mutex::default(), - publish: None, - }); - (token, control) - } - - #[tokio::test] - async fn abandoned_admission_joins_transport_without_waiting_for_finalization() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (token, control) = control(&path, 1, 1); - let (response, receiver) = oneshot::channel(); - drop(receiver); - let runner = Runner { - control: control.clone(), - previous: Vec::new(), - response: Some(response), - transport: None, - stop: None, - frames: watch::channel(None).0, - }; - tokio::time::timeout(Duration::from_secs(2), runner.run()) - .await - .expect("Abandoned admission waited for recording finalization") - .unwrap(); - assert!(!control.is_active()); - assert!(control.finalization.is_pending()); - token.finish(Ok(())); - } - - #[tokio::test] - async fn loading_barrier_cancels_and_joins_even_when_the_first_waiter_is_dropped() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (_token, control) = control(&path, 1, 1); - let (release, receiver) = oneshot::channel(); - let (completed, mut completion) = watch::channel(false); - let joined = RetainedJoin::new(tokio::spawn(async move { - receiver.await.unwrap(); - completed.send_replace(true); - Ok(()) - })); - let registry = PreparingConsumers::default(); - registry - .0 - .lock() - .unwrap() - .projects - .entry(path.clone()) - .or_default() - .entries - .push(Entry { - control: control.clone(), - joined: joined.clone(), - }); - let waiter = { - let registry = registry.clone(); - let path = path.clone(); - tokio::spawn(async move { registry.before_ordinary_loading(&path).await }) - }; - while !*control.cancelled.borrow() { - tokio::task::yield_now().await; - } - assert!(!waiter.is_finished()); - assert!(registry.0.lock().unwrap().projects[&path].ordinary_loading); - waiter.abort(); - assert!(waiter.await.unwrap_err().is_cancelled()); - release.send(()).unwrap(); - completion.wait_for(|done| *done).await.unwrap(); - joined.wait().await.unwrap(); - registry.before_ordinary_loading(&path).await.unwrap(); - } - - #[tokio::test] - async fn replacement_admission_retires_previous_audio_after_join() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (_previous_token, previous) = control(&path, 1, 1); - let (_next_token, next) = control(&path, 1, 2); - let output = Arc::new(cap_editor::AudioOutput::new()); - let retained = Arc::downgrade(&output); - previous.playback.lock().unwrap().audio_output = Some(output); - let (release, receiver) = oneshot::channel(); - let joined = RetainedJoin::new(tokio::spawn(async move { - receiver.await.unwrap(); - Ok(()) - })); - let (response, receiver) = oneshot::channel(); - drop(receiver); - let runner = Runner { - control: next, - previous: vec![(joined, previous)], - response: Some(response), - transport: None, - stop: None, - frames: watch::channel(None).0, - }; - let task = tokio::spawn(runner.run()); - tokio::task::yield_now().await; - assert!(retained.upgrade().is_some()); - assert!(!task.is_finished()); - release.send(()).unwrap(); - tokio::time::timeout(Duration::from_secs(2), task) - .await - .unwrap() - .unwrap() - .unwrap(); - assert!(retained.upgrade().is_none()); - } - - #[tokio::test] - async fn failed_cleanup_remains_a_barrier_on_reopen_and_retry() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (_token, control) = control(&path, 1, 1); - let joined = RetainedJoin::new(tokio::spawn(async { - panic!("injected owned cleanup failure"); - })); - let registry = PreparingConsumers::default(); - registry - .0 - .lock() - .unwrap() - .projects - .entry(path.clone()) - .or_default() - .entries - .push(Entry { control, joined }); - let first = registry.before_ordinary_loading(&path).await.unwrap_err(); - let next = registry.before_ordinary_loading(&path).await.unwrap_err(); - assert_eq!(first, next); - assert_eq!(registry.0.lock().unwrap().projects[&path].entries.len(), 1); - } - - #[tokio::test] - async fn closed_window_retires_audio_only_after_owned_cleanup_finishes() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (_token, control) = control(&path, 3, 1); - let output = Arc::new(cap_editor::AudioOutput::new()); - let retained = Arc::downgrade(&output); - control.playback.lock().unwrap().audio_output = Some(output); - let (release, receiver) = oneshot::channel(); - let joined = RetainedJoin::new(tokio::spawn(async move { - receiver.await.unwrap(); - Ok(()) - })); - let registry = PreparingConsumers::default(); - registry - .0 - .lock() - .unwrap() - .projects - .entry(path.clone()) - .or_default() - .entries - .push(Entry { - control: control.clone(), - joined, - }); - registry.cancel_window(3, None); - tokio::task::yield_now().await; - assert!(retained.upgrade().is_some()); - assert!(!control.is_active()); - release.send(()).unwrap(); - tokio::time::timeout(Duration::from_secs(2), async { - loop { - if registry.0.lock().unwrap().projects[&path] - .entries - .is_empty() - { - break; - } - tokio::task::yield_now().await; - } - }) - .await - .unwrap(); - assert!(retained.upgrade().is_none()); - } - - #[tokio::test] - async fn old_frontend_stop_does_not_cancel_a_new_window_request() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (_token, control) = control(&path, 8, 2); - let registry = PreparingConsumers::default(); - let joined = RetainedJoin::new(tokio::spawn(async { Ok(()) })); - registry - .0 - .lock() - .unwrap() - .projects - .entry(path) - .or_default() - .entries - .push(Entry { - control: control.clone(), - joined, - }); - registry.cancel_window(8, Some(1)); - registry.cancel_window(7, None); - assert!(control.accepts()); - registry.cancel_window(8, Some(2)); - assert!(!control.accepts()); - } - - #[test] - fn native_registration_and_finalization_both_guard_frame_delivery() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().canonicalize().unwrap(); - let (token, control) = control(&path, 1, 1); - assert!(control.accepts()); - control.window_ids.ids.lock().unwrap().clear(); - assert!(!control.accepts()); - control.window_ids.ids.lock().unwrap().push((path, 2)); - assert!(!control.accepts()); - token.finish(Ok(())); - assert!(!control.is_active()); - } - - #[test] - fn cancelled_cursor_parse_terminates_without_retrying_interrupted_reads() { - let mut reader = CheckedCursorReader { - reader: std::io::Cursor::new([1, 2]), - is_live: || false, - remaining: 2, - }; - let error = reader.read(&mut [0; 1]).unwrap_err(); - assert_ne!(error.kind(), std::io::ErrorKind::Interrupted); - assert_eq!(reader.reader.position(), 0); - } -} - -#[cfg(test)] -mod presentation_guard_tests { - use super::*; - - fn camera() -> cap_project::VideoMeta { - serde_json::from_value(serde_json::json!({ - "path": "camera", "fps": 24, "start_time": 0.125 - })) - .unwrap() - } - - #[test] - fn screen_only_descriptors_pass_but_any_recorded_camera_is_declined() { - let camera = camera(); - ensure_preparing_track_presentation([(None, None)]).unwrap(); - for tracks in [ - vec![(Some(&camera), None)], - vec![(None, None), (Some(&camera), None)], - ] { - assert_eq!( - ensure_preparing_track_presentation(tracks).unwrap_err(), - "Camera and keyboard presentation require ordinary editor loading" - ); - } - } - - #[test] - fn keyboard_sidecars_decline_even_without_a_camera_or_visible_overlay() { - for path in [Path::new("keyboard.json"), Path::new("keyboard.msgpack")] { - assert_eq!( - ensure_preparing_track_presentation([(None, Some(path))]).unwrap_err(), - "Camera and keyboard presentation require ordinary editor loading" - ); - } - } - - #[test] - fn click_up_and_click_down_both_decline_but_pointer_movement_alone_passes() { - let mut cursor = CursorEvents::default(); - cursor.moves.push(cap_project::CursorMoveEvent { - active_modifiers: Vec::new(), - cursor_id: "arrow".into(), - time_ms: 0.0, - x: 0.2, - y: 0.8, - }); - ensure_preparing_cursor_presentation(&cursor).unwrap(); - for down in [false, true] { - cursor.clicks = vec![cap_project::CursorClickEvent { - active_modifiers: Vec::new(), - cursor_num: 0, - cursor_id: "arrow".into(), - time_ms: 0.0, - down, - }]; - assert_eq!( - ensure_preparing_cursor_presentation(&cursor).unwrap_err(), - "Recorded clicks require late ordinary auto-zoom settings" - ); - } - } - #[test] - fn stopped_layout_rejects_multiple_or_missing_segments_and_recorded_notches() { - let segment = cap_project::TimelineSegment { - recording_clip: 0, - start: 0.0, - end: 6.0, - timescale: 1.0, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }; - for notch in [ - None, - Some(cap_project::DisplayNotch { - x: 0.4, - width: 0.2, - height: 0.03, - }), - ] { - let metadata: cap_project::RecordingMeta = serde_json::from_value(serde_json::json!({ - "pretty_name": "Tauri presentation metadata", "sharing": null, - "segments": [{"display": {"path": "display", "fps": 30, "start_time": 0.0}, "display_notch": notch}] - })).unwrap(); - for count in [0, 1, 2] { - let timeline = - crate::recording::recording_timeline(vec![segment.clone(); count], Vec::new()); - let result = - ensure_preparing_stopped_layout(&timeline, metadata.studio_meta().unwrap()); - assert_eq!(result.is_ok(), count == 1 && notch.is_none()); - if let Err(error) = result { - assert_eq!( - error, - "Tauri preparing requires one screen segment without a recorded notch" - ); - } - } - } - } -} diff --git a/apps/desktop/src-tauri/src/editor_preparing/audio.rs b/apps/desktop/src-tauri/src/editor_preparing/audio.rs deleted file mode 100644 index a7e106ada93..00000000000 --- a/apps/desktop/src-tauri/src/editor_preparing/audio.rs +++ /dev/null @@ -1,227 +0,0 @@ -use super::*; -use cap_editor::PreparingAudioSegmentInput; -use cap_project::{AudioMeta, RecordingMeta, StudioRecordingMeta}; -use cap_recording::recovery::PreparingAudioTrack; - -const MAX_AUDIO_TIMING_LOG_BYTES: u64 = 4 * 1024 * 1024; - -pub(super) struct AudioAdaptation { - pub(super) tracks: Vec, - pub(super) expected_metadata: RecordingMeta, -} - -pub(super) fn adapt_audio( - sources: &Arc, - metadata: &RecordingMeta, - control: &Arc, -) -> Result { - let mut expected_metadata = metadata.clone(); - let cap_project::RecordingMetaInner::Studio(studio) = &mut expected_metadata.inner else { - return Err("Preparing audio requires Studio metadata".into()); - }; - let StudioRecordingMeta::MultipleSegments { inner } = studio.as_mut() else { - return Err("Preparing audio requires stopped segments".into()); - }; - for segment in &mut inner.segments { - for audio in [&mut segment.mic, &mut segment.system_audio] - .into_iter() - .flatten() - { - audio.gap_summary = None; - } - } - let expected_meta = expected_metadata - .studio_meta() - .ok_or("Preparing Studio metadata ended")?; - // Clean Stop joins the synchronous writers of timing-repair lines; finalization can still append unrelated log events. - let timing_log = read_timing_log( - &metadata.project_path.join("recording-logs.log"), - MAX_AUDIO_TIMING_LOG_BYTES, - ); - let timing_eligible = timing_log.is_ok(); - if let Err(error) = &timing_log { - tracing::debug!(%error, "Preparing audio timing requires ordinary loading"); - } - let timing_log = timing_log.ok().flatten(); - let repairs = cap_editor::segment_audio_timing_repairs(expected_meta, timing_log.as_deref()); - let live = sources.live().ok_or("Preparing audio sources ended")?; - let descriptors = live.segments().ok_or("Preparing audio sources ended")?; - let mut tracks = Vec::with_capacity(descriptors.len()); - for (index, descriptor) in descriptors.iter().enumerate() { - let repair = *repairs - .get(index) - .ok_or("Preparing audio timing layout changed")?; - let track = |kind, stem| { - if !timing_eligible { - return None; - } - let lease = live.audio(descriptor.index(), kind)?; - let metadata = lease.metadata()?; - let (source, path) = lease.input()?; - if !preserved_audio_path(metadata, path, stem) { - return None; - } - if path.extension().is_some_and(|extension| extension == "m4a") { - let control = control.clone(); - let sources = sources.clone(); - let input = cap_enc_ffmpeg::SegmentedInput::open_relocatable_interruptible( - source, - [path], - Arc::new(move || *control.cancelled.borrow() || sources.live().is_none()), - ) - .ok()?; - if input - .input() - .streams() - .best(ffmpeg::media::Type::Video) - .is_some() - || !input - .input() - .streams() - .best(ffmpeg::media::Type::Audio) - .is_some_and(|stream| stream.parameters().id() == ffmpeg::codec::Id::AAC) - { - return None; - } - } - cap_audio::ManagedAudioInput::new(source.clone(), path.to_path_buf()).ok() - }; - tracks.push(PreparingAudioSegmentInput { - mic: track(PreparingAudioTrack::Mic, "audio-input"), - system_audio: track(PreparingAudioTrack::SystemAudio, "system_audio"), - timing_repair: repair, - }); - if *control.cancelled.borrow() || sources.live().is_none() { - return Err("Preparing audio sources ended".into()); - } - } - Ok(AudioAdaptation { - tracks, - expected_metadata, - }) -} - -fn read_timing_log(path: &Path, limit: u64) -> Result, String> { - let initial = match std::fs::symlink_metadata(path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), - Err(error) => return Err(format!("Preparing audio timing log metadata: {error}")), - }; - if !initial.is_file() || initial.len() > limit { - return Err("Preparing audio timing log is not a bounded regular file".into()); - } - let file = std::fs::File::open(path) - .map_err(|error| format!("Preparing audio timing log open: {error}"))?; - let opened = file.metadata().map_err(|error| error.to_string())?; - if !opened.is_file() - || opened.len() != initial.len() - || opened.modified().ok() != initial.modified().ok() - { - return Err("Preparing audio timing log changed before reading".into()); - } - let mut bytes = String::with_capacity(opened.len() as usize); - let mut bounded = file.take(limit.saturating_add(1)); - bounded - .read_to_string(&mut bytes) - .map_err(|error| format!("Preparing audio timing log read: {error}"))?; - let finished = bounded - .get_ref() - .metadata() - .map_err(|error| error.to_string())?; - if bytes.len() as u64 != opened.len() - || finished.len() != opened.len() - || finished.modified().ok() != opened.modified().ok() - { - return Err("Preparing audio timing log changed while reading".into()); - } - Ok(Some(bytes)) -} - -fn preserved_audio_path(metadata: &AudioMeta, relative: &Path, stem: &str) -> bool { - let expected = Path::new("content/segments").join(relative); - if Path::new(metadata.path.as_str()) != expected { - return false; - } - ["m4a", "ogg"].into_iter().any(|extension| { - relative - .file_name() - .is_some_and(|name| name == format!("{stem}.{extension}").as_str()) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn timing_snapshot_preserves_utf8_and_distinguishes_absence_from_ineligible() { - let root = tempfile::tempdir().unwrap(); - let path = root.path().join("recording-logs.log"); - assert_eq!(read_timing_log(&path, 100).unwrap(), None); - let text = "微phone segment{index=0}:mic-out: Dropping overlapping audio frame frame_count=1 overlap_ms=100\n"; - std::fs::write(&path, text).unwrap(); - assert_eq!( - read_timing_log(&path, text.len() as u64) - .unwrap() - .as_deref(), - Some(text) - ); - assert!(read_timing_log(&path, text.len() as u64 - 1).is_err()); - std::fs::write(&path, [0xff, 0xfe]).unwrap(); - assert!(read_timing_log(&path, 100).is_err()); - assert!(read_timing_log(root.path(), 100).is_err()); - } - - #[test] - fn oversized_timing_log_is_declined_without_reading_its_payload() { - let root = tempfile::tempdir().unwrap(); - let path = root.path().join("recording-logs.log"); - let file = std::fs::File::create(&path).unwrap(); - file.set_len(MAX_AUDIO_TIMING_LOG_BYTES + 1).unwrap(); - assert!(read_timing_log(&path, MAX_AUDIO_TIMING_LOG_BYTES).is_err()); - } - - #[cfg(unix)] - #[test] - fn timing_log_symlink_is_ineligible() { - let root = tempfile::tempdir().unwrap(); - let target = root.path().join("target.log"); - let path = root.path().join("recording-logs.log"); - std::fs::write(&target, "retained").unwrap(); - std::os::unix::fs::symlink(&target, &path).unwrap(); - assert!(read_timing_log(&path, 100).is_err()); - } - - #[test] - fn audio_admission_requires_the_unchanged_final_path_and_container_name() { - let metadata = |path: &str| AudioMeta { - path: path.into(), - start_time: Some(0.0), - device_id: None, - gap_summary: None, - digital_silence_ms: None, - }; - for extension in ["m4a", "ogg"] { - let relative = PathBuf::from(format!("segment-0/audio-input.{extension}")); - let meta = metadata(&format!("content/segments/{}", relative.display())); - assert!(preserved_audio_path(&meta, &relative, "audio-input")); - assert!(!preserved_audio_path(&meta, &relative, "system_audio")); - } - for path in [ - "segment-0/audio-input.mp3", - "segment-0/audio-input/audio.m4a", - "segment-0/audio-input.M4A", - ] { - assert!(!preserved_audio_path( - &metadata(&format!("content/segments/{path}")), - Path::new(path), - "audio-input" - )); - } - assert!(!preserved_audio_path( - &metadata("content/segments/other/audio-input.m4a"), - Path::new("segment-0/audio-input.m4a"), - "audio-input" - )); - } -} diff --git a/apps/desktop/src-tauri/src/editor_recording.rs b/apps/desktop/src-tauri/src/editor_recording.rs deleted file mode 100644 index 8300cbb16ac..00000000000 --- a/apps/desktop/src-tauri/src/editor_recording.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Mutex; - -use cap_project::RecordingMeta; -use cap_recording::RecordingMode; -use tauri::{AppHandle, Manager}; -use tauri_specta::Event; - -use crate::recording_settings::RecordingSettingsStore; -use crate::windows::{EditorRecordingTarget, ShowCapWindow, editor_window_for_path}; -use crate::{App, ArcLock, tray}; - -#[derive(serde::Serialize, serde::Deserialize, specta::Type, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct EditorRecordingFlowInfo { - pub project_path: PathBuf, - pub project_name: String, -} - -#[derive(serde::Serialize, serde::Deserialize, specta::Type, tauri_specta::Event, Debug, Clone)] -pub struct EditorRecordingFlowChanged { - pub target: Option, -} - -/// The recording mode the user had selected before "Record a new clip" forced -/// Studio, so the preference survives the flow no matter which path ends it. -#[derive(Default)] -pub struct EditorRecordingFlowState { - previous_mode: Mutex>, -} - -fn project_name(project_path: &Path) -> String { - RecordingMeta::load_for_project(project_path) - .map(|meta| meta.pretty_name) - .ok() - .filter(|name| !name.trim().is_empty()) - .or_else(|| { - project_path - .file_stem() - .map(|stem| stem.to_string_lossy().into_owned()) - }) - .unwrap_or_else(|| "this project".into()) -} - -pub fn current(app: &AppHandle) -> Option { - let project_path = EditorRecordingTarget::current(app)?; - Some(EditorRecordingFlowInfo { - project_name: project_name(&project_path), - project_path, - }) -} - -fn emit_changed(app: &AppHandle) { - let _ = EditorRecordingFlowChanged { - target: current(app), - } - .emit(app); -} - -fn begin(app: &AppHandle, project_path: PathBuf) -> Result<(), String> { - let current_mode = RecordingSettingsStore::get(app)? - .and_then(|settings| settings.mode) - .unwrap_or_default(); - if current_mode != RecordingMode::Studio { - RecordingSettingsStore::set_mode(app, RecordingMode::Studio)?; - tray::update_tray_icon_for_mode(app, RecordingMode::Studio); - } - if let Some(state) = app.try_state::() { - *state.previous_mode.lock().unwrap() = Some(current_mode); - } - EditorRecordingTarget::set(app, Some(project_path)); - emit_changed(app); - Ok(()) -} - -/// Runs after every path that takes the editor recording target -- clean stop, -/// failed or cancelled recording, failed restart, and an explicit cancel -- so -/// the forced Studio mode is put back and every window learns the flow ended. -pub fn finish(app: &AppHandle) { - let previous_mode = app - .try_state::() - .and_then(|state| state.previous_mode.lock().unwrap().take()); - if let Some(previous_mode) = previous_mode - && previous_mode != RecordingMode::Studio - { - let still_studio = RecordingSettingsStore::get(app) - .ok() - .flatten() - .and_then(|settings| settings.mode) - .is_none_or(|mode| mode == RecordingMode::Studio); - if still_studio { - if let Err(error) = RecordingSettingsStore::set_mode(app, previous_mode) { - tracing::warn!(%error, "failed to restore the recording mode after the clip flow"); - } else { - tray::update_tray_icon_for_mode(app, previous_mode); - } - } - } - emit_changed(app); -} - -/// Takes the target without touching any window. Callers must already know -/// no recording is live or pending, since a live recording owns the target. -pub fn abort(app: &AppHandle) -> Option { - let project_path = EditorRecordingTarget::take(app)?; - finish(app); - tracing::info!(path = %project_path.display(), "editor recording flow aborted"); - Some(project_path) -} - -pub fn reveal_editor(app: &AppHandle, project_path: &Path) -> bool { - let Some(editor_window) = editor_window_for_path(app, project_path) else { - return false; - }; - let _ = editor_window.unminimize(); - let _ = editor_window.show(); - let _ = editor_window.set_focus(); - true -} - -#[tauri::command] -#[specta::specta] -pub async fn open_editor_recording_main( - app: AppHandle, - state: tauri::State<'_, ArcLock>, - project_path: PathBuf, -) -> Result<(), String> { - if state.read().await.is_recording_active_or_pending() { - return Err("A recording is already in progress".into()); - } - let editor_window = editor_window_for_path(&app, &project_path) - .ok_or_else(|| "The editor window is no longer open".to_string())?; - - if let Some(previous) = abort(&app) - && previous != project_path - { - reveal_editor(&app, &previous); - } - begin(&app, project_path.clone())?; - - if let Err(error) = (ShowCapWindow::Main { - init_target_mode: None, - }) - .show(&app) - .await - { - abort(&app); - return Err(format!("Could not open the recorder: {error}")); - } - - let _ = editor_window.hide(); - tracing::info!(path = %project_path.display(), "editor recording flow started"); - Ok(()) -} - -#[tauri::command] -#[specta::specta] -pub async fn cancel_editor_recording_flow( - app: AppHandle, - state: tauri::State<'_, ArcLock>, -) -> Result<(), String> { - if state.read().await.is_recording_active_or_pending() { - return Err("Stop the recording before leaving the recorder".into()); - } - let Some(project_path) = EditorRecordingTarget::current(&app) else { - return Ok(()); - }; - if editor_window_for_path(&app, &project_path).is_none() { - abort(&app); - return Ok(()); - } - crate::dismiss_main_window(&app); - Ok(()) -} - -/// The editor that armed the flow went away (deleted from the recordings list, -/// say) while nothing was recording: drop the flow so the main window returns -/// to its normal state instead of advertising a clip nobody can receive. -pub fn abort_if_editor_gone(app: &AppHandle) { - let Some(project_path) = EditorRecordingTarget::current(app) else { - return; - }; - if editor_window_for_path(app, &project_path).is_some() { - return; - } - let idle = app - .try_state::>() - .and_then(|state| { - state - .try_read() - .ok() - .map(|state| !state.is_recording_active_or_pending()) - }) - .unwrap_or(false); - if idle { - abort(app); - } -} - -#[tauri::command] -#[specta::specta] -pub async fn get_editor_recording_target( - app: AppHandle, -) -> Result, String> { - Ok(current(&app)) -} diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-tauri/src/editor_window.rs deleted file mode 100644 index dc6b2a111ab..00000000000 --- a/apps/desktop/src-tauri/src/editor_window.rs +++ /dev/null @@ -1,776 +0,0 @@ -use std::{ - collections::HashMap, - ops::Deref, - path::PathBuf, - str::FromStr, - sync::{ - Arc, - atomic::{AtomicU8, Ordering}, - }, - time::Instant, -}; -use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, Window, ipc::CommandArg}; -use tokio::sync::{RwLock, watch}; -use tokio_util::sync::CancellationToken; - -use cap_rendering::GpuOutputFormat; -use tauri_specta::Event; - -use crate::{ - FrameLayoutEvent, create_editor_instance_impl, - frame_ws::{WSFrame, WSFrameFormat, create_watch_frame_ws}, - windows::{CapWindowId, EditorWindowIds}, -}; - -/// Forwards rendered frames to the preview websocket and mirrors each frame's -/// display/camera placement to the webview so overlay hit-boxes always match -/// what was actually rendered. -fn make_frame_callback( - app: AppHandle, - frame_tx: watch::Sender>>, -) -> cap_editor::EditorFrameCallback { - Box::new(move |output, layout| { - let Some(ws_frame) = frame_for_websocket(output) else { - return; - }; - let _ = frame_tx.send(Some(std::sync::Arc::new(ws_frame))); - - // Emitted unconditionally: a prewarmed instance renders before the - // webview attaches its listener, so deduping here would leave a - // fresh window without layout data until the next config change. - let _ = FrameLayoutEvent::from(layout).emit(&app); - }) -} - -pub(crate) fn frame_for_websocket(output: cap_editor::EditorFrameOutput) -> Option { - let frame = match output { - cap_editor::EditorFrameOutput::Nv12(frame) => { - let ws_format = match frame.format { - GpuOutputFormat::Nv12 => WSFrameFormat::Nv12 { full_range: false }, - GpuOutputFormat::Rgba => WSFrameFormat::Rgba, - }; - WSFrame { - data: Arc::new(frame.data.into_vec()).into(), - width: frame.width, - height: frame.height, - stride: frame.y_stride, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - format: ws_format, - created_at: Instant::now(), - } - } - cap_editor::EditorFrameOutput::Rgba(frame) => WSFrame { - data: frame.data.into(), - width: frame.width, - height: frame.height, - stride: frame.padded_bytes_per_row, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - format: WSFrameFormat::Rgba, - created_at: Instant::now(), - }, - // The Tauri editor transports frames over a websocket, so it never - // requests the gpui-only zero-copy surface format. - #[cfg(target_os = "macos")] - cap_editor::EditorFrameOutput::Surface(_) => return None, - }; - Some(frame.into_packed()) -} - -pub struct EditorInstance { - inner: Arc, - pub instance_id: uuid::Uuid, - handoff_playback: tokio::sync::Mutex>, - disposed: std::sync::atomic::AtomicBool, - pub ws_port: u16, - pub ws_shutdown_token: CancellationToken, - app_handle: AppHandle, - render_frame_event_id: tauri::EventId, -} - -type PendingResult = Result, String>; -type PendingReceiver = tokio::sync::watch::Receiver>; - -pub(crate) struct EditorInstanceDelivery { - instance: Arc, - cleanup_runtime: tokio::runtime::Handle, - state: AtomicU8, -} - -impl EditorInstanceDelivery { - const PENDING: u8 = 0; - const ADOPTED: u8 = 1; - const RETIRED: u8 = 2; - - fn new(instance: Arc, cleanup_runtime: tokio::runtime::Handle) -> Arc { - Arc::new(Self { - instance, - cleanup_runtime, - state: AtomicU8::new(Self::PENDING), - }) - } - - fn adopt_into( - &self, - instances: &mut HashMap>, - window_label: &str, - ) -> Result, String> { - self.state - .compare_exchange( - Self::PENDING, - Self::ADOPTED, - Ordering::AcqRel, - Ordering::Acquire, - ) - .map_err(|_| "Editor instance delivery is no longer pending".to_string())?; - let instance = self.instance.clone(); - let _ = instances.insert(window_label.to_string(), instance.clone()); - Ok(instance) - } - - fn retire(&self) -> Option> { - self.state - .compare_exchange( - Self::PENDING, - Self::RETIRED, - Ordering::AcqRel, - Ordering::Acquire, - ) - .ok()?; - let instance = self.instance.clone(); - Some(self.cleanup_runtime.spawn(async move { - instance.dispose().await; - })) - } - - async fn dispose(&self) { - if let Some(cleanup) = self.retire() { - let _ = cleanup.await; - } - } -} - -impl Drop for EditorInstanceDelivery { - fn drop(&mut self) { - drop(self.retire()); - } -} - -#[derive(Clone, Default)] -pub struct PendingEditorInstances(Arc>>); - -async fn do_prewarm(app: AppHandle, path: PathBuf) -> Result, String> { - let (frame_tx, frame_rx) = watch::channel(None); - - let (ws_port, ws_shutdown_token) = create_watch_frame_ws(frame_rx, Default::default()).await; - let ws_guard = ws_shutdown_token.clone().drop_guard(); - let (inner, render_frame_event_id) = - create_editor_instance_impl(&app, path, make_frame_callback(app.clone(), frame_tx)).await?; - - let instance = Arc::new(EditorInstance { - inner, - instance_id: uuid::Uuid::new_v4(), - handoff_playback: Default::default(), - disposed: Default::default(), - ws_port, - ws_shutdown_token, - app_handle: app, - render_frame_event_id, - }); - ws_guard.disarm(); - Ok(instance) -} - -async fn recreate_preparing_instance( - app: AppHandle, - previous: &Arc, -) -> Result, String> { - let (frame_tx, frame_rx) = watch::channel(None); - let (ws_port, ws_shutdown_token) = create_watch_frame_ws(frame_rx, Default::default()).await; - let ws_guard = ws_shutdown_token.clone().drop_guard(); - let state_app = app.clone(); - let inner = previous - .inner - .recreate_preparing_candidate( - move |state| { - let _ = crate::EditorStateChanged::new(state).emit(&state_app); - }, - make_frame_callback(app.clone(), frame_tx), - cap_editor::EditorFrameFormat::Rgba, - ) - .await?; - let render_frame_event_id = crate::RenderFrameEvent::listen_checked(&app, { - let preview_tx = inner.preview_tx.clone(); - move |event| { - preview_tx.send_modify(|request| { - *request = Some((event.frame_number, event.fps, event.resolution_base)) - }); - } - }); - let instance = Arc::new(EditorInstance { - inner, - instance_id: uuid::Uuid::new_v4(), - handoff_playback: Default::default(), - disposed: Default::default(), - ws_port, - ws_shutdown_token, - app_handle: app, - render_frame_event_id, - }); - let fps = crate::EDITOR_PREVIEW_FPS; - let resolution = crate::default_editor_preview_resolution(); - let started = match instance - .inner - .start_preparing_handoff(fps, resolution) - .await - { - Ok(started) => started, - Err(error) => { - instance.dispose().await; - return Err(format!("Preparing handoff start failed: {error:?}")); - } - }; - if !started { - let frame = instance - .inner - .preparing_adoption() - .and_then(|adoption| adoption.frame_number(fps)) - .unwrap_or(0); - instance - .inner - .preview_tx - .send_modify(|request| *request = Some((frame, fps, resolution))); - } - ws_guard.disarm(); - Ok(instance) -} - -fn with_registered_editor( - window_ids: &EditorWindowIds, - id: u32, - action: impl FnOnce() -> T, -) -> Result { - let ids = window_ids.ids.lock().map_err(|error| error.to_string())?; - if !ids.iter().any(|(_, registered_id)| *registered_id == id) { - return Err("Editor window is no longer registered".to_string()); - } - Ok(action()) -} - -impl PendingEditorInstances { - pub fn get(app: &AppHandle) -> Self { - match app.try_state::() { - Some(s) => (*s).clone(), - None => { - let pending = Self::default(); - app.manage(pending); - (*app.state::()).clone() - } - } - } - - pub async fn start_prewarm(app: &AppHandle, window_label: String, path: PathBuf) { - let Ok(CapWindowId::Editor { id }) = CapWindowId::from_str(&window_label) else { - return; - }; - let window_ids = EditorWindowIds::get(app); - let pending = Self::get(app); - let app = app.clone(); - let tx = { - let mut instances = pending.0.write().await; - let admitted = with_registered_editor(&window_ids, id, || { - use std::collections::hash_map::Entry; - match instances.entry(window_label) { - Entry::Vacant(entry) => { - let (tx, rx) = watch::channel(None); - entry.insert(rx); - Some(tx) - } - Entry::Occupied(_) => None, - } - }); - match admitted { - Ok(Some(tx)) => tx, - Ok(None) => return, - Err(error) => { - tracing::debug!(%error, "Skipping prewarm for a retired editor"); - return; - } - } - }; - - let cleanup_runtime = tokio::runtime::Handle::current(); - tokio::spawn(async move { - let result = do_prewarm(app, path) - .await - .map(|instance| EditorInstanceDelivery::new(instance, cleanup_runtime)); - tx.send(Some(result)).ok(); - }); - } - - pub async fn take_prewarmed(&self, window_label: &str) -> Option { - let mut instances = self.0.write().await; - instances.remove(window_label) - } - - pub async fn cancel_prewarm(&self, window_label: &str) { - let mut instances = self.0.write().await; - if let Some(mut rx) = instances.remove(window_label) { - tokio::spawn(async move { - let timeout = tokio::time::timeout(std::time::Duration::from_secs(10), async { - loop { - let instance_to_dispose = { - let borrowed = rx.borrow_and_update().clone(); - match borrowed { - Some(Ok(instance)) => Some(instance), - Some(Err(_)) => break, - None => None, - } - }; - - if let Some(instance) = instance_to_dispose { - instance.dispose().await; - break; - } - - if rx.changed().await.is_err() { - break; - } - } - }); - if timeout.await.is_err() { - tracing::warn!( - "Timed out waiting for prewarmed editor instance to complete for cleanup" - ); - } - }); - } - } - - pub async fn dispose_all(app: &AppHandle) { - let Some(pending) = app.try_state::() else { - return; - }; - - let pending = { - let mut instances = pending.0.write().await; - std::mem::take(&mut *instances) - }; - - let count = pending.len(); - for (_, mut rx) in pending { - let result = tokio::time::timeout(std::time::Duration::from_millis(500), async { - loop { - let instance_to_dispose = { - let borrowed = rx.borrow_and_update().clone(); - match borrowed { - Some(Ok(instance)) => Some(instance), - Some(Err(_)) => break, - None => None, - } - }; - - if let Some(instance) = instance_to_dispose { - instance.dispose().await; - break; - } - - if rx.changed().await.is_err() { - break; - } - } - }) - .await; - - if result.is_err() { - tracing::warn!("Timed out disposing pending editor instance during app exit"); - } - } - - if count > 0 { - tracing::info!(count, "Disposed pending editor instances during app exit"); - } - } -} - -impl EditorInstance { - pub async fn dispose(&self) { - self.dispose_inner(false).await; - } - - async fn dispose_inner(&self, refresh_thumbnail: bool) { - self.disposed.store(true, Ordering::Release); - let mut handoff = self.handoff_playback.lock().await; - if let Some((_, playback)) = handoff.take() { - playback.stop(); - } - if refresh_thumbnail { - if self.inner.dispose_with_thumbnail().await { - let _ = self - .app_handle - .emit("recording-thumbnail-changed", &self.project_path); - } - } else { - self.inner.dispose().await; - } - - self.ws_shutdown_token.cancel(); - self.app_handle.unlisten(self.render_frame_event_id); - } - - fn validate_handoff(&self, expected: &str) -> Result<(), String> { - if expected != self.instance_id.to_string() || self.disposed.load(Ordering::Acquire) { - Err("The handoff editor instance has ended".into()) - } else { - Ok(()) - } - } - - pub(crate) fn commit_preparing_frame( - &self, - expected: &str, - frame: u32, - fps: u32, - ) -> Result { - self.validate_handoff(expected)?; - if self - .inner - .preparing_adoption() - .is_some_and(|adoption| adoption.is_owner()) - { - return Ok(true); - } - if self - .inner - .preparing_adoption() - .is_some_and(|adoption| adoption.invalidated()) - { - return Err("Preparing handoff candidate was superseded".into()); - } - Ok(self - .inner - .preparing_adoption() - .is_none_or(|adoption| adoption.try_commit(frame, fps))) - } - - pub(crate) async fn start_handoff_playback( - &self, - expected: &str, - frame_number: u32, - fps: u32, - resolution: cap_project::XY, - ) -> Result { - let mut handoff = self.handoff_playback.lock().await; - self.validate_handoff(expected)?; - if self - .inner - .preparing_adoption() - .is_some_and(|adoption| adoption.is_owner()) - && let Some(playback) = self.inner.state.lock().await.playback_task.clone() - { - let playback_id = uuid::Uuid::new_v4(); - *handoff = Some((playback_id, playback)); - return Ok(playback_id.to_string()); - } - self.inner - .modify_and_emit_state(|state| state.playhead_position = frame_number) - .await; - let playback = self - .inner - .start_playback_with_handle(fps, resolution, Some(frame_number)) - .await - .map_err(|error| format!("The handoff playback could not start: {error:?}"))?; - if let Err(error) = self.validate_handoff(expected) { - playback.stop(); - return Err(error); - } - let playback_id = uuid::Uuid::new_v4(); - *handoff = Some((playback_id, playback)); - Ok(playback_id.to_string()) - } - - pub(crate) async fn stop_handoff_playback( - &self, - expected: &str, - playback_id: &str, - ) -> Result<(), String> { - let mut handoff = self.handoff_playback.lock().await; - self.validate_handoff(expected)?; - if handoff - .as_ref() - .is_some_and(|(id, _)| id.to_string() == playback_id) - && let Some((_, playback)) = handoff.take() - { - playback.stop(); - } - Ok(()) - } -} - -impl Drop for EditorInstance { - fn drop(&mut self) { - self.ws_shutdown_token.cancel(); - self.app_handle.unlisten(self.render_frame_event_id); - } -} - -impl Deref for EditorInstance { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -#[derive(Clone)] -pub struct EditorInstances(Arc>>>); - -pub struct WindowEditorInstance(pub Arc); - -impl specta::function::FunctionArg for WindowEditorInstance { - fn to_datatype(_: &mut specta::TypeMap) -> Option { - None - } -} - -impl Deref for WindowEditorInstance { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef for WindowEditorInstance { - fn as_ref(&self) -> &EditorInstance { - &self.0 - } -} - -impl<'de, R: Runtime> CommandArg<'de, R> for WindowEditorInstance { - fn from_command( - command: tauri::ipc::CommandItem<'de, R>, - ) -> Result { - let window = Window::from_command(command)?; - - let Some(instances) = window.try_state::() else { - return Err("editor instance registry unavailable".into()); - }; - - // Avoid `futures::executor::block_on` on a tokio RwLock here. That can deadlock or - // panic when the IPC handler runs from inside the tokio runtime (release builds hit - // this path much more aggressively than dev builds and silently terminate the process). - // `try_read` is sync and never blocks; if the lock is contended we surface a transient - // error and let the frontend retry. - let Ok(instance_guard) = instances.0.try_read() else { - return Err("editor instance registry busy".into()); - }; - - let Some(instance) = instance_guard.get(window.label()).cloned() else { - return Err("editor instance unavailable".into()); - }; - - Ok(Self(instance)) - } -} - -pub struct OptionalWindowEditorInstance(pub Option>); - -impl specta::function::FunctionArg for OptionalWindowEditorInstance { - fn to_datatype(_: &mut specta::TypeMap) -> Option { - None - } -} - -impl Deref for OptionalWindowEditorInstance { - type Target = Option>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'de, R: Runtime> CommandArg<'de, R> for OptionalWindowEditorInstance { - fn from_command( - command: tauri::ipc::CommandItem<'de, R>, - ) -> Result { - let Ok(window) = Window::from_command(command) else { - return Ok(Self(None)); - }; - - let Some(instances) = window.try_state::() else { - return Ok(Self(None)); - }; - - match instances.0.try_read() { - Ok(instance_guard) => Ok(Self(instance_guard.get(window.label()).cloned())), - Err(_) => Ok(Self(None)), - } - } -} - -impl EditorInstances { - pub async fn get_or_create( - window: &Window, - path: PathBuf, - ) -> Result, String> { - let CapWindowId::Editor { id } = - CapWindowId::from_str(window.label()).map_err(|error| error.to_string())? - else { - return Err("Invalid editor window".to_string()); - }; - let window_ids = EditorWindowIds::get(window.app_handle()); - with_registered_editor(&window_ids, id, || ())?; - let instances = match window.try_state::() { - Some(s) => (*s).clone(), - None => { - window.manage(Self(Arc::new(RwLock::new(HashMap::new())))); - (*window.state::()).clone() - } - }; - let mut instances = instances.0.write().await; - if let Some(instance) = - with_registered_editor(&window_ids, id, || instances.get(window.label()).cloned())? - { - if !instance - .inner - .preparing_adoption() - .is_some_and(|adoption| adoption.invalidated()) - { - return Ok(instance); - } - let replacement = - recreate_preparing_instance(window.app_handle().clone(), &instance).await?; - if let Err(error) = with_registered_editor(&window_ids, id, || ()) { - replacement.dispose().await; - return Err(error); - } - let _ = instances.insert(window.label().to_string(), replacement.clone()); - instance.dispose().await; - return Ok(replacement); - } - - let requested_at = Instant::now(); - let pending = PendingEditorInstances::get(window.app_handle()); - let mut prewarmed = None; - if let Some(mut prewarmed_rx) = pending.take_prewarmed(window.label()).await { - loop { - let result = prewarmed_rx.borrow_and_update().clone(); - if let Some(result) = result { - prewarmed = Some(result?); - break; - } - if prewarmed_rx.changed().await.is_err() { - break; - } - } - if prewarmed.is_none() { - tracing::warn!( - "Editor open: prewarm channel closed without a result, building on demand" - ); - } - } - let was_prewarmed = prewarmed.is_some(); - let instance = match prewarmed { - Some(instance) => instance, - None => { - with_registered_editor(&window_ids, id, || ())?; - let cleanup_runtime = tokio::runtime::Handle::current(); - let (frame_tx, frame_rx) = watch::channel(None); - let (ws_port, ws_shutdown_token) = - create_watch_frame_ws(frame_rx, Default::default()).await; - let ws_guard = ws_shutdown_token.clone().drop_guard(); - let app_handle = window.app_handle().clone(); - let (inner, render_frame_event_id) = create_editor_instance_impl( - window.app_handle(), - path.clone(), - make_frame_callback(app_handle.clone(), frame_tx), - ) - .await?; - let instance = Arc::new(EditorInstance { - inner, - instance_id: uuid::Uuid::new_v4(), - handoff_playback: Default::default(), - disposed: Default::default(), - ws_port, - ws_shutdown_token, - app_handle, - render_frame_event_id, - }); - ws_guard.disarm(); - EditorInstanceDelivery::new(instance, cleanup_runtime) - } - }; - - let published = with_registered_editor(&window_ids, id, || { - instance.adopt_into(&mut instances, window.label()) - }); - drop(instances); - let instance = match published { - Ok(Ok(instance)) => instance, - Ok(Err(error)) | Err(error) => { - instance.dispose().await; - return Err(error); - } - }; - if was_prewarmed { - tracing::info!( - wait_ms = requested_at.elapsed().as_millis() as u64, - "Editor open: instance served from prewarm" - ); - } else { - tracing::info!( - build_ms = requested_at.elapsed().as_millis() as u64, - "Editor open: instance built on demand (no prewarm hit)" - ); - } - Ok(instance) - } - - /// Project paths of every currently open editor. Used to avoid touching - /// projects that are in use (e.g. when migrating recordings between - /// storage folders). - pub async fn open_project_paths(app: &AppHandle) -> Vec { - let Some(instances) = app.try_state::() else { - return Vec::new(); - }; - - let instances = instances.0.read().await; - instances - .values() - .map(|instance| instance.project_path.clone()) - .collect() - } - - pub async fn remove(window: Window) { - let Some(instances) = window.try_state::() else { - return; - }; - - let instance = instances.0.write().await.remove(window.label()); - if let Some(instance) = instance { - instance.dispose_inner(true).await; - } - } - - pub async fn dispose_all(app: &AppHandle) { - let Some(instances) = app.try_state::() else { - return; - }; - - let instances = { - let mut instances = instances.0.write().await; - std::mem::take(&mut *instances) - }; - - let count = instances.len(); - for (_, instance) in instances { - instance.dispose().await; - } - - if count > 0 { - tracing::info!(count, "Disposed editor instances during app exit"); - } - } -} diff --git a/apps/desktop/src-tauri/src/exit_shutdown.rs b/apps/desktop/src-tauri/src/exit_shutdown.rs deleted file mode 100644 index 9c9e8b13804..00000000000 --- a/apps/desktop/src-tauri/src/exit_shutdown.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::sync::atomic::{AtomicU8, Ordering}; -use tokio::task::JoinHandle; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ExitBlocked { - StateUnavailable, - RecordingActive, - FinalizationActive, - ExportActive, - UploadActive, - UpdateInstalling, - AlreadyExiting, -} - -impl ExitBlocked { - pub(crate) fn message(self) -> &'static str { - match self { - Self::StateUnavailable => { - "Cap could not confirm that recording has finished. Wait for any recording to finish, then try again." - } - Self::RecordingActive => { - "Finish or cancel your recording before quitting or restarting Cap. If you already pressed Stop, wait for it to finish." - } - Self::FinalizationActive => { - "Cap is still saving your recording. Wait for it to finish before quitting or restarting." - } - Self::ExportActive => { - "Wait for your export to finish before quitting or restarting Cap." - } - Self::UploadActive => { - "Cap is still uploading your recording. Wait for it to finish before quitting or restarting." - } - Self::UpdateInstalling => "Cap is installing an update. Wait for it to finish.", - Self::AlreadyExiting => "Cap is already shutting down.", - } - } -} - -pub(crate) fn with_idle_recording_state( - state: &tokio::sync::RwLock, - verify_idle: impl FnOnce(&T) -> Result<(), ExitBlocked>, - begin: impl FnOnce() -> R, -) -> Result { - let state = state - .try_read() - .map_err(|_| ExitBlocked::StateUnavailable)?; - verify_idle(&state)?; - Ok(begin()) -} - -pub(crate) fn recording_start_allowed( - is_exiting: bool, - update_blocks_recording: bool, -) -> Result<(), &'static str> { - if is_exiting { - Err("Cap is shutting down. Recording has not started.") - } else if update_blocks_recording { - Err("Cap is installing an update. Finish updating or restart Cap before recording.") - } else { - Ok(()) - } -} - -pub(crate) fn prepare_then_begin_exit( - prepare: impl FnOnce() -> Result<(), String>, - begin: impl FnOnce() -> bool, -) -> Result<(), String> { - prepare()?; - if begin() { - Ok(()) - } else { - Err(ExitBlocked::AlreadyExiting.message().into()) - } -} - -#[derive(Default)] -pub(crate) struct UpdateInstallState(AtomicU8); - -impl UpdateInstallState { - pub(crate) fn is_installing(&self) -> bool { - self.0.load(Ordering::Acquire) == 1 - } - - pub(crate) fn blocks_recording(&self) -> bool { - self.0.load(Ordering::Acquire) != 0 - } - - pub(crate) fn begin(&self) -> Option> { - self.0 - .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire) - .ok() - .map(|_| UpdateInstallGuard { - state: self, - completed: false, - }) - } -} - -pub(crate) struct UpdateInstallGuard<'a> { - state: &'a UpdateInstallState, - completed: bool, -} - -impl UpdateInstallGuard<'_> { - pub(crate) fn complete(mut self, can_exit_later: bool) { - self.state - .0 - .store(if can_exit_later { 2 } else { 0 }, Ordering::Release); - self.completed = true; - } -} - -impl Drop for UpdateInstallGuard<'_> { - fn drop(&mut self) { - if !self.completed { - let _ = self - .state - .0 - .compare_exchange(1, 0, Ordering::AcqRel, Ordering::Acquire); - } - } -} - -pub(crate) fn run_while_active(is_exiting: FExit, operation: F) -> Option -where - FExit: Fn() -> bool, - F: FnOnce() -> T, -{ - if is_exiting() { - None - } else { - Some(operation()) - } -} - -pub(crate) fn collect_device_inventory( - is_exiting: FExit, - camera_permitted: bool, - microphone_permitted: bool, - list_cameras: FCamera, - list_microphones: FMicrophone, -) -> Option<(Vec, Vec)> -where - FExit: Fn() -> bool, - FCamera: FnOnce() -> Vec, - FMicrophone: FnOnce() -> Vec, -{ - if is_exiting() { - return None; - } - - let cameras = if camera_permitted { - if is_exiting() { - return None; - } - - list_cameras() - } else { - Vec::new() - }; - - if is_exiting() { - return None; - } - - let microphones = if microphone_permitted { - if is_exiting() { - return None; - } - - list_microphones() - } else { - Vec::new() - }; - - if is_exiting() { - return None; - } - - Some((cameras, microphones)) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum AppExitAction { - #[cfg(target_os = "macos")] - Process(i32), - #[cfg(not(target_os = "macos"))] - Runtime(i32), -} - -pub(crate) fn app_exit_action(exit_code: i32) -> AppExitAction { - #[cfg(target_os = "macos")] - { - AppExitAction::Process(exit_code) - } - - #[cfg(not(target_os = "macos"))] - { - AppExitAction::Runtime(exit_code) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ExitRequestDecision { - StartCleanup, - AlreadyExiting, - ExportActive, - AllowRuntimeExit, - AllowRuntimeRestart, -} - -pub(crate) fn handle_exit_requested( - is_exiting: bool, - export_active: bool, - runtime_exit_requested: bool, - runtime_restart_requested: bool, - prevent_exit: FPrevent, -) -> ExitRequestDecision -where - FPrevent: FnOnce(), -{ - if runtime_restart_requested { - ExitRequestDecision::AllowRuntimeRestart - } else if is_exiting && runtime_exit_requested { - ExitRequestDecision::AllowRuntimeExit - } else if export_active { - prevent_exit(); - ExitRequestDecision::ExportActive - } else if is_exiting { - prevent_exit(); - ExitRequestDecision::AlreadyExiting - } else { - prevent_exit(); - ExitRequestDecision::StartCleanup - } -} - -pub(crate) fn read_target_under_cursor( - is_exiting: FExit, - display: FDisplay, - window: FWindow, -) -> Option<(Option, Option)> -where - FExit: Fn() -> bool, - FDisplay: FnOnce() -> Option, - FWindow: FnOnce() -> Option, -{ - if is_exiting() { - return None; - } - - let display = display(); - - if is_exiting() { - return None; - } - - let window = window(); - - if is_exiting() { - return None; - } - - Some((display, window)) -} - -pub(crate) fn abort_join_handles( - tasks: impl IntoIterator>, - task: Option>, -) { - for task in tasks { - task.abort(); - } - - if let Some(task) = task { - task.abort(); - } -} diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs deleted file mode 100644 index 662fe679128..00000000000 --- a/apps/desktop/src-tauri/src/export.rs +++ /dev/null @@ -1,2747 +0,0 @@ -mod estimate_request; - -use crate::editor_window::{OptionalWindowEditorInstance, WindowEditorInstance}; -use crate::{FramesRendered, get_video_metadata}; -use cap_export::{ExporterBase, make_cursor_only_project}; -use cap_project::{RecordingMeta, TimelineFrameMapping, XY}; -use cap_rendering::{ - FrameRenderer, ProjectRecordingsMeta, ProjectUniforms, RenderSegment, RenderVideoConstants, - RendererLayers, TransitionRenderInput, ZoomTransformTimeline, -}; -use cap_utils::export_resources::{DiskBudget, ExportResources, estimated_working_bytes}; -use futures::FutureExt; -use image::codecs::jpeg::JpegEncoder; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::any::Any; -use std::future::Future; -use std::panic::AssertUnwindSafe; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - process::Stdio, - sync::{ - Arc, LazyLock, Mutex, MutexGuard, - atomic::{AtomicBool, AtomicUsize, Ordering}, - }, -}; -use tauri::Manager; -use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; -use tokio::io::AsyncBufReadExt; -use tokio_util::sync::CancellationToken; -use tracing::{error, info, instrument, warn}; - -fn panic_message(panic: Box) -> String { - if let Some(msg) = panic.downcast_ref::<&str>() { - msg.to_string() - } else if let Some(msg) = panic.downcast_ref::() { - msg.clone() - } else { - "unknown panic".to_string() - } -} - -fn export_panic_error(panic: Box) -> String { - let panic_msg = panic_message(panic); - error!( - target: "cap_desktop_export", - panic = %panic_msg, - "export command panicked" - ); - sentry::capture_message( - &format!("Export command panicked: {panic_msg}"), - sentry::Level::Error, - ); - "Export failed unexpectedly".to_string() -} - -#[cfg(all(windows, not(debug_assertions)))] -async fn run_export_command(make_future: F) -> Result -where - T: Send + 'static, - F: FnOnce() -> Fut + Send + 'static, - Fut: Future> + 'static, -{ - let (tx, rx) = tokio::sync::oneshot::channel(); - - // Windows release builds can stack-overflow in Tauri's IPC command entry before the save - // dialog or exporter sidecar starts. Keep the export future off that command-entry stack. - std::thread::Builder::new() - .name("cap-export-command".to_string()) - .stack_size(EXPORT_COMMAND_THREAD_STACK_SIZE) - .spawn(move || { - let result = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(runtime) => runtime.block_on(async move { - match AssertUnwindSafe(make_future()).catch_unwind().await { - Ok(result) => result, - Err(panic) => Err(export_panic_error(panic)), - } - }), - Err(err) => Err(format!("Failed to build export command runtime: {err}")), - }; - - let _ = tx.send(result); - }) - .map_err(|err| format!("Failed to spawn export command thread: {err}"))?; - - rx.await - .map_err(|err| format!("Export command thread stopped: {err}"))? -} - -#[cfg(not(all(windows, not(debug_assertions))))] -async fn run_export_command(make_future: F) -> Result -where - F: FnOnce() -> Fut, - Fut: Future>, -{ - match AssertUnwindSafe(make_future()).catch_unwind().await { - Ok(result) => result, - Err(panic) => Err(export_panic_error(panic)), - } -} - -async fn run_protected_export( - project_path: &Path, - settings: &ExportSettings, - progress: ExportProgress, - force_ffmpeg: bool, - cancel_token: CancellationToken, -) -> Result { - match AssertUnwindSafe(do_export( - project_path, - settings, - progress, - force_ffmpeg, - cancel_token, - )) - .catch_unwind() - .await - { - Ok(result) => result, - Err(panic) => { - let panic_msg = panic_message(panic); - error!( - target: "cap_desktop_export", - panic = %panic_msg, - "export task panicked" - ); - sentry::capture_message( - &format!("Export task panicked: {panic_msg}"), - sentry::Level::Error, - ); - Err("Export failed unexpectedly".to_string()) - } - } -} - -const EXPORTER_STDERR_TAIL_LIMIT: usize = 80; -const EXPORT_PROGRESS_FORWARD_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100); -#[cfg(all(windows, not(debug_assertions)))] -const EXPORT_COMMAND_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024; -static ACTIVE_EXPORT_SESSIONS: AtomicUsize = AtomicUsize::new(0); -static ACTIVE_EXPORT_CANCELLATIONS: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); -static ACTIVE_EXPORT_WINDOW_SESSIONS: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); -static NEXT_EXPORT_COMMAND_ID: AtomicUsize = AtomicUsize::new(1); - -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x0800_0000; - -#[derive(Deserialize)] -#[serde(tag = "type")] -enum ExportSidecarMessage { - Progress { - rendered_count: u32, - total_frames: u32, - }, - Completed { - path: PathBuf, - }, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ExportWorkerMode { - HardwareOptimized, - SoftwareSafe, -} - -impl ExportWorkerMode { - fn force_ffmpeg_decoder(self, requested: bool) -> bool { - requested || matches!(self, Self::SoftwareSafe) - } - - fn is_software_safe(self) -> bool { - matches!(self, Self::SoftwareSafe) - } -} - -#[derive(Clone)] -struct ExportProgress(tauri::ipc::Channel); - -#[derive(Debug, PartialEq, Eq)] -enum ExportSaveDestination { - Selected(PathBuf), - #[cfg(any(target_os = "macos", test))] - Default, -} - -#[cfg(any(target_os = "macos", test))] -fn resolve_macos_export_destination( - result: Result, String>, -) -> Option { - match result { - Ok(path) => path.map(ExportSaveDestination::Selected), - Err(error) => { - warn!( - error, - "Save dialog unavailable; keeping the export in its project output folder" - ); - Some(ExportSaveDestination::Default) - } - } -} - -struct ExportSaveDialogRequest { - #[cfg(not(target_os = "linux"))] - app: tauri::AppHandle, - #[cfg(target_os = "linux")] - parent: tauri::Window, - #[cfg(target_os = "linux")] - cancel_token: CancellationToken, - file_name: String, - file_type: String, -} - -impl ExportProgress { - fn send(&self, progress: FramesRendered) -> bool { - self.0.send(progress).is_ok() - } -} - -struct ExportProgressForwarder { - progress: ExportProgress, - last_emit_at: Option, -} - -impl ExportProgressForwarder { - fn new(progress: ExportProgress) -> Self { - Self { - progress, - last_emit_at: None, - } - } - - fn send(&mut self, rendered_count: u32, total_frames: u32) -> bool { - let now = std::time::Instant::now(); - let should_emit = rendered_count == 0 - || rendered_count >= total_frames - || self - .last_emit_at - .is_none_or(|last| now.duration_since(last) >= EXPORT_PROGRESS_FORWARD_INTERVAL); - - if !should_emit { - return true; - } - - self.last_emit_at = Some(now); - self.progress.send(FramesRendered { - rendered_count, - total_frames, - }) - } -} - -struct ActiveExportCancellation { - token: CancellationToken, - window_label: Option, -} - -struct ExportCancellationGuard { - export_id: String, - token: CancellationToken, -} - -impl ExportCancellationGuard { - fn new(export_id: String, window_label: Option) -> Self { - let token = CancellationToken::new(); - active_export_cancellations().insert( - export_id.clone(), - ActiveExportCancellation { - token: token.clone(), - window_label, - }, - ); - - Self { export_id, token } - } - - fn token(&self) -> CancellationToken { - self.token.clone() - } -} - -impl Drop for ExportCancellationGuard { - fn drop(&mut self) { - active_export_cancellations().remove(&self.export_id); - } -} - -fn active_export_cancellations() -> MutexGuard<'static, HashMap> { - match ACTIVE_EXPORT_CANCELLATIONS.lock() { - Ok(guard) => guard, - Err(err) => err.into_inner(), - } -} - -fn active_export_window_sessions() -> MutexGuard<'static, HashMap> { - match ACTIVE_EXPORT_WINDOW_SESSIONS.lock() { - Ok(guard) => guard, - Err(err) => err.into_inner(), - } -} - -fn next_export_command_id(prefix: &str) -> String { - let id = NEXT_EXPORT_COMMAND_ID.fetch_add(1, Ordering::AcqRel); - format!("{prefix}-{id}") -} - -fn cancel_export_by_id(export_id: &str) -> bool { - let cancellations = active_export_cancellations(); - if let Some(active_export) = cancellations.get(export_id) { - active_export.token.cancel(); - return true; - } - - false -} - -pub fn cancel_exports_for_window(window_label: &str) { - let tokens = { - let cancellations = active_export_cancellations(); - cancellations - .values() - .filter(|active_export| active_export.window_label.as_deref() == Some(window_label)) - .map(|active_export| active_export.token.clone()) - .collect::>() - }; - - for token in &tokens { - token.cancel(); - } - - let released_sessions = release_export_sessions_for_window(window_label); - - if !tokens.is_empty() || released_sessions > 0 { - info!( - window = window_label, - cancelled_count = tokens.len(), - released_sessions, - "Cancelled window exports" - ); - } -} - -pub fn cancel_all_exports() { - let tokens = { - let cancellations = active_export_cancellations(); - cancellations - .values() - .map(|active_export| active_export.token.clone()) - .collect::>() - }; - - for token in &tokens { - token.cancel(); - } - - let released_sessions = release_all_window_export_sessions(); - - if !tokens.is_empty() || released_sessions > 0 { - info!( - cancelled_count = tokens.len(), - released_sessions, "Cancelled active exports" - ); - } -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub fn cancel_export(export_id: String) -> bool { - cancel_export_by_id(&export_id) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window))] -pub fn cancel_current_window_exports(window: tauri::Window) { - cancel_exports_for_window(window.label()); -} - -fn retain_export_session() { - let active_exports = ACTIVE_EXPORT_SESSIONS.fetch_add(1, Ordering::AcqRel) + 1; - info!(active_exports, "Export session guard started"); -} - -fn release_export_session() { - let mut current = ACTIVE_EXPORT_SESSIONS.load(Ordering::Acquire); - loop { - if current == 0 { - tracing::warn!("Export session guard release requested with no active exports"); - return; - } - - match ACTIVE_EXPORT_SESSIONS.compare_exchange( - current, - current - 1, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => { - info!( - active_exports = current - 1, - "Export session guard released" - ); - return; - } - Err(next) => current = next, - } - } -} - -pub fn export_session_active() -> bool { - ACTIVE_EXPORT_SESSIONS.load(Ordering::Acquire) > 0 -} - -fn retain_export_session_for_window(window_label: &str) { - retain_export_session(); - let mut sessions = active_export_window_sessions(); - *sessions.entry(window_label.to_string()).or_insert(0) += 1; -} - -fn release_export_session_for_window(window_label: &str) { - let should_release = { - let mut sessions = active_export_window_sessions(); - if let Some(count) = sessions.get_mut(window_label) { - *count -= 1; - if *count == 0 { - sessions.remove(window_label); - } - true - } else { - false - } - }; - - if should_release { - release_export_session(); - } else { - tracing::warn!( - window = window_label, - "Export session guard release requested with no active window export" - ); - } -} - -fn release_export_sessions_for_window(window_label: &str) -> usize { - let count = active_export_window_sessions() - .remove(window_label) - .unwrap_or(0); - - for _ in 0..count { - release_export_session(); - } - - count -} - -fn release_all_window_export_sessions() -> usize { - let count = active_export_window_sessions() - .drain() - .map(|(_, count)| count) - .sum::(); - - for _ in 0..count { - release_export_session(); - } - - count -} - -#[tauri::command] -#[specta::specta] -pub fn begin_export_session(window: tauri::Window) { - retain_export_session_for_window(window.label()); -} - -#[tauri::command] -#[specta::specta] -pub fn end_export_session(window: tauri::Window) { - release_export_session_for_window(window.label()); -} - -struct ExportSessionGuard; - -impl ExportSessionGuard { - fn new() -> Self { - retain_export_session(); - Self - } -} - -impl Drop for ExportSessionGuard { - fn drop(&mut self) { - release_export_session(); - } -} - -#[cfg(windows)] -pub(crate) fn configure_exporter_command(command: &mut tokio::process::Command) { - command.creation_flags(CREATE_NO_WINDOW); -} - -#[cfg(not(windows))] -pub(crate) fn configure_exporter_command(_command: &mut tokio::process::Command) {} - -async fn run_out_of_process_export( - project_path: &Path, - settings: &ExportSettings, - progress: ExportProgress, - force_ffmpeg: bool, - cancel_token: CancellationToken, -) -> Result { - let mode = initial_export_worker_mode(); - match run_out_of_process_export_attempt( - project_path, - settings, - progress.clone(), - force_ffmpeg, - mode, - cancel_token.clone(), - ) - .await - { - Ok(path) => Ok(path), - Err(e) if e != "Export cancelled" && !mode.is_software_safe() => { - error!( - error = %e, - "Export worker failed, retrying with software rendering and encoding" - ); - run_out_of_process_export_attempt( - project_path, - settings, - progress, - force_ffmpeg, - ExportWorkerMode::SoftwareSafe, - cancel_token, - ) - .await - } - Err(e) => Err(e), - } -} - -fn initial_export_worker_mode() -> ExportWorkerMode { - if should_start_export_worker_in_software_safe_mode() { - ExportWorkerMode::SoftwareSafe - } else { - ExportWorkerMode::HardwareOptimized - } -} - -async fn run_out_of_process_export_attempt( - project_path: &Path, - settings: &ExportSettings, - progress: ExportProgress, - force_ffmpeg: bool, - mode: ExportWorkerMode, - cancel_token: CancellationToken, -) -> Result { - use cap_utils::operation_diagnostics::{Field, Operation}; - let mut diagnostic = Operation::start( - "export_worker", - &[ - Field::identifier( - "resource", - cap_utils::operation_diagnostics::resource_id(project_path), - ), - Field::number("requested_fps", settings.fps() as u64), - Field::flag("software_safe", mode.is_software_safe()), - Field::flag( - "force_ffmpeg_decoder", - mode.force_ffmpeg_decoder(force_ffmpeg), - ), - ], - ); - let result = run_out_of_process_export_attempt_inner( - project_path, - settings, - progress, - force_ffmpeg, - mode, - cancel_token, - &mut diagnostic, - ) - .await; - diagnostic.finish(result.is_ok()); - result -} - -async fn run_out_of_process_export_attempt_inner( - project_path: &Path, - settings: &ExportSettings, - progress: ExportProgress, - force_ffmpeg: bool, - mode: ExportWorkerMode, - cancel_token: CancellationToken, - diagnostic: &mut cap_utils::operation_diagnostics::Operation, -) -> Result { - use cap_utils::operation_diagnostics::Field; - if cancel_token.is_cancelled() { - return Err("Export cancelled".to_string()); - } - - diagnostic.stage("resolving_worker"); - let bin_path = resolve_exporter_binary()?; - let settings_json = serde_json::to_string(settings) - .map_err(|e| format!("Failed to serialize settings: {e}"))?; - let force_ffmpeg_decoder = mode.force_ffmpeg_decoder(force_ffmpeg); - let mut progress_forwarder = ExportProgressForwarder::new(progress); - - info!( - path = %bin_path.display(), - project_path = %project_path.display(), - force_ffmpeg = force_ffmpeg_decoder, - mode = ?mode, - "Starting export worker process" - ); - - let mut command = tokio::process::Command::new(&bin_path); - command - .arg("export") - .arg(project_path) - .arg("--settings-json") - .arg(settings_json) - .stdin(Stdio::null()); - - if let Ok(parent) = serde_json::to_string(&diagnostic.id()) { - command.env("CAP_DIAGNOSTIC_PARENT", parent); - } - command.arg("--progress-json"); - - if force_ffmpeg_decoder { - command.arg("--force-ffmpeg-decoder"); - } - - if mode.is_software_safe() { - command.env("CAP_EXPORT_FORCE_SOFTWARE_ENCODER", "1"); - if cfg!(windows) { - command.env("CAP_RENDER_FORCE_SOFTWARE_ADAPTER", "1"); - } - } - configure_exporter_command(&mut command); - - let mut child = cap_utils::process::spawn_with_piped_output(command).map_err(|e| { - format!( - "Failed to start export worker '{}': {e}", - bin_path.display() - ) - })?; - - if let Some(process_id) = child.id() { - diagnostic.field(Field::number("worker_process", process_id as u64)); - } - diagnostic.stage("waiting_for_progress"); - let stdout = child - .stdout - .take() - .ok_or_else(|| "Export worker stdout was not captured".to_string())?; - let stderr = child - .stderr - .take() - .ok_or_else(|| "Export worker stderr was not captured".to_string())?; - let stderr_task = tokio::spawn(collect_exporter_stderr_tail(stderr)); - - let mut completed_path = None; - let mut next_diagnostic_frame = 0; - let mut stdout_lines = tokio::io::BufReader::new(stdout).lines(); - - while let Some(line) = tokio::select! { - line = stdout_lines.next_line() => { - line.map_err(|e| format!("Failed reading export worker stdout: {e}"))? - } - _ = cancel_token.cancelled() => { - let _ = child.kill().await; - let _ = child.wait().await; - let _ = stderr_task.await; - diagnostic.stage("cancelled"); - return Err("Export cancelled".to_string()); - } - } { - match serde_json::from_str::(&line) { - Ok(ExportSidecarMessage::Progress { - rendered_count, - total_frames, - }) => { - if rendered_count >= next_diagnostic_frame { - diagnostic.field(Field::number("rendered_frames", rendered_count as u64)); - diagnostic.field(Field::number("expected_frames", total_frames as u64)); - diagnostic.stage("rendering"); - next_diagnostic_frame = - rendered_count.saturating_add((total_frames / 10).max(1)); - } - if !progress_forwarder.send(rendered_count, total_frames) { - let _ = child.kill().await; - diagnostic.stage("cancelled"); - return Err("Export cancelled".to_string()); - } - } - Ok(ExportSidecarMessage::Completed { path }) => { - diagnostic.field(Field::flag("worker_reported_completion", true)); - diagnostic.stage("waiting_for_worker_exit"); - completed_path = Some(path); - } - Err(e) => { - error!( - line = %line, - error = %e, - "Ignoring invalid export worker stdout line" - ); - } - } - } - - let status = tokio::select! { - status = child.wait() => { - status.map_err(|e| format!("Failed waiting for export worker: {e}"))? - } - _ = cancel_token.cancelled() => { - let _ = child.kill().await; - let _ = child.wait().await; - let _ = stderr_task.await; - diagnostic.stage("cancelled"); - return Err("Export cancelled".to_string()); - } - }; - diagnostic.field(Field::flag("worker_exit_success", status.success())); - if let Some(code) = status.code() { - diagnostic.field(Field::number( - "worker_exit_code_bits", - u64::from(code as u32), - )); - } - #[cfg(unix)] - { - use std::os::unix::process::ExitStatusExt; - if let Some(signal) = status.signal() { - diagnostic.field(Field::number("worker_signal", signal as u64)); - } - } - diagnostic.stage("worker_exited"); - let stderr_tail = stderr_task.await.unwrap_or_default(); - - if !status.success() { - if let Some(path) = completed_path { - // A worker can report completion and then abort during Windows GPU/codec teardown. - // Treat the completed output as authoritative so a post-export sidecar crash does - // not become a failed export for the user. - error!( - status = %status, - path = %path.display(), - "Export worker exited unsuccessfully after reporting completion" - ); - return Ok(path); - } - - let stderr_tail = stderr_tail.join("\n"); - return Err(format!( - "Export worker exited with status {status}. Stderr tail:\n{stderr_tail}" - )); - } - - completed_path - .ok_or_else(|| "Export worker finished without reporting an output path".to_string()) -} - -async fn collect_exporter_stderr_tail(stderr: tokio::process::ChildStderr) -> Vec { - let mut lines = tokio::io::BufReader::new(stderr).lines(); - let mut tail = Vec::new(); - - loop { - match lines.next_line().await { - Ok(Some(line)) => { - if cap_utils::operation_diagnostics::relay_worker_record(&line) { - continue; - } - if cfg!(debug_assertions) { - info!(line = %line, "Export worker stderr"); - } - tail.push(line); - if tail.len() > EXPORTER_STDERR_TAIL_LIMIT { - tail.remove(0); - } - } - Ok(None) => break, - Err(e) => { - tail.push(format!("failed reading stderr: {e}")); - break; - } - } - } - - tail -} - -pub(crate) fn resolve_exporter_binary() -> Result { - let exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?; - if let Some(dir) = exe.parent() { - for candidate in adjacent_exporter_binary_candidates(dir) { - if candidate.exists() { - warn_if_exporter_stale(&exe, &candidate); - return Ok(candidate); - } - } - } - - if let Ok(cwd) = std::env::current_dir() { - for root in std::iter::once(cwd.as_path()).chain(cwd.ancestors()) { - for candidate in exporter_binary_candidates(root) { - if candidate.exists() { - warn_if_exporter_stale(&exe, &candidate); - return Ok(candidate); - } - } - } - } - - Err(format!( - "Export worker binary not found; place {} next to the app executable or build the Tauri sidecar bundle", - exporter_bin_name() - )) -} - -/// Dev-loop trap: `tauri dev` 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) { - if !cfg!(debug_assertions) { - return; - } - let mtime = |p: &Path| std::fs::metadata(p).and_then(|m| m.modified()).ok(); - if let (Some(app_time), Some(exporter_time)) = (mtime(app_exe), mtime(exporter)) - && let Ok(lag) = app_time.duration_since(exporter_time) - && lag.as_secs() > 60 - { - tracing::warn!( - exporter = %exporter.display(), - lag_secs = lag.as_secs(), - "Export worker binary is older than the app; exports may not match \ - the preview. Rebuild it (cargo build -p cap) and copy it over \ - target/debug/cap-exporter." - ); - } -} - -fn exporter_binary_candidates(root: &Path) -> Vec { - let mut candidates = Vec::new(); - - if cfg!(debug_assertions) { - candidates.extend(debug_exporter_binary_candidates(root)); - } - - candidates.push( - root.join("apps") - .join("desktop") - .join("src-tauri") - .join("binaries") - .join(exporter_bin_name()), - ); - - if let Some(target_triple) = current_target_triple() { - candidates.push( - root.join("apps") - .join("desktop") - .join("src-tauri") - .join("binaries") - .join(format!( - "cap-exporter-{target_triple}{}", - std::env::consts::EXE_SUFFIX - )), - ); - } - - candidates -} - -fn debug_exporter_binary_candidates(root: &Path) -> Vec { - let mut candidates = vec![ - root.join("target").join("debug").join(exporter_bin_name()), - root.join("target") - .join("debug") - .join(format!("cap{}", std::env::consts::EXE_SUFFIX)), - ]; - - if let Some(target_triple) = current_target_triple() { - candidates.push( - root.join("target") - .join(target_triple) - .join("debug") - .join(exporter_bin_name()), - ); - candidates.push( - root.join("target") - .join(target_triple) - .join("debug") - .join(format!("cap{}", std::env::consts::EXE_SUFFIX)), - ); - candidates.push(root.join("target").join("debug").join(format!( - "cap-exporter-{target_triple}{}", - std::env::consts::EXE_SUFFIX - ))); - } - - candidates -} - -fn adjacent_exporter_binary_candidates(dir: &Path) -> Vec { - let mut candidates = Vec::new(); - - candidates.push(dir.join(exporter_bin_name())); - - if let Some(target_triple) = current_target_triple() { - candidates.push(dir.join(format!( - "cap-exporter-{target_triple}{}", - std::env::consts::EXE_SUFFIX - ))); - } - - for subdir in ["../MacOS", "../Resources"] { - candidates.push(dir.join(subdir).join(exporter_bin_name())); - if let Some(target_triple) = current_target_triple() { - candidates.push(dir.join(subdir).join(format!( - "cap-exporter-{target_triple}{}", - std::env::consts::EXE_SUFFIX - ))); - } - } - - candidates -} - -fn current_target_triple() -> Option<&'static str> { - if cfg!(all( - target_os = "windows", - target_arch = "x86_64", - target_env = "msvc" - )) { - Some("x86_64-pc-windows-msvc") - } else if cfg!(all( - target_os = "windows", - target_arch = "aarch64", - target_env = "msvc" - )) { - Some("aarch64-pc-windows-msvc") - } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { - Some("aarch64-apple-darwin") - } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) { - Some("x86_64-apple-darwin") - } else { - None - } -} - -fn exporter_bin_name() -> &'static str { - if cfg!(windows) { - "cap-exporter.exe" - } else { - "cap-exporter" - } -} - -struct ExportActiveGuard<'a>(&'a AtomicBool); - -impl Drop for ExportActiveGuard<'_> { - fn drop(&mut self) { - self.0.store(false, Ordering::Release); - tracing::info!("Resuming editor preview after export"); - } -} - -struct ExportPreviewActiveGuard<'a>(&'a AtomicBool); - -impl<'a> ExportPreviewActiveGuard<'a> { - fn try_new(flag: &'a AtomicBool) -> Result { - flag.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .map(|_| Self(flag)) - .map_err(|_| "Export preview generation is already in progress".to_string()) - } -} - -impl Drop for ExportPreviewActiveGuard<'_> { - fn drop(&mut self) { - self.0.store(false, Ordering::Release); - } -} - -async fn wait_for_export_preview_idle_or_cancel( - flag: &AtomicBool, - cancel_token: &CancellationToken, -) -> Result<(), String> { - while flag.load(Ordering::Acquire) { - tokio::select! { - _ = tokio::time::sleep(std::time::Duration::from_millis(20)) => {} - _ = cancel_token.cancelled() => return Err("Export cancelled".to_string()), - } - } - - Ok(()) -} - -#[derive(Serialize, Deserialize, Clone, Copy, Debug, Type)] -#[serde(tag = "format")] -pub enum ExportSettings { - Mp4(cap_export::mp4::Mp4ExportSettings), - Gif(cap_export::gif::GifExportSettings), - Mov(cap_export::mov::MovExportSettings), -} - -impl ExportSettings { - fn fps(&self) -> u32 { - match self { - ExportSettings::Mp4(settings) => settings.fps, - ExportSettings::Gif(settings) => settings.fps, - ExportSettings::Mov(settings) => settings.fps, - } - } - - fn force_ffmpeg_decoder(&self) -> bool { - match self { - ExportSettings::Mp4(settings) => settings.force_ffmpeg_decoder, - ExportSettings::Gif(_) | ExportSettings::Mov(_) => false, - } - } - - fn cursor_only(&self) -> bool { - match self { - ExportSettings::Mov(settings) => settings.cursor_only, - _ => false, - } - } -} - -fn export_project_config( - project_config: cap_project::ProjectConfiguration, - cursor_only: bool, -) -> cap_project::ProjectConfiguration { - if cursor_only { - make_cursor_only_project(project_config) - } else { - project_config - } -} - -async fn do_export( - project_path: &Path, - settings: &ExportSettings, - progress: ExportProgress, - force_ffmpeg: bool, - cancel_token: CancellationToken, -) -> Result { - if cancel_token.is_cancelled() { - return Err("Export cancelled".to_string()); - } - - let mut exporter_builder = - ExporterBase::builder(project_path.to_path_buf()).with_force_ffmpeg_decoder(force_ffmpeg); - - if settings.cursor_only() { - let meta = RecordingMeta::load_for_project(project_path).map_err(|e| e.to_string())?; - exporter_builder = - exporter_builder.with_config(export_project_config(meta.project_config(), true)); - } - - let exporter_base = exporter_builder.build().await.map_err(|e| e.to_string())?; - - let total_frames = exporter_base.total_frames(settings.fps()); - - if !progress.send(FramesRendered { - rendered_count: 0, - total_frames, - }) { - return Err("Export cancelled".to_string()); - } - - match settings { - ExportSettings::Mp4(mp4_settings) => { - let progress = progress.clone(); - let cancel_token = cancel_token.clone(); - mp4_settings - .export(exporter_base, move |frame_index| { - if cancel_token.is_cancelled() { - return false; - } - - progress.send(FramesRendered { - rendered_count: (frame_index + 1).min(total_frames), - total_frames, - }) - }) - .await - } - ExportSettings::Gif(gif_settings) => { - let progress = progress.clone(); - let cancel_token = cancel_token.clone(); - gif_settings - .export(exporter_base, move |frame_index| { - if cancel_token.is_cancelled() { - return false; - } - - progress.send(FramesRendered { - rendered_count: (frame_index + 1).min(total_frames), - total_frames, - }) - }) - .await - } - ExportSettings::Mov(mov_settings) => { - let progress = progress.clone(); - let cancel_token = cancel_token.clone(); - mov_settings - .export(exporter_base, move |frame_index| { - if cancel_token.is_cancelled() { - return false; - } - - progress.send(FramesRendered { - rendered_count: (frame_index + 1).min(total_frames), - total_frames, - }) - }) - .await - } - } -} - -fn is_frame_decode_error(error: &str) -> bool { - error.contains("Failed to decode video frames") - || error.contains("Too many consecutive frame failures") - || error.contains("waiting for frame 0") -} - -fn should_force_ffmpeg_export(_project_path: &Path, settings: &ExportSettings) -> bool { - settings.force_ffmpeg_decoder() -} - -fn should_force_ffmpeg_preview() -> bool { - false -} - -fn should_start_export_worker_in_software_safe_mode() -> bool { - false -} - -fn should_use_out_of_process_export() -> bool { - cfg!(any(target_os = "macos", target_os = "windows")) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window, progress, editor))] -pub async fn export_video( - window: tauri::Window, - project_path: PathBuf, - progress: tauri::ipc::Channel, - settings: ExportSettings, - editor: OptionalWindowEditorInstance, -) -> Result { - let window_label = window.label().to_string(); - let app = window.app_handle().clone(); - Box::pin(run_export_command(move || async move { - let cancellation_guard = - ExportCancellationGuard::new(next_export_command_id("export"), Some(window_label)); - let resources = export_resource_preflight( - &app, - &project_path, - &settings, - &editor, - None, - &cancellation_guard.token(), - ) - .await?; - export_video_inner( - project_path, - settings, - editor, - ExportProgress(progress), - cancellation_guard.token(), - resources, - ) - .await - })) - .await -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window, progress, editor))] -pub async fn export_video_with_id( - window: tauri::Window, - project_path: PathBuf, - progress: tauri::ipc::Channel, - settings: ExportSettings, - export_id: String, - editor: OptionalWindowEditorInstance, -) -> Result { - let window_label = window.label().to_string(); - let app = window.app_handle().clone(); - Box::pin(run_export_command(move || async move { - let cancellation_guard = ExportCancellationGuard::new(export_id, Some(window_label)); - let resources = export_resource_preflight( - &app, - &project_path, - &settings, - &editor, - None, - &cancellation_guard.token(), - ) - .await?; - export_video_inner( - project_path, - settings, - editor, - ExportProgress(progress), - cancellation_guard.token(), - resources, - ) - .await - })) - .await -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window, progress, editor))] -pub async fn export_video_to_file( - window: tauri::Window, - project_path: PathBuf, - progress: tauri::ipc::Channel, - settings: ExportSettings, - file_name: String, - file_type: String, - editor: OptionalWindowEditorInstance, -) -> Result { - let app = window.app_handle().clone(); - let window_label = window.label().to_string(); - Box::pin(run_export_command(move || async move { - let cancellation_guard = ExportCancellationGuard::new( - next_export_command_id("export-to-file"), - Some(window_label), - ); - export_video_to_file_inner( - app.clone(), - project_path, - settings, - editor, - ExportProgress(progress), - ExportSaveDialogRequest { - #[cfg(not(target_os = "linux"))] - app, - #[cfg(target_os = "linux")] - parent: window, - #[cfg(target_os = "linux")] - cancel_token: cancellation_guard.token(), - file_name, - file_type, - }, - cancellation_guard.token(), - ) - .await - })) - .await -} - -async fn export_video_to_file_inner( - app: tauri::AppHandle, - project_path: PathBuf, - settings: ExportSettings, - editor: OptionalWindowEditorInstance, - progress: ExportProgress, - save_dialog: ExportSaveDialogRequest, - cancel_token: CancellationToken, -) -> Result { - let _session_guard = ExportSessionGuard::new(); - let Some(save_path) = show_export_save_dialog(save_dialog).await? else { - return Err("Save dialog cancelled".to_string()); - }; - - let destination = match &save_path { - ExportSaveDestination::Selected(path) => Some(path.as_path()), - #[cfg(any(target_os = "macos", test))] - ExportSaveDestination::Default => None, - }; - let resources = export_resource_preflight( - &app, - &project_path, - &settings, - &editor, - destination, - &cancel_token, - ) - .await?; - let output_path = export_video_inner( - project_path, - settings, - editor, - progress, - cancel_token, - resources, - ) - .await?; - match save_path { - ExportSaveDestination::Selected(path) => { - copy_export_to_path(&output_path, &path).await?; - Ok(path) - } - #[cfg(any(target_os = "macos", test))] - ExportSaveDestination::Default => Ok(output_path), - } -} - -async fn export_resource_preflight( - app: &tauri::AppHandle, - project_path: &Path, - settings: &ExportSettings, - editor: &OptionalWindowEditorInstance, - destination: Option<&Path>, - cancel: &CancellationToken, -) -> Result { - if cancel.is_cancelled() { - return Err("Export cancelled".into()); - } - let estimate = editor - .as_ref() - .map(|editor| { - let duration = export_estimate_duration( - &editor.project_config.1.borrow(), - editor.recordings.duration(), - ); - estimated_working_bytes( - export_estimates_for_duration(settings, duration).estimated_size_mb, - ) - }) - .unwrap_or(0); - let mut disks = vec![DiskBudget { - path: project_path.to_path_buf(), - description: "your recording drive", - estimated_bytes: estimate, - }]; - if let Some(path) = destination { - disks.push(DiskBudget { - path: path.to_path_buf(), - description: "your export drive", - estimated_bytes: estimate, - }); - } - let resources = ExportResources::new(disks); - if let Some(message) = resources.check().await? { - if cancel.is_cancelled() { - return Err("Export cancelled".into()); - } - let (sender, receiver) = tokio::sync::oneshot::channel(); - app.dialog() - .message(message) - .title("Check export resources") - .kind(MessageDialogKind::Warning) - .buttons(MessageDialogButtons::OkCancelCustom( - "Proceed anyway".into(), - "Go back".into(), - )) - .show(move |confirmed| { - let _ = sender.send(confirmed); - }); - let confirmed = tokio::select! { - biased; - _ = cancel.cancelled() => false, - confirmed = receiver => confirmed.unwrap_or(false), - }; - if !confirmed { - return Err("Export cancelled".into()); - } - let _ = resources.check().await?; - } - if cancel.is_cancelled() { - return Err("Export cancelled".into()); - } - Ok(resources) -} - -async fn export_video_inner( - project_path: PathBuf, - settings: ExportSettings, - editor: OptionalWindowEditorInstance, - progress: ExportProgress, - cancel_token: CancellationToken, - resources: ExportResources, -) -> Result { - let stopped = cancel_token.child_token(); - resources - .supervise( - export_video_attempts(project_path, settings, editor, progress, stopped.clone()), - || stopped.cancel(), - ) - .await -} - -async fn export_video_attempts( - project_path: PathBuf, - settings: ExportSettings, - editor: OptionalWindowEditorInstance, - progress: ExportProgress, - cancel_token: CancellationToken, -) -> Result { - let _session_guard = ExportSessionGuard::new(); - let force_ffmpeg = should_force_ffmpeg_export(&project_path, &settings); - info!( - project_path = %project_path.display(), - force_ffmpeg, - settings = ?settings, - "Starting export" - ); - - let _guard = if let Some(ref ed) = *editor { - ed.export_active.store(true, Ordering::Release); - tracing::info!("Pausing editor preview during export"); - Some(ExportActiveGuard(&ed.export_active)) - } else { - None - }; - - if let Some(ref ed) = *editor { - wait_for_export_preview_idle_or_cancel(&ed.export_preview_active, &cancel_token).await?; - } - - let result = if should_use_out_of_process_export() { - run_out_of_process_export( - &project_path, - &settings, - progress.clone(), - force_ffmpeg, - cancel_token.clone(), - ) - .await - } else { - run_protected_export( - &project_path, - &settings, - progress.clone(), - force_ffmpeg, - cancel_token.clone(), - ) - .await - }; - - match result { - Ok(path) => { - info!("Exported to {} completed", path.display()); - Ok(path) - } - Err(e) if !force_ffmpeg && is_frame_decode_error(&e) => { - if cancel_token.is_cancelled() { - return Err("Export cancelled".to_string()); - } - - info!( - "Export failed with frame decode error, retrying with FFmpeg decoder: {}", - e - ); - - let retry_result = if should_use_out_of_process_export() { - run_out_of_process_export( - &project_path, - &settings, - progress, - true, - cancel_token.clone(), - ) - .await - } else { - run_protected_export( - &project_path, - &settings, - progress, - true, - cancel_token.clone(), - ) - .await - }; - - match retry_result { - Ok(path) => { - info!( - "Export succeeded with FFmpeg decoder fallback: {}", - path.display() - ); - Ok(path) - } - Err(retry_e) => { - if cancel_token.is_cancelled() || retry_e == "Export cancelled" { - return Err("Export cancelled".to_string()); - } - - sentry::capture_message(&retry_e, sentry::Level::Error); - Err(retry_e) - } - } - } - Err(e) if cancel_token.is_cancelled() || e == "Export cancelled" => { - Err("Export cancelled".to_string()) - } - Err(e) => { - sentry::capture_message(&e, sentry::Level::Error); - Err(e) - } - } -} - -async fn show_export_save_dialog( - request: ExportSaveDialogRequest, -) -> Result, String> { - let ExportSaveDialogRequest { - #[cfg(not(target_os = "linux"))] - app, - #[cfg(target_os = "linux")] - parent, - #[cfg(target_os = "linux")] - cancel_token, - file_name, - file_type, - } = request; - info!(file_name, file_type, "Save file dialog requested"); - - let (name, extension) = match file_type.as_str() { - "mp4" => ("MP4 Video", "mp4"), - "gif" => ("GIF Image", "gif"), - "mov" => ("MOV Video", "mov"), - _ => { - warn!(file_type, "Invalid export save file dialog type"); - return Err("Invalid file type".to_string()); - } - }; - - info!(file_name, name, extension, "Showing save file dialog"); - - #[cfg(target_os = "linux")] - let result = show_linux_save_dialog(parent, cancel_token, file_name, name, extension) - .await - .map(|path| path.map(ExportSaveDestination::Selected)); - #[cfg(target_os = "macos")] - let result = Ok(resolve_macos_export_destination( - show_macos_save_dialog(&app, file_name, extension).await, - )); - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - let result = { - 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(|error| error.to_string()) - .map(|path| path.map(ExportSaveDestination::Selected)) - }; - result.inspect(|result| { - info!(path = ?result, "Save file dialog completed"); - }) -} - -#[cfg(target_os = "macos")] -pub(crate) async fn show_macos_save_dialog( - app: &tauri::AppHandle, - file_name: String, - extension: &'static str, -) -> Result, String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - app.run_on_main_thread(move || { - crate::macos_save_panel::show(&file_name, &[extension], move |result| { - let _ = tx.send(result); - }); - }) - .map_err(|error| format!("Unable to show save dialog: {error}"))?; - rx.await - .map_err(|error| format!("Save dialog stopped before completing: {error}"))? -} - -#[cfg(target_os = "linux")] -pub(crate) async fn show_linux_save_dialog( - parent: tauri::Window, - cancel_token: CancellationToken, - file_name: String, - name: &'static str, - extension: &'static str, -) -> Result, String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let window = parent.clone(); - parent - .run_on_main_thread(move || { - use gtk::prelude::*; - - let prepared = window - .gtk_window() - .map_err(|error| format!("Export window is unavailable: {error}")) - .map(|parent| parent.upcast::()) - .and_then(|parent| { - create_linux_export_save_dialog(&parent, &file_name, name, extension) - .map(|dialog| (parent, dialog)) - }); - match prepared { - Ok((parent, dialog)) => { - run_linux_export_save_dialog(parent, dialog, cancel_token, tx); - } - Err(error) => { - let _ = tx.send(Err(error)); - } - } - }) - .map_err(|error| format!("Unable to show export save dialog: {error}"))?; - rx.await.map_err(|error| error.to_string())? -} - -#[cfg(target_os = "linux")] -fn create_linux_export_save_dialog( - parent: >k::Window, - file_name: &str, - filter_name: &str, - extension: &str, -) -> Result { - use gtk::prelude::*; - - if parent.in_destruction() || !parent.is_mapped() { - return Err("Export window is no longer available".to_string()); - } - - // rfd's GTK backend discards its parent, so tiled compositors can clip the save controls. - let dialog = gtk::FileChooserNative::new( - Some("Save File"), - Some(parent), - gtk::FileChooserAction::Save, - None, - None, - ); - dialog.set_do_overwrite_confirmation(true); - if !file_name.contains('\0') { - dialog.set_current_name(file_name); - } - let filter = gtk::FileFilter::new(); - filter.set_name(Some(filter_name)); - filter.add_pattern(&format!("*.{extension}")); - dialog.add_filter(filter); - Ok(dialog) -} - -#[cfg(target_os = "linux")] -fn run_linux_export_save_dialog( - parent: gtk::Window, - dialog: gtk::FileChooserNative, - cancel_token: CancellationToken, - mut result_tx: tokio::sync::oneshot::Sender, String>>, -) { - use gtk::prelude::*; - - let (destroy_tx, destroyed) = tokio::sync::oneshot::channel(); - let destroy_tx = std::cell::Cell::new(Some(destroy_tx)); - let parent_destroyed = std::rc::Rc::new(std::cell::Cell::new(false)); - let destroyed_flag = parent_destroyed.clone(); - let destroy_handler = parent.connect_destroy(move |_| { - destroyed_flag.set(true); - if let Some(sender) = destroy_tx.take() { - let _ = sender.send(()); - } - }); - std::mem::drop(gtk::glib::MainContext::default().spawn_local(async move { - let result = tokio::select! { - biased; - _ = cancel_token.cancelled() => Err("Export cancelled".to_string()), - _ = result_tx.closed() => Err("Export cancelled".to_string()), - _ = destroyed => Err("Export window was closed".to_string()), - response = dialog.run_future() => { - Ok(if response == gtk::ResponseType::Accept { - dialog.filename() - } else { - None - }) - } - }; - if !parent_destroyed.get() { - parent.disconnect(destroy_handler); - } - dialog.destroy(); - let _ = result_tx.send(result); - })); -} - -async fn copy_export_to_path(src: &Path, dst: &Path) -> Result<(), String> { - info!( - src = %src.display(), - dst = %dst.display(), - "Copying exported video to selected path" - ); - - if let Some(parent) = dst.parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|e| format!("Failed to create export target directory: {e}"))?; - } - - let bytes = tokio::fs::copy(src, dst) - .await - .map_err(|e| format!("Failed to copy exported file: {e}"))?; - - let src_size = tokio::fs::metadata(src) - .await - .map_err(|e| format!("Failed to read exported file metadata: {e}"))? - .len(); - - if bytes != src_size { - let _ = tokio::fs::remove_file(dst).await; - return Err(format!( - "Export copy verification failed: copied {bytes} bytes but source is {src_size} bytes" - )); - } - - info!(bytes, dst = %dst.display(), "Copied exported video to selected path"); - Ok(()) -} - -#[derive(Debug, serde::Serialize, specta::Type)] -pub struct ExportEstimates { - pub duration_seconds: f64, - pub estimated_time_seconds: f64, - pub estimated_size_mb: f64, -} - -static EXPORT_ESTIMATE_REQUESTS: LazyLock = - LazyLock::new(estimate_request::EstimateRequests::default); - -#[tauri::command] -#[specta::specta] -pub fn cancel_export_estimates(editor: WindowEditorInstance) { - EXPORT_ESTIMATE_REQUESTS.cancel(&editor.project_path); -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor, on_estimate))] -pub async fn get_export_estimates( - path: PathBuf, - settings: ExportSettings, - on_estimate: tauri::ipc::Channel, - editor: WindowEditorInstance, -) -> Result { - if path != editor.project_path { - return Err("Export estimate does not match the open project".into()); - } - EXPORT_ESTIMATE_REQUESTS - .run(path.clone(), |cancel| async move { - let project = load_export_preview_config(path, settings.cursor_only()).await?; - let settings = match settings { - ExportSettings::Mp4(settings) => { - cap_export::settings::ExportSettings::Mp4(settings) - } - ExportSettings::Gif(settings) => { - cap_export::settings::ExportSettings::Gif(settings) - } - ExportSettings::Mov(settings) => { - cap_export::settings::ExportSettings::Mov(settings) - } - }; - cap_export::estimates::estimate_export( - (**editor).clone(), - project, - settings, - cancel, - move |estimate| { - let _ = on_estimate.send(estimate); - }, - ) - .await - }) - .await -} - -fn export_estimates_for_duration( - settings: &ExportSettings, - duration_seconds: f64, -) -> ExportEstimates { - let (resolution, fps) = match settings { - ExportSettings::Mp4(s) => (s.resolution_base, s.fps), - ExportSettings::Gif(s) => (s.resolution_base, s.fps), - ExportSettings::Mov(s) => (s.resolution_base, s.fps), - }; - - let (width, height) = (resolution.x, resolution.y); - let total_pixels = (width * height) as f64; - let fps_f64 = fps as f64; - let total_frames = (duration_seconds * fps_f64).ceil(); - - let (estimated_size_mb, estimated_time_seconds) = match &settings { - ExportSettings::Mp4(mp4_settings) => { - let bits_per_pixel = mp4_settings.effective_bpp() as f64; - let effective_fps = ((fps_f64 - 30.0).max(0.0) * 0.6) + fps_f64.min(30.0); - let video_bitrate = total_pixels * bits_per_pixel * effective_fps; - let audio_bitrate = 192_000.0; - let total_bitrate = video_bitrate + audio_bitrate; - let encoder_efficiency = 0.5; - let size_mb = - (total_bitrate * encoder_efficiency * duration_seconds) / (8.0 * 1024.0 * 1024.0); - - let effective_render_fps = match (width, height) { - (w, _) if w >= 3840 => 175.0, - _ => 290.0, - }; - let time_estimate = total_frames / effective_render_fps; - - (size_mb, time_estimate) - } - ExportSettings::Gif(_) => { - let bytes_per_frame = total_pixels * 0.5; - let gif_efficiency = 0.07; - let size_mb = (bytes_per_frame * gif_efficiency * total_frames) / (1024.0 * 1024.0); - - let frames_per_sec = match (width, height) { - (w, h) if w <= 1280 && h <= 720 => 10.0, - (w, h) if w <= 1920 && h <= 1080 => 5.0, - _ => 2.0, - }; - let time_estimate = total_frames / frames_per_sec; - - (size_mb, time_estimate) - } - ExportSettings::Mov(_) => { - let size_mb = estimate_cursor_only_size_mb(total_pixels, total_frames); - let effective_render_fps = match (width, height) { - (w, _) if w >= 3840 => 140.0, - _ => 220.0, - }; - let time_estimate = total_frames / effective_render_fps; - - (size_mb, time_estimate) - } - }; - - ExportEstimates { - duration_seconds, - estimated_time_seconds, - estimated_size_mb, - } -} - -#[derive(Debug, Serialize, Deserialize, Type)] -pub struct ExportPreviewSettings { - pub fps: u32, - pub resolution_base: XY, - pub compression_bpp: f32, - #[serde(default)] - pub cursor_only: bool, -} - -#[derive(Debug, Serialize, Deserialize, Type)] -pub struct ExportPreviewResult { - pub jpeg_base64: String, - pub estimated_size_mb: f64, - pub actual_width: u32, - pub actual_height: u32, - pub frame_render_time_ms: f64, - pub total_frames: u32, -} - -fn estimate_cursor_only_size_mb(total_pixels: f64, total_frames: f64) -> f64 { - let bytes_per_frame = total_pixels * 0.4; - (bytes_per_frame * total_frames) / (1024.0 * 1024.0) -} - -fn export_estimate_duration( - project_config: &cap_project::ProjectConfiguration, - source_duration: f64, -) -> f64 { - project_config - .timeline - .as_ref() - .map(cap_project::TimelineConfiguration::duration) - .unwrap_or(source_duration) -} - -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] -#[specta::specta] -#[instrument(skip_all)] -pub async fn generate_export_preview( - project_path: PathBuf, - frame_time: f64, - settings: ExportPreviewSettings, -) -> Result { - match AssertUnwindSafe(generate_export_preview_inner( - project_path, - frame_time, - settings, - )) - .catch_unwind() - .await - { - Ok(result) => result, - Err(panic) => { - let panic_msg = panic_message(panic); - error!( - target: "cap_desktop_export", - panic = %panic_msg, - "generate_export_preview panicked" - ); - sentry::capture_message( - &format!("Export preview panicked: {panic_msg}"), - sentry::Level::Error, - ); - Err("Export preview failed unexpectedly".to_string()) - } - } -} - -#[instrument(skip_all)] -async fn generate_export_preview_inner( - project_path: PathBuf, - frame_time: f64, - settings: ExportPreviewSettings, -) -> Result { - use base64::{Engine, engine::general_purpose::STANDARD}; - use cap_editor::create_segments; - use std::time::Instant; - - let recording_meta = RecordingMeta::load_for_project(&project_path) - .map_err(|e| format!("Failed to load recording meta: {e}"))?; - - let cap_project::RecordingMetaInner::Studio(studio_meta) = &recording_meta.inner else { - return Err("Cannot preview non-studio recordings".to_string()); - }; - - let mut project_config = - export_project_config(recording_meta.project_config(), settings.cursor_only); - - let recordings = Arc::new( - ProjectRecordingsMeta::new(&recording_meta.project_path, studio_meta) - .map_err(|e| format!("Failed to load recordings: {e}"))?, - ); - - synchronize_preview_timing( - &recording_meta, - &mut project_config, - &recordings - .segments - .iter() - .map(|segment| segment.display.duration) - .collect::>(), - ); - let project_config = Arc::new(project_config); - - let render_constants = Arc::new( - RenderVideoConstants::new( - &recordings.segments, - recording_meta.clone(), - (**studio_meta).clone(), - ) - .await - .map_err(|e| format!("Failed to create render constants: {e}"))?, - ); - - let force_ffmpeg = should_force_ffmpeg_preview(); - info!( - project_path = %project_path.display(), - force_ffmpeg, - "Starting export preview" - ); - - let segments = create_segments(&recording_meta, studio_meta, force_ffmpeg) - .await - .map_err(|e| format!("Failed to create segments: {e}"))?; - - let render_segments: Vec = segments - .iter() - .map(|s| RenderSegment { - cursor: s.cursor.clone(), - keyboard: s.keyboard.clone(), - decoders: s.decoders.clone(), - render_display: !settings.cursor_only, - }) - .collect(); - - let transition_mapping = project_config.timeline.as_ref().and_then(|timeline| { - if timeline.transitions.is_empty() { - return None; - } - match timeline.get_frame_mapping(frame_time) { - Some(TimelineFrameMapping::Transition { - outgoing, - kind, - progress, - .. - }) => Some((outgoing, kind, progress)), - _ => None, - } - }); - - let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { - return Err("Frame time is outside video duration".to_string()); - }; - - let render_segment = &render_segments[segment.recording_clip as usize]; - let clip_config = project_config - .clips - .iter() - .find(|v| v.index == segment.recording_clip); - - let render_start = Instant::now(); - - let segment_frames = render_segment - .decoders - .get_frames( - segment_time as f32, - !project_config.camera.hide && render_segment.render_display, - render_segment.render_display, - clip_config.map(|v| v.offsets).unwrap_or_default(), - ) - .await - .ok_or_else(|| "Failed to decode frame".to_string())?; - - let frame_number = (frame_time * settings.fps as f64).floor() as u32; - let total_duration = project_config - .timeline - .as_ref() - .map(|t| t.duration()) - .unwrap_or(0.0); - - let mut zoom_timeline = ZoomTransformTimeline::from_project_for_clip( - &project_config, - &render_segment.cursor, - total_duration, - render_constants.options.screen_size, - segment.recording_clip, - ); - zoom_timeline.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); - - let uniforms = ProjectUniforms::new( - &render_constants, - &project_config, - frame_number, - settings.fps, - settings.resolution_base, - &render_segment.cursor, - &segment_frames, - total_duration, - &zoom_timeline, - ); - - let mut frame_renderer = FrameRenderer::new(&render_constants); - let mut layers = RendererLayers::new_with_options( - &render_constants.device, - &render_constants.queue, - render_constants.is_software_adapter, - ); - - let frame = if let Some((outgoing, kind, progress)) = transition_mapping { - let outgoing_segment = &render_segments[outgoing.segment.recording_clip as usize]; - let outgoing_offsets = project_config - .clips - .iter() - .find(|clip| clip.index == outgoing.segment.recording_clip) - .map(|clip| clip.offsets) - .unwrap_or_default(); - let outgoing_frames = outgoing_segment - .decoders - .get_frames( - outgoing.source_time as f32, - !project_config.camera.hide && outgoing_segment.render_display, - outgoing_segment.render_display, - outgoing_offsets, - ) - .await - .ok_or_else(|| "Failed to decode outgoing frame".to_string())?; - let mut outgoing_zoom = ZoomTransformTimeline::from_project_for_outgoing_clip( - &project_config, - &outgoing_segment.cursor, - total_duration, - render_constants.options.screen_size, - outgoing.segment.recording_clip, - ); - outgoing_zoom.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); - let outgoing_uniforms = ProjectUniforms::new( - &render_constants, - &project_config, - frame_number, - settings.fps, - settings.resolution_base, - &outgoing_segment.cursor, - &outgoing_frames, - total_duration, - &outgoing_zoom, - ); - frame_renderer - .render_transition_immediate( - TransitionRenderInput { - segment_frames: outgoing_frames, - uniforms: outgoing_uniforms, - cursor: &outgoing_segment.cursor, - render_display: outgoing_segment.render_display, - }, - TransitionRenderInput { - segment_frames, - uniforms, - cursor: &render_segment.cursor, - render_display: render_segment.render_display, - }, - kind, - progress as f32, - &mut layers, - ) - .await - } else { - frame_renderer - .render_immediate( - segment_frames, - uniforms, - &render_segment.cursor, - render_segment.render_display, - &mut layers, - ) - .await - } - .map_err(|e| format!("Failed to render frame: {e}"))?; - - let frame_render_time_ms = render_start.elapsed().as_secs_f64() * 1000.0; - - let width = frame.width; - let height = frame.height; - - let rgb_data: Vec = frame - .data - .chunks(frame.padded_bytes_per_row as usize) - .flat_map(|row| { - row[0..(frame.width * 4) as usize] - .chunks(4) - .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) - }) - .collect(); - - let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp); - let mut jpeg_buffer = Vec::new(); - { - let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality); - encoder - .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8) - .map_err(|e| format!("Failed to encode JPEG: {e}"))?; - } - - let jpeg_base64 = STANDARD.encode(&jpeg_buffer); - - let total_pixels = (settings.resolution_base.x * settings.resolution_base.y) as f64; - let fps_f64 = settings.fps as f64; - - let metadata = get_video_metadata(project_path.clone()).await?; - let duration_seconds = export_estimate_duration(&project_config, metadata.duration); - let total_frames = (duration_seconds * fps_f64).ceil() as u32; - - let estimated_size_mb = if settings.cursor_only { - let total_frames_f64 = (duration_seconds * fps_f64).ceil(); - estimate_cursor_only_size_mb(total_pixels, total_frames_f64) - } else { - let effective_fps = ((fps_f64 - 30.0).max(0.0) * 0.6) + fps_f64.min(30.0); - let video_bitrate = total_pixels * settings.compression_bpp as f64 * effective_fps; - let audio_bitrate = 192_000.0; - let total_bitrate = video_bitrate + audio_bitrate; - let encoder_efficiency = 0.5; - (total_bitrate * encoder_efficiency * duration_seconds) / (8.0 * 1024.0 * 1024.0) - }; - - Ok(ExportPreviewResult { - jpeg_base64, - estimated_size_mb, - actual_width: width, - actual_height: height, - frame_render_time_ms, - total_frames, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - #[test] - fn native_save_failure_uses_the_rendered_project_output() { - assert_eq!( - resolve_macos_export_destination(Err("NSSavePanel unavailable".to_string())), - Some(ExportSaveDestination::Default), - ); - } - - #[test] - fn cancelling_a_native_save_dialog_does_not_export() { - assert_eq!(resolve_macos_export_destination(Ok(None)), None); - } - - #[test] - fn native_save_selection_keeps_the_requested_destination() { - let path = PathBuf::from("selected.mp4"); - assert_eq!( - resolve_macos_export_destination(Ok(Some(path.clone()))), - Some(ExportSaveDestination::Selected(path)), - ); - } - - #[tokio::test] - async fn export_preview_reads_saved_caption_choice_after_stale_live_preview_update() { - for export_with_subtitles in [false, true] { - let dir = tempdir().unwrap(); - let mut saved = cap_project::ProjectConfiguration { - captions: Some(cap_project::CaptionsData { - settings: cap_project::CaptionSettings { - enabled: true, - export_with_subtitles, - ..Default::default() - }, - ..Default::default() - }), - ..Default::default() - }; - saved.write(dir.path()).unwrap(); - let (sender, receiver) = tokio::sync::watch::channel(saved.clone()); - saved - .captions - .as_mut() - .unwrap() - .settings - .export_with_subtitles = !export_with_subtitles; - saved.captions.as_mut().unwrap().settings.enabled = false; - sender.send(saved).unwrap(); - - let preview = load_export_preview_config(dir.path().to_path_buf(), false) - .await - .unwrap(); - let settings = preview.captions.unwrap().settings; - assert!(settings.enabled); - assert_eq!(settings.export_with_subtitles, export_with_subtitles); - assert!( - !receiver - .borrow() - .captions - .as_ref() - .unwrap() - .settings - .enabled - ); - let persisted = cap_project::ProjectConfiguration::load(dir.path()).unwrap(); - assert!(persisted.captions.as_ref().unwrap().settings.enabled); - } - } - - #[tokio::test] - async fn export_preview_reports_missing_or_malformed_saved_config() { - let dir = tempdir().unwrap(); - let missing = load_export_preview_config(dir.path().to_path_buf(), false) - .await - .unwrap_err(); - assert!(missing.contains("Failed to read saved project config")); - std::fs::write(dir.path().join("project-config.json"), "{not valid json").unwrap(); - let malformed = load_export_preview_config(dir.path().to_path_buf(), false) - .await - .unwrap_err(); - assert!(malformed.contains("Failed to read saved project config")); - } - - #[tokio::test] - async fn saved_export_preview_projects_captions_after_a_cut() { - let directory = tempdir().unwrap(); - let mut meta: RecordingMeta = serde_json::from_value(serde_json::json!({ - "pretty_name":"preview", "segments":[], "cursors":{} - })) - .unwrap(); - meta.project_path = directory.path().to_path_buf(); - let config: cap_project::ProjectConfiguration = serde_json::from_value(serde_json::json!({ - "timeline": { - "segments":[ - {"start":0.0,"end":5.0,"timescale":1.0}, - {"start":6.0,"end":10.0,"timescale":1.0} - ], "zoomSegments":[] - }, - "captions": { - "sourceTimed":true, "settings":{}, - "segments":[{"id":"word","text":"retained","start":8.0,"end":8.5,"words":[]}] - } - })) - .unwrap(); - config.write(directory.path()).unwrap(); - let mut preview = load_export_preview_config(directory.path().to_path_buf(), false) - .await - .unwrap(); - synchronize_preview_timing(&meta, &mut preview, &[10.0]); - let caption = &preview.timeline.as_ref().unwrap().caption_segments[0]; - assert_eq!(caption.start, 7.0); - assert_eq!(caption.end, 7.5); - assert_eq!(caption.text, "retained"); - let mut cursor_only = load_export_preview_config(directory.path().to_path_buf(), true) - .await - .unwrap(); - synchronize_preview_timing(&meta, &mut cursor_only, &[10.0]); - assert!(cursor_only.captions.is_none()); - } - - #[test] - fn export_preview_rejects_unloaded_incoming_and_outgoing_media() { - let medias = ["first", "second"]; - assert_eq!(export_preview_media(&medias, 0).unwrap(), &"first"); - assert_eq!(export_preview_media(&medias, 1).unwrap(), &"second"); - assert!(export_preview_media(&medias, 2).is_err()); - assert!(export_preview_media(&medias, u32::MAX).is_err()); - assert!(export_preview_media::<()>(&[], 0).is_err()); - } - - #[test] - fn export_preview_caption_policy_preserves_editor_and_cursor_only_states() { - for enabled in [false, true] { - for export in [false, true] { - for cursor_only in [false, true] { - let editor = cap_project::ProjectConfiguration { - captions: Some(cap_project::CaptionsData { - settings: cap_project::CaptionSettings { - enabled, - export_with_subtitles: export, - ..Default::default() - }, - ..Default::default() - }), - ..Default::default() - }; - let original = serde_json::to_value(&editor).unwrap(); - let preview = export_project_config(editor.clone(), cursor_only); - if cursor_only { - assert!(preview.captions.is_none()); - } else { - assert_eq!(preview.captions.unwrap().settings.enabled, enabled); - } - assert_eq!(serde_json::to_value(editor).unwrap(), original); - } - } - } - } - - #[test] - fn export_estimates_use_source_duration_without_a_timeline() { - assert_eq!( - export_estimate_duration(&cap_project::ProjectConfiguration::default(), 361.0), - 361.0 - ); - } - - #[test] - fn export_estimates_follow_trims_and_playback_speed() { - for (timescale, expected) in [(1.0, 12.0), (2.0, 6.0)] { - let project = serde_json::from_value(serde_json::json!({ - "timeline": { - "segments": [{"start": 45.0, "end": 57.0, "timescale": timescale}], - "zoomSegments": [] - } - })) - .unwrap(); - - assert_eq!(export_estimate_duration(&project, 361.0), expected); - } - } - - #[test] - fn export_estimates_include_transition_overlap() { - let project = serde_json::from_value(serde_json::json!({ - "timeline": { - "segments": [ - {"start": 5.0, "end": 11.0, "timescale": 1.0}, - {"start": 20.0, "end": 26.0, "timescale": 1.0} - ], - "transitions": [{"segmentIndex": 1, "type": "cross-fade", "duration": 1.0}], - "zoomSegments": [] - } - })) - .unwrap(); - - assert_eq!(export_estimate_duration(&project, 361.0), 11.0); - } - - #[test] - fn export_settings_exposes_force_ffmpeg_for_mp4_only() { - let mp4_settings = ExportSettings::Mp4(cap_export::mp4::Mp4ExportSettings { - fps: 30, - resolution_base: XY { x: 1280, y: 720 }, - compression: cap_export::mp4::ExportCompression::Web, - custom_bpp: None, - force_ffmpeg_decoder: true, - optimize_filesize: false, - }); - let gif_settings = ExportSettings::Gif(cap_export::gif::GifExportSettings { - fps: 15, - resolution_base: XY { x: 1280, y: 720 }, - quality: None, - }); - - assert!(mp4_settings.force_ffmpeg_decoder()); - assert!(!gif_settings.force_ffmpeg_decoder()); - } - - #[test] - fn frame_decode_error_matcher_includes_initial_frame_timeout() { - assert!(is_frame_decode_error( - "Export timed out 3 times consecutively after 120s each waiting for frame 0" - )); - } - - #[cfg(not(target_os = "windows"))] - #[test] - fn recovered_projects_do_not_force_ffmpeg_without_explicit_setting() { - let dir = tempdir().unwrap(); - std::fs::write( - dir.path() - .join(crate::recording::FRAGMENTED_EXPORT_FFMPEG_MARKER), - b"fragmented-remux", - ) - .unwrap(); - - let gif_settings = ExportSettings::Gif(cap_export::gif::GifExportSettings { - fps: 15, - resolution_base: XY { x: 1280, y: 720 }, - quality: None, - }); - - assert!(!should_force_ffmpeg_export(dir.path(), &gif_settings)); - } - - #[test] - fn exports_do_not_force_ffmpeg_without_explicit_setting() { - let dir = tempdir().unwrap(); - - let gif_settings = ExportSettings::Gif(cap_export::gif::GifExportSettings { - fps: 15, - resolution_base: XY { x: 1280, y: 720 }, - quality: None, - }); - - assert!(!should_force_ffmpeg_export(dir.path(), &gif_settings)); - } - - #[test] - fn export_worker_starts_in_hardware_mode() { - assert!(!should_start_export_worker_in_software_safe_mode()); - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip_all)] -pub async fn generate_export_preview_fast( - editor: WindowEditorInstance, - frame_time: f64, - settings: ExportPreviewSettings, -) -> Result { - match AssertUnwindSafe(generate_export_preview_fast_inner( - editor, frame_time, settings, - )) - .catch_unwind() - .await - { - Ok(result) => result, - Err(panic) => { - let panic_msg = panic_message(panic); - error!( - target: "cap_desktop_export", - panic = %panic_msg, - "generate_export_preview_fast panicked" - ); - sentry::capture_message( - &format!("Export preview panicked: {panic_msg}"), - sentry::Level::Error, - ); - Err("Export preview failed unexpectedly".to_string()) - } - } -} - -async fn load_export_preview_config( - project_path: PathBuf, - cursor_only: bool, -) -> Result { - tokio::task::spawn_blocking(move || { - cap_project::ProjectConfiguration::load(&project_path) - .map(|config| export_project_config(config, cursor_only)) - .map_err(|error| { - format!("Failed to read saved project config for export preview: {error}") - }) - }) - .await - .map_err(|error| format!("Failed to load export preview config: {error}"))? -} - -fn synchronize_preview_timing( - meta: &RecordingMeta, - project: &mut cap_project::ProjectConfiguration, - display_durations: &[f64], -) { - cap_project::synchronize_legacy_keyboard(meta, project); - cap_project::synchronize_captions(project, display_durations); -} - -fn export_preview_media(medias: &[T], recording_clip: u32) -> Result<&T, String> { - medias.get(recording_clip as usize).ok_or_else(|| { - format!( - "Recording clip {recording_clip} is unavailable in this editor. Reopen the project before exporting." - ) - }) -} - -#[instrument(skip_all)] -async fn generate_export_preview_fast_inner( - editor: WindowEditorInstance, - frame_time: f64, - settings: ExportPreviewSettings, -) -> Result { - if editor.export_active.load(Ordering::Acquire) { - return Err("Export is in progress - preview generation skipped".to_string()); - } - - use base64::{Engine, engine::general_purpose::STANDARD}; - use std::time::Instant; - - let _preview_guard = ExportPreviewActiveGuard::try_new(&editor.export_preview_active)?; - - crate::gpu_context::ensure_shared_device_available()?; - - let mut project_config = - load_export_preview_config(editor.project_path.clone(), settings.cursor_only).await?; - let meta = editor.meta().clone(); - let recordings = editor.recordings.clone(); - let project_config = tokio::task::spawn_blocking(move || { - synchronize_preview_timing( - &meta, - &mut project_config, - &recordings - .segments - .iter() - .map(|segment| segment.display.duration) - .collect::>(), - ); - project_config - }) - .await - .map_err(|error| format!("Failed to synchronize export preview timing: {error}"))?; - let project_config = Arc::new(project_config); - let transition_mapping = project_config.timeline.as_ref().and_then(|timeline| { - if timeline.transitions.is_empty() { - return None; - } - match timeline.get_frame_mapping(frame_time) { - Some(TimelineFrameMapping::Transition { - outgoing, - kind, - progress, - .. - }) => Some((outgoing, kind, progress)), - _ => None, - } - }); - - let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { - return Err("Frame time is outside video duration".to_string()); - }; - - let segment_media = export_preview_media(&editor.segment_medias, segment.recording_clip)?; - let clip_config = project_config - .clips - .iter() - .find(|v| v.index == segment.recording_clip); - - let render_start = Instant::now(); - - let segment_frames = segment_media - .decoders - .get_frames( - segment_time as f32, - !project_config.camera.hide && !settings.cursor_only, - !settings.cursor_only, - clip_config.map(|v| v.offsets).unwrap_or_default(), - ) - .await; - let segment_frames = segment_frames.ok_or_else(|| "Failed to decode frame".to_string())?; - - let frame_number = (frame_time * settings.fps as f64).floor() as u32; - let total_duration = project_config - .timeline - .as_ref() - .map(|t| t.duration()) - .unwrap_or(0.0); - - let mut zoom_timeline = ZoomTransformTimeline::from_project_for_clip( - &project_config, - &segment_media.cursor, - total_duration, - editor.render_constants.options.screen_size, - segment.recording_clip, - ); - zoom_timeline.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); - - let uniforms = ProjectUniforms::new( - &editor.render_constants, - &project_config, - frame_number, - settings.fps, - settings.resolution_base, - &segment_media.cursor, - &segment_frames, - total_duration, - &zoom_timeline, - ); - - crate::gpu_context::ensure_shared_device_available()?; - let mut frame_renderer = FrameRenderer::new(&editor.render_constants); - let mut layers = RendererLayers::new_with_options( - &editor.render_constants.device, - &editor.render_constants.queue, - editor.render_constants.is_software_adapter, - ); - - let frame = if let Some((outgoing, kind, progress)) = transition_mapping { - let outgoing_media = - export_preview_media(&editor.segment_medias, outgoing.segment.recording_clip)?; - let outgoing_offsets = project_config - .clips - .iter() - .find(|clip| clip.index == outgoing.segment.recording_clip) - .map(|clip| clip.offsets) - .unwrap_or_default(); - let outgoing_frames = outgoing_media - .decoders - .get_frames( - outgoing.source_time as f32, - !project_config.camera.hide && !settings.cursor_only, - !settings.cursor_only, - outgoing_offsets, - ) - .await - .ok_or_else(|| "Failed to decode outgoing frame".to_string())?; - let mut outgoing_zoom = ZoomTransformTimeline::from_project_for_outgoing_clip( - &project_config, - &outgoing_media.cursor, - total_duration, - editor.render_constants.options.screen_size, - outgoing.segment.recording_clip, - ); - outgoing_zoom.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); - let outgoing_uniforms = ProjectUniforms::new( - &editor.render_constants, - &project_config, - frame_number, - settings.fps, - settings.resolution_base, - &outgoing_media.cursor, - &outgoing_frames, - total_duration, - &outgoing_zoom, - ); - frame_renderer - .render_transition_immediate( - TransitionRenderInput { - segment_frames: outgoing_frames, - uniforms: outgoing_uniforms, - cursor: &outgoing_media.cursor, - render_display: !settings.cursor_only, - }, - TransitionRenderInput { - segment_frames, - uniforms, - cursor: &segment_media.cursor, - render_display: !settings.cursor_only, - }, - kind, - progress as f32, - &mut layers, - ) - .await - } else { - frame_renderer - .render_immediate( - segment_frames, - uniforms, - &segment_media.cursor, - !settings.cursor_only, - &mut layers, - ) - .await - } - .map_err(|e| format!("Failed to render frame: {e}"))?; - - let frame_render_time_ms = render_start.elapsed().as_secs_f64() * 1000.0; - - let width = frame.width; - let height = frame.height; - - let rgb_data: Vec = frame - .data - .chunks(frame.padded_bytes_per_row as usize) - .flat_map(|row| { - row[0..(frame.width * 4) as usize] - .chunks(4) - .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) - }) - .collect(); - - let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp); - let mut jpeg_buffer = Vec::new(); - { - let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality); - encoder - .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8) - .map_err(|e| format!("Failed to encode JPEG: {e}"))?; - } - - let jpeg_base64 = STANDARD.encode(&jpeg_buffer); - - let total_pixels = (settings.resolution_base.x * settings.resolution_base.y) as f64; - let fps_f64 = settings.fps as f64; - - let duration_seconds = export_estimate_duration(&project_config, editor.recordings.duration()); - let total_frames = (duration_seconds * fps_f64).ceil() as u32; - - let estimated_size_mb = if settings.cursor_only { - let total_frames_f64 = (duration_seconds * fps_f64).ceil(); - estimate_cursor_only_size_mb(total_pixels, total_frames_f64) - } else { - let effective_fps = ((fps_f64 - 30.0).max(0.0) * 0.6) + fps_f64.min(30.0); - let video_bitrate = total_pixels * settings.compression_bpp as f64 * effective_fps; - let audio_bitrate = 192_000.0; - let total_bitrate = video_bitrate + audio_bitrate; - let encoder_efficiency = 0.5; - (total_bitrate * encoder_efficiency * duration_seconds) / (8.0 * 1024.0 * 1024.0) - }; - - Ok(ExportPreviewResult { - jpeg_base64, - estimated_size_mb, - actual_width: width, - actual_height: height, - frame_render_time_ms, - total_frames, - }) -} diff --git a/apps/desktop/src-tauri/src/export/estimate_request.rs b/apps/desktop/src-tauri/src/export/estimate_request.rs deleted file mode 100644 index b8d6d347fdc..00000000000 --- a/apps/desktop/src-tauri/src/export/estimate_request.rs +++ /dev/null @@ -1,290 +0,0 @@ -use futures::FutureExt; -use std::{ - collections::HashMap, - future::Future, - panic::AssertUnwindSafe, - path::{Path, PathBuf}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, -}; - -#[derive(Default)] -pub struct EstimateRequests { - cancellations: Mutex>>, -} - -impl EstimateRequests { - pub async fn run( - &self, - path: PathBuf, - estimate: impl FnOnce(Arc) -> Fut, - ) -> Result - where - Fut: Future>, - { - let request = self.start(path)?; - let cancel = request.cancellation(); - request.run(async move { estimate(cancel).await }).await - } - - fn start(&self, path: PathBuf) -> Result, String> { - let cancel = Arc::new(AtomicBool::new(false)); - if let Some(previous) = self - .cancellations - .lock() - .map_err(|error| error.to_string())? - .insert(path.clone(), cancel.clone()) - { - previous.store(true, Ordering::Release); - } - Ok(EstimateRequest { - requests: self, - path, - cancel, - }) - } - - pub fn cancel(&self, path: &Path) { - if let Ok(cancellations) = self.cancellations.lock() - && let Some(cancel) = cancellations.get(path) - { - cancel.store(true, Ordering::Release); - } - } -} - -struct EstimateRequest<'a> { - requests: &'a EstimateRequests, - path: PathBuf, - cancel: Arc, -} - -impl EstimateRequest<'_> { - fn cancellation(&self) -> Arc { - self.cancel.clone() - } - - async fn run(self, future: impl Future>) -> Result { - match AssertUnwindSafe(future).catch_unwind().await { - Ok(result) => result, - Err(_) => { - tracing::error!(target: "cap_desktop_export", "Export estimate panicked"); - Err("Export estimate failed unexpectedly".to_string()) - } - } - } -} - -impl Drop for EstimateRequest<'_> { - fn drop(&mut self) { - self.cancel.store(true, Ordering::Release); - if let Ok(mut cancellations) = self.requests.cancellations.lock() - && cancellations - .get(&self.path) - .is_some_and(|current| Arc::ptr_eq(current, &self.cancel)) - { - let _ = cancellations.remove(&self.path); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::{executor::block_on, future}; - use std::task::Poll; - - struct DropSignal(Arc); - - impl Drop for DropSignal { - fn drop(&mut self) { - self.0.store(true, Ordering::Release); - } - } - - #[test] - fn successful_request_returns_estimate_and_cleans_up() { - let requests = EstimateRequests::default(); - let request = requests.start("example.cap".into()).unwrap(); - let cancel = request.cancellation(); - let result = block_on(request.run(async { Ok(42) })); - assert_eq!(result, Ok(42)); - assert!(cancel.load(Ordering::Acquire)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn ordinary_error_is_preserved_and_cleans_up() { - let requests = EstimateRequests::default(); - let request = requests.start("example.cap".into()).unwrap(); - let result = block_on(request.run(async { Err::<(), _>("Invalid resolution".into()) })); - assert_eq!(result, Err("Invalid resolution".into())); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn polling_panic_returns_safe_error_and_drops_sample_resources() { - let requests = EstimateRequests::default(); - let request = requests.start("example.cap".into()).unwrap(); - let cancel = request.cancellation(); - let dropped = Arc::new(AtomicBool::new(false)); - let signal = dropped.clone(); - let result = block_on(request.run(async move { - let _sample = DropSignal(signal); - let mut yielded = false; - future::poll_fn(|cx| { - if yielded { - Poll::Ready(()) - } else { - yielded = true; - cx.waker().wake_by_ref(); - Poll::Pending - } - }) - .await; - future::poll_fn(|_| -> Poll> { - panic!("synthetic pipeline validation failure with private details") - }) - .await - })); - assert_eq!(result, Err("Export estimate failed unexpectedly".into())); - assert!(dropped.load(Ordering::Acquire)); - assert!(cancel.load(Ordering::Acquire)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn dropping_pending_request_cancels_and_cleans_up() { - let requests = EstimateRequests::default(); - let request = requests.start("example.cap".into()).unwrap(); - let cancel = request.cancellation(); - let mut running = Box::pin(request.run(future::pending::>())); - assert!(running.as_mut().now_or_never().is_none()); - assert!(!cancel.load(Ordering::Acquire)); - drop(running); - assert!(cancel.load(Ordering::Acquire)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn superseded_request_cannot_remove_replacement() { - let requests = EstimateRequests::default(); - let path = PathBuf::from("example.cap"); - let first = requests.start(path.clone()).unwrap(); - let first_cancel = first.cancellation(); - let second = requests.start(path.clone()).unwrap(); - let second_cancel = second.cancellation(); - assert!(first_cancel.load(Ordering::Acquire)); - assert_eq!(block_on(first.run(async { Ok(()) })), Ok(())); - assert!(!second_cancel.load(Ordering::Acquire)); - assert!(Arc::ptr_eq( - requests.cancellations.lock().unwrap().get(&path).unwrap(), - &second_cancel, - )); - requests.cancel(&path); - assert!(second_cancel.load(Ordering::Acquire)); - drop(second); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn panicking_request_cannot_remove_replacement() { - let requests = EstimateRequests::default(); - let path = PathBuf::from("example.cap"); - let first = requests.start(path.clone()).unwrap(); - let second = requests.start(path.clone()).unwrap(); - let second_cancel = second.cancellation(); - let result = block_on(first.run(future::poll_fn(|_| -> Poll> { - panic!("synthetic failure") - }))); - assert!(result.is_err()); - assert!(Arc::ptr_eq( - requests.cancellations.lock().unwrap().get(&path).unwrap(), - &second_cancel, - )); - assert!(!second_cancel.load(Ordering::Acquire)); - requests.cancel(&path); - assert!(second_cancel.load(Ordering::Acquire)); - } - - #[test] - fn dropping_unpolled_request_cleans_up_registration() { - let requests = EstimateRequests::default(); - let request = requests.start("example.cap".into()).unwrap(); - let cancel = request.cancellation(); - drop(request.run(future::pending::>())); - assert!(cancel.load(Ordering::Acquire)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn cancellation_is_scoped_to_the_requested_project() { - let requests = EstimateRequests::default(); - let first = requests.start("first.cap".into()).unwrap(); - let second = requests.start("second.cap".into()).unwrap(); - requests.cancel(Path::new("first.cap")); - assert!(first.cancellation().load(Ordering::Acquire)); - assert!(!second.cancellation().load(Ordering::Acquire)); - } - - #[test] - fn run_passes_the_registered_cancellation_to_the_estimate() { - let requests = EstimateRequests::default(); - let path = PathBuf::from("example.cap"); - let result = block_on(requests.run(path.clone(), |cancel| { - let requests = &requests; - let path = path.clone(); - async move { - assert!(!cancel.load(Ordering::Acquire)); - requests.cancel(&path); - Ok(cancel.load(Ordering::Acquire)) - } - })); - assert_eq!(result, Ok(true)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn run_settles_a_panicking_estimate_and_cleans_up() { - let requests = EstimateRequests::default(); - let observed = Arc::new(Mutex::new(None)); - let seen = observed.clone(); - let result = block_on(requests.run("example.cap".into(), |cancel| { - *seen.lock().unwrap() = Some(cancel); - future::poll_fn(|_| -> Poll> { - panic!("synthetic rendering failure") - }) - })); - assert_eq!(result, Err("Export estimate failed unexpectedly".into())); - let cancel = observed.lock().unwrap().take().unwrap(); - assert!(cancel.load(Ordering::Acquire)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn run_settles_a_panic_while_building_the_estimate() { - let requests = EstimateRequests::default(); - let result = block_on(requests.run( - "example.cap".into(), - |_| -> future::Ready> { panic!("synthetic setup failure") }, - )); - assert_eq!(result, Err("Export estimate failed unexpectedly".into())); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } - - #[test] - fn new_request_can_succeed_after_a_panic() { - let requests = EstimateRequests::default(); - let first = requests.start("example.cap".into()).unwrap(); - let result = block_on(first.run(future::poll_fn(|_| -> Poll> { - panic!("synthetic failure") - }))); - assert!(result.is_err()); - let second = requests.start("example.cap".into()).unwrap(); - assert!(!second.cancellation().load(Ordering::Acquire)); - assert_eq!(block_on(second.run(async { Ok(42) })), Ok(42)); - assert!(requests.cancellations.lock().unwrap().is_empty()); - } -} diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-tauri/src/fake_window.rs deleted file mode 100644 index 1b6d6302afe..00000000000 --- a/apps/desktop/src-tauri/src/fake_window.rs +++ /dev/null @@ -1,962 +0,0 @@ -use cap_recording::sources::screen_capture::ScreenCaptureTarget; -use scap_targets::{Display, DisplayId, Window as ScapWindow, bounds::LogicalBounds}; -use std::{ - collections::HashMap, - sync::{ - Arc, Mutex, - atomic::{AtomicU64, Ordering}, - }, - time::Duration, -}; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tokio::{sync::RwLock, time::sleep}; -use tokio_util::sync::CancellationToken; -use tracing::{debug, instrument}; - -use crate::{App, ArcLock, RecordingState}; - -const RECORDING_CONTROLS_LABEL: &str = "in-progress-recording"; -#[cfg(any(target_os = "macos", test))] -const RECORDING_CONTROLS_BOUNDS_NAME: &str = "recording-controls-interactive-area"; -const RECORDING_CONTROLS_WIDTH: f64 = 320.0; -const RECORDING_CONTROLS_HEIGHT: f64 = 150.0; -const RECORDING_CONTROLS_BAR_HEIGHT: f64 = 40.0; -const RECORDING_CONTROLS_BOTTOM_PADDING: f64 = 12.0; -const RECORDING_CONTROLS_OFFSET_Y: f64 = 120.0; -const TICK_INTERVAL: Duration = Duration::from_millis(50); -const DEAD_WINDOW_ERROR_THRESHOLD: u8 = 5; -const HIT_TEST_PADDING_PHYSICAL: f64 = 2.0; -const RECORDING_CONTROLS_FALLBACK_PADDING_LOGICAL: f64 = 48.0; -const MAX_INTERACTIVE_BOUNDS_WIDTH: f64 = 360.0; -const MAX_INTERACTIVE_BOUNDS_HEIGHT: f64 = 220.0; - -pub struct FakeWindowBounds(pub Arc>>>); - -struct TokenEntry { - id: u64, - token: CancellationToken, -} - -#[derive(Default)] -pub struct FakeWindowListeners { - tokens: Mutex>, - next_id: AtomicU64, -} - -impl FakeWindowListeners { - fn register(&self, label: String) -> (u64, CancellationToken) { - let id = self.next_id.fetch_add(1, Ordering::Relaxed); - let token = CancellationToken::new(); - let mut guard = self.tokens.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(previous) = guard.insert( - label, - TokenEntry { - id, - token: token.clone(), - }, - ) { - previous.token.cancel(); - } - (id, token) - } - - fn finish(&self, label: &str, id: u64) -> bool { - let mut guard = self.tokens.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(current) = guard.get(label) - && current.id == id - { - guard.remove(label); - return true; - } - false - } - - pub fn cancel(&self, label: &str) { - let guard = self.tokens.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(entry) = guard.get(label) { - entry.token.cancel(); - } - } - - pub fn cancel_all(&self) { - let guard = self.tokens.lock().unwrap_or_else(|e| e.into_inner()); - for entry in guard.values() { - entry.token.cancel(); - } - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(state))] -pub async fn set_fake_window_bounds( - window: tauri::Window, - name: String, - bounds: LogicalBounds, - state: tauri::State<'_, FakeWindowBounds>, -) -> Result<(), String> { - let mut state = state.0.write().await; - let map = state.entry(window.label().to_string()).or_default(); - - map.insert(name, bounds); - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(state, window))] -pub async fn remove_fake_window( - window: tauri::Window, - name: String, - state: tauri::State<'_, FakeWindowBounds>, -) -> Result<(), String> { - let mut state = state.0.write().await; - let Some(map) = state.get_mut(window.label()) else { - return Ok(()); - }; - - map.remove(&name); - - if map.is_empty() { - state.remove(window.label()); - } - - Ok(()) -} - -fn should_ignore_cursor_events( - window_position: tauri::PhysicalPosition, - mouse_position: tauri::PhysicalPosition, - scale_factor: f64, - windows: &HashMap, - default_ignore: bool, - allow_default_interaction: bool, -) -> bool { - let mut saw_bounds = false; - - for bounds in windows.values() { - let width = bounds.size().width(); - let height = bounds.size().height(); - if width <= 0.0 - || height <= 0.0 - || width > MAX_INTERACTIVE_BOUNDS_WIDTH - || height > MAX_INTERACTIVE_BOUNDS_HEIGHT - { - continue; - } - - saw_bounds = true; - let x_min = (window_position.x as f64) + bounds.position().x() * scale_factor - - HIT_TEST_PADDING_PHYSICAL; - let x_max = (window_position.x as f64) - + (bounds.position().x() + bounds.size().width()) * scale_factor - + HIT_TEST_PADDING_PHYSICAL; - let y_min = (window_position.y as f64) + bounds.position().y() * scale_factor - - HIT_TEST_PADDING_PHYSICAL; - let y_max = (window_position.y as f64) - + (bounds.position().y() + bounds.size().height()) * scale_factor - + HIT_TEST_PADDING_PHYSICAL; - - if mouse_position.x >= x_min - && mouse_position.x <= x_max - && mouse_position.y >= y_min - && mouse_position.y <= y_max - { - return false; - } - } - - if saw_bounds { - return true; - } - - default_ignore || !allow_default_interaction -} - -#[cfg(any(target_os = "macos", test))] -fn macos_cursor_in_window_scale( - cursor: tauri::PhysicalPosition, - primary_scale: f64, - window_scale: f64, -) -> Option> { - if !primary_scale.is_finite() - || primary_scale <= 0.0 - || !window_scale.is_finite() - || window_scale <= 0.0 - || !cursor.x.is_finite() - || !cursor.y.is_finite() - { - return None; - } - // Tao scales global mouse coordinates by the primary display, but window - // positions and DOM bounds use the window's display scale. - let factor = window_scale / primary_scale; - Some(tauri::PhysicalPosition::new( - cursor.x * factor, - cursor.y * factor, - )) -} - -fn recording_controls_size_allows_default_interaction( - window_size: tauri::PhysicalSize, - scale_factor: f64, -) -> bool { - let scale_factor = scale_factor.max(1.0); - let max_width = - (RECORDING_CONTROLS_WIDTH + RECORDING_CONTROLS_FALLBACK_PADDING_LOGICAL) * scale_factor; - let max_height = - (RECORDING_CONTROLS_HEIGHT + RECORDING_CONTROLS_FALLBACK_PADDING_LOGICAL) * scale_factor; - - (window_size.width as f64) <= max_width && (window_size.height as f64) <= max_height -} - -fn prepare_recording_controls_default_interaction(window: &WebviewWindow) -> bool { - let (Ok(window_size), Ok(scale_factor)) = (window.outer_size(), window.scale_factor()) else { - let _ = window.set_ignore_cursor_events(true); - return false; - }; - - if recording_controls_size_allows_default_interaction(window_size, scale_factor) { - return true; - } - - if window.set_ignore_cursor_events(true).is_err() { - let _ = window.hide(); - return false; - } - - let _ = window.set_size(tauri::LogicalSize::new( - RECORDING_CONTROLS_WIDTH, - RECORDING_CONTROLS_HEIGHT, - )); - - false -} - -fn spawn_recording_controls_sanity_checks(app: AppHandle, window: WebviewWindow) { - let label = window.label().to_string(); - - tokio::spawn(async move { - for delay in [ - Duration::from_millis(250), - Duration::from_secs(2), - Duration::from_secs(5), - ] { - sleep(delay).await; - - if crate::app_is_exiting(&app) { - break; - } - - if crate::power_observer::is_system_asleep() { - continue; - } - - let Some(window) = app.get_webview_window(&label) else { - break; - }; - - let ignore = !prepare_recording_controls_default_interaction(&window); - let _ = window.set_ignore_cursor_events(ignore); - } - }); -} - -#[cfg(any(target_os = "macos", test))] -fn ensure_recording_controls_bounds( - bounds: &mut HashMap, - width: f64, - height: f64, -) { - bounds - .entry(RECORDING_CONTROLS_BOUNDS_NAME.to_string()) - .or_insert_with(|| { - LogicalBounds::new( - scap_targets::bounds::LogicalPosition::new( - RECORDING_CONTROLS_BOTTOM_PADDING, - height - RECORDING_CONTROLS_BOTTOM_PADDING - RECORDING_CONTROLS_BAR_HEIGHT, - ), - scap_targets::bounds::LogicalSize::new( - width - RECORDING_CONTROLS_BOTTOM_PADDING * 2.0, - RECORDING_CONTROLS_BAR_HEIGHT, - ), - ) - }); -} - -#[cfg(any(target_os = "macos", test))] -fn preserve_pointer_gesture(ignore: bool, pressed: bool, was_ignoring: bool) -> bool { - ignore && (!pressed || was_ignoring) -} - -#[cfg(target_os = "macos")] -async fn update_recording_controls_hit_test( - window: &WebviewWindow, - mut bounds: HashMap, - token: CancellationToken, -) -> Option { - use objc2_app_kit::{NSEvent, NSWindow}; - use objc2_foundation::NSSize; - - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = window.clone(); - window - .run_on_main_thread(move || { - if tx.is_closed() || token.is_cancelled() { - return; - } - let result = objc2::rc::autoreleasepool(|_| { - let ptr = handle.ns_window().ok()? as *const NSWindow; - let native = unsafe { ptr.as_ref()? }; - let view = native.contentView()?; - let frame = native.frame(); - let scale = native.backingScaleFactor(); - let small = recording_controls_size_allows_default_interaction( - tauri::LogicalSize::new(frame.size.width, frame.size.height).to_physical(scale), - scale, - ); - let pressed = unsafe { NSEvent::pressedMouseButtons() } & 1 != 0; - if !small { - native.setIgnoresMouseEvents(true); - native.setContentSize(NSSize::new( - RECORDING_CONTROLS_WIDTH, - RECORDING_CONTROLS_HEIGHT, - )); - return Some(pressed); - } - let point = view.convertPoint_fromView( - unsafe { native.mouseLocationOutsideOfEventStream() }, - None, - ); - let view_bounds = view.bounds(); - ensure_recording_controls_bounds( - &mut bounds, - view_bounds.size.width, - view_bounds.size.height, - ); - let y = if view.isFlipped() { - point.y - view_bounds.origin.y - } else { - view_bounds.origin.y + view_bounds.size.height - point.y - }; - let ignore = should_ignore_cursor_events( - tauri::PhysicalPosition::new(0, 0), - tauri::PhysicalPosition::new( - (point.x - view_bounds.origin.x) * scale, - y * scale, - ), - scale, - &bounds, - false, - true, - ); - // Keep the mouse-up routed to this panel when a drag crosses its hit area. - let ignore = preserve_pointer_gesture(ignore, pressed, unsafe { - native.ignoresMouseEvents() - }); - if unsafe { native.ignoresMouseEvents() } != ignore { - native.setIgnoresMouseEvents(ignore); - debug!( - ignore, - pressed, - bounds = bounds.len(), - "Recording controls hit test changed" - ); - } - Some(pressed) - }); - let _ = tx.send(result); - }) - .ok()?; - tokio::time::timeout(Duration::from_millis(250), rx) - .await - .ok()? - .ok()? -} - -fn get_display_id_for_cursor() -> Option { - Display::get_containing_cursor().map(|d| d.id()) -} - -fn get_display_by_id(id: &DisplayId) -> Option { - Display::list().into_iter().find(|d| &d.id() == id) -} - -fn calculate_bottom_center_position(display: &Display) -> Option<(f64, f64)> { - let bounds = display.raw_handle().logical_bounds()?; - let x = bounds.position().x(); - let y = bounds.position().y(); - let width = bounds.size().width(); - let height = bounds.size().height(); - - let pos_x = x + (width - RECORDING_CONTROLS_WIDTH) / 2.0; - let pos_y = y + height - RECORDING_CONTROLS_HEIGHT - RECORDING_CONTROLS_OFFSET_Y; - Some((pos_x, pos_y)) -} - -const TARGET_CONTROLS_OFFSET_Y: f64 = 48.0; -const AREA_CONTROLS_MARGIN: f64 = 16.0; - -fn calculate_area_recording_controls_position( - display_bounds: LogicalBounds, - area_bounds: LogicalBounds, -) -> (f64, f64) { - let display_left = display_bounds.position().x(); - let display_top = display_bounds.position().y(); - let display_right = display_left + display_bounds.size().width(); - let display_bottom = display_top + display_bounds.size().height(); - let area_left = display_left + area_bounds.position().x(); - let area_top = display_top + area_bounds.position().y(); - let area_bottom = area_top + area_bounds.size().height(); - - let max_x = (display_right - RECORDING_CONTROLS_WIDTH).max(display_left); - let pos_x = (area_left + (area_bounds.size().width() - RECORDING_CONTROLS_WIDTH) / 2.0) - .clamp(display_left, max_x); - let bar_top_offset = RECORDING_CONTROLS_HEIGHT - - RECORDING_CONTROLS_BOTTOM_PADDING - - RECORDING_CONTROLS_BAR_HEIGHT; - let bar_bottom_offset = RECORDING_CONTROLS_HEIGHT - RECORDING_CONTROLS_BOTTOM_PADDING; - - let below_y = area_bottom + AREA_CONTROLS_MARGIN - bar_top_offset; - if below_y >= display_top && below_y + RECORDING_CONTROLS_HEIGHT <= display_bottom { - return (pos_x, below_y); - } - - let above_y = area_top - AREA_CONTROLS_MARGIN - bar_bottom_offset; - if above_y >= display_top && above_y + RECORDING_CONTROLS_HEIGHT <= display_bottom { - return (pos_x, above_y); - } - - let max_y = (display_bottom - RECORDING_CONTROLS_HEIGHT).max(display_top); - let fallback_y = (area_bottom - RECORDING_CONTROLS_HEIGHT - TARGET_CONTROLS_OFFSET_Y) - .clamp(display_top, max_y); - (pos_x, fallback_y) -} - -pub fn calculate_recording_controls_position_for_target( - capture_target: &ScreenCaptureTarget, -) -> Option<(f64, f64)> { - match capture_target { - ScreenCaptureTarget::Window { id } => { - let window = ScapWindow::from_id(id)?; - let bounds = window.raw_handle().logical_bounds()?; - let pos_x = - bounds.position().x() + (bounds.size().width() - RECORDING_CONTROLS_WIDTH) / 2.0; - let pos_y = bounds.position().y() + bounds.size().height() - - RECORDING_CONTROLS_HEIGHT - - TARGET_CONTROLS_OFFSET_Y; - Some((pos_x, pos_y)) - } - ScreenCaptureTarget::Area { screen, bounds } => { - let display = Display::from_id(screen)?; - let display_bounds = display.raw_handle().logical_bounds()?; - Some(calculate_area_recording_controls_position( - display_bounds, - *bounds, - )) - } - _ => None, - } -} - -pub fn spawn_fake_window_listener(app: AppHandle, window: WebviewWindow) { - let label = window.label().to_string(); - let is_recording_controls = label == RECORDING_CONTROLS_LABEL; - let default_ignore = !is_recording_controls; - let initial_ignore = if is_recording_controls { - !prepare_recording_controls_default_interaction(&window) - } else { - default_ignore - }; - window.set_ignore_cursor_events(initial_ignore).ok(); - let listeners = app.state::(); - let (listener_id, token) = listeners.register(label.clone()); - - if is_recording_controls { - spawn_recording_controls_sanity_checks(app.clone(), window.clone()); - } - - tokio::spawn(async move { - let listeners = app.state::(); - let state = app.state::(); - let mut current_display_id: Option = get_display_id_for_cursor(); - let mut last_target_pos: Option<(f64, f64)> = None; - let mut consecutive_errors: u8 = 0; - - loop { - tokio::select! { - biased; - _ = token.cancelled() => { - debug!(window = %label, "Fake window listener cancelled"); - break; - } - _ = sleep(TICK_INTERVAL) => {} - } - - if crate::app_is_exiting(&app) { - break; - } - - if crate::power_observer::is_system_asleep() { - continue; - } - - if !app.webview_windows().contains_key(&label) { - debug!(window = %label, "Fake window listener stopping: window no longer exists"); - break; - } - - #[cfg(target_os = "macos")] - let controls_pressed = if is_recording_controls { - let bounds = state - .0 - .read() - .await - .get(&label) - .cloned() - .unwrap_or_default(); - match update_recording_controls_hit_test(&window, bounds, token.clone()).await { - Some(pressed) => pressed, - None => continue, - } - } else { - false - }; - #[cfg(not(target_os = "macos"))] - let controls_pressed = false; - - if is_recording_controls && !controls_pressed { - let capture_target = app.state::>().try_read().ok().and_then(|s| { - match &s.recording_state { - RecordingState::Pending { target, .. } => Some(target.clone()), - RecordingState::Active(inner) => Some(inner.capture_target().clone()), - RecordingState::None => None, - } - }); - - let mut handled = false; - - if let Some(ref target) = capture_target { - match target { - ScreenCaptureTarget::Window { .. } => { - if let Some((px, py)) = - calculate_recording_controls_position_for_target(target) - { - let changed = match last_target_pos { - Some((lx, ly)) => { - (px - lx).abs() > 0.5 || (py - ly).abs() > 0.5 - } - None => true, - }; - if changed { - let _ = window.set_position( - crate::windows::logical_point_position(px, py), - ); - last_target_pos = Some((px, py)); - } - handled = true; - } - } - ScreenCaptureTarget::Area { .. } => { - handled = true; - } - _ => {} - } - } - - if !handled && let Some(cursor_display_id) = get_display_id_for_cursor() { - let display_changed = current_display_id.as_ref() != Some(&cursor_display_id); - - if display_changed - && let Some(display) = get_display_by_id(&cursor_display_id) - && let Some((pos_x, pos_y)) = calculate_bottom_center_position(&display) - { - let _ = window - .set_position(crate::windows::logical_point_position(pos_x, pos_y)); - current_display_id = Some(cursor_display_id); - } - } - } - - #[cfg(target_os = "macos")] - if is_recording_controls { - continue; - } - - let windows = state.0.read().await.get(&label).cloned(); - - let Some(windows) = windows else { - let ignore = if is_recording_controls { - !prepare_recording_controls_default_interaction(&window) - } else { - default_ignore - }; - if window.set_ignore_cursor_events(ignore).is_err() { - consecutive_errors = consecutive_errors.saturating_add(1); - if consecutive_errors >= DEAD_WINDOW_ERROR_THRESHOLD { - debug!( - window = %label, - "Fake window listener stopping: window handle is no longer responsive" - ); - break; - } - } else { - consecutive_errors = 0; - } - continue; - }; - - let (Ok(window_position), Ok(mouse_position), Ok(scale_factor)) = ( - window.outer_position(), - window.cursor_position(), - window.scale_factor(), - ) else { - consecutive_errors = consecutive_errors.saturating_add(1); - if consecutive_errors >= DEAD_WINDOW_ERROR_THRESHOLD { - debug!( - window = %label, - "Fake window listener stopping: repeated failures querying window state" - ); - break; - } - let ignore = if is_recording_controls { - !prepare_recording_controls_default_interaction(&window) - } else { - default_ignore - }; - let _ = window.set_ignore_cursor_events(ignore); - continue; - }; - - consecutive_errors = 0; - #[cfg(target_os = "macos")] - let mouse_position = match objc2::rc::autoreleasepool(|_| { - window.primary_monitor().ok().flatten().and_then(|monitor| { - macos_cursor_in_window_scale( - mouse_position, - monitor.scale_factor(), - scale_factor, - ) - }) - }) { - Some(position) => position, - None => { - let ignore = !is_recording_controls - || !prepare_recording_controls_default_interaction(&window); - let _ = window.set_ignore_cursor_events(ignore); - continue; - } - }; - let allow_default_interaction = if is_recording_controls { - prepare_recording_controls_default_interaction(&window) - } else { - false - }; - - let ignore = should_ignore_cursor_events( - window_position, - mouse_position, - scale_factor, - &windows, - default_ignore, - allow_default_interaction, - ); - - window.set_ignore_cursor_events(ignore).ok(); - - let focused = window.is_focused().unwrap_or(false); - if !ignore { - if !focused { - window.set_focus().ok(); - } - } else if focused { - window.set_ignore_cursor_events(ignore).ok(); - } - } - - let mut map = state.0.write().await; - if listeners.finish(&label, listener_id) && !app.webview_windows().contains_key(&label) { - map.remove(&label); - } - }); -} - -pub fn cancel_fake_window_listener(app: &AppHandle, label: &str) { - if let Some(listeners) = app.try_state::() { - listeners.cancel(label); - } -} - -pub fn cancel_all_fake_window_listeners(app: &AppHandle) { - if let Some(listeners) = app.try_state::() { - listeners.cancel_all(); - } -} - -pub fn init(app: &AppHandle) { - app.manage(FakeWindowBounds(Default::default())); - app.manage(FakeWindowListeners::default()); -} - -#[cfg(test)] -mod tests { - use super::*; - use scap_targets::bounds::{LogicalPosition, LogicalSize}; - - #[test] - fn tooltip_only_bounds_do_not_disable_the_recording_bar() { - let mut registered = HashMap::from([("tooltip".into(), bounds(30.0, 60.0, 80.0, 24.0))]); - ensure_recording_controls_bounds(&mut registered, 320.0, 150.0); - for (x, y, ignore) in [(24.0, 110.0, false), (40.0, 70.0, false), (4.0, 30.0, true)] { - assert_eq!( - should_ignore_cursor_events( - tauri::PhysicalPosition::new(0, 0), - tauri::PhysicalPosition::new(x, y), - 1.0, - ®istered, - false, - true, - ), - ignore, - ); - } - } - - #[test] - fn registered_issue_panel_bounds_are_preserved() { - let issue = bounds(12.0, 40.0, 296.0, 98.0); - let mut registered = HashMap::from([(RECORDING_CONTROLS_BOUNDS_NAME.into(), issue)]); - ensure_recording_controls_bounds(&mut registered, 320.0, 150.0); - let actual = registered[RECORDING_CONTROLS_BOUNDS_NAME]; - assert_eq!(actual.position().x(), issue.position().x()); - assert_eq!(actual.position().y(), issue.position().y()); - assert_eq!(actual.size().width(), issue.size().width()); - assert_eq!(actual.size().height(), issue.size().height()); - } - - #[test] - fn replaced_listener_cannot_finish_the_current_listener() { - let listeners = FakeWindowListeners::default(); - let (old, old_token) = listeners.register("controls".into()); - let (current, current_token) = listeners.register("controls".into()); - assert!(old_token.is_cancelled()); - assert!(!listeners.finish("controls", old)); - assert!(!current_token.is_cancelled()); - assert!(listeners.finish("controls", current)); - } - - #[test] - fn cancelled_listener_retains_ownership_until_cleanup() { - let listeners = FakeWindowListeners::default(); - let (id, token) = listeners.register("controls".into()); - listeners.cancel("controls"); - assert!(token.is_cancelled()); - assert!(listeners.finish("controls", id)); - } - - #[test] - fn pointer_gesture_remains_interactive_until_release() { - assert!(!preserve_pointer_gesture(true, true, false)); - assert!(preserve_pointer_gesture(true, false, false)); - assert!(preserve_pointer_gesture(true, true, true)); - assert!(!preserve_pointer_gesture(false, true, true)); - } - - fn bounds(x: f64, y: f64, width: f64, height: f64) -> LogicalBounds { - LogicalBounds::new(LogicalPosition::new(x, y), LogicalSize::new(width, height)) - } - - #[test] - fn macos_hit_testing_uses_the_same_scale_on_secondary_displays() { - let mut windows = HashMap::new(); - windows.insert("controls".to_string(), bounds(10.0, 20.0, 100.0, 40.0)); - for (origin, primary_scale, window_scale) in [ - ((1920.0, 100.0), 2.0, 1.0), - ((-1280.0, 100.0), 1.0, 2.0), - ((0.0, -900.0), 2.0, 1.0), - ((0.0, 0.0), 2.0, 2.0), - ] { - let position = - tauri::LogicalPosition::new(origin.0, origin.1).to_physical::(window_scale); - for (offset, ignored) in [((30.0, 30.0), false), ((150.0, 80.0), true)] { - let mouse = tauri::LogicalPosition::new(origin.0 + offset.0, origin.1 + offset.1) - .to_physical::(primary_scale); - let mouse = - macos_cursor_in_window_scale(mouse, primary_scale, window_scale).unwrap(); - assert_eq!( - should_ignore_cursor_events( - position, - mouse, - window_scale, - &windows, - false, - true, - ), - ignored, - ); - } - } - } - - #[test] - fn macos_hit_testing_rejects_invalid_coordinate_scales() { - for scale in [0.0, -1.0, f64::NAN, f64::INFINITY] { - assert!( - macos_cursor_in_window_scale(tauri::PhysicalPosition::new(20.0, 30.0), scale, 2.0,) - .is_none() - ); - } - } - - #[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), - 2.0, - &HashMap::new(), - false, - true, - )); - } - - #[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), - 2.0, - &HashMap::new(), - false, - false, - )); - } - - #[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), - 2.0, - &HashMap::new(), - true, - false, - )); - } - - #[test] - fn cursor_inside_registered_bounds_is_interactive() { - let mut windows = HashMap::new(); - 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), - 2.0, - &windows, - false, - true, - )); - } - - #[test] - fn cursor_outside_registered_bounds_passes_through() { - let mut windows = HashMap::new(); - 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), - 2.0, - &windows, - false, - true, - )); - } - - #[test] - fn zero_sized_recording_bounds_fail_open() { - let mut windows = HashMap::new(); - 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), - 2.0, - &windows, - false, - true, - )); - } - - #[test] - fn oversized_bounds_do_not_make_the_window_interactive() { - let mut windows = HashMap::new(); - 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), - 2.0, - &windows, - false, - false, - )); - } - - #[test] - fn recording_controls_default_interaction_is_size_limited() { - assert!(recording_controls_size_allows_default_interaction( - tauri::PhysicalSize::new(640, 300), - 2.0, - )); - assert!(!recording_controls_size_allows_default_interaction( - tauri::PhysicalSize::new(1920, 1080), - 2.0, - )); - } - - #[test] - fn area_recording_controls_prefer_below_the_capture() { - let display = bounds(100.0, 200.0, 1920.0, 1080.0); - let area = bounds(200.0, 100.0, 800.0, 500.0); - - let position = calculate_area_recording_controls_position(display, area); - - assert_eq!(position, (540.0, 718.0)); - } - - #[test] - fn area_recording_controls_move_above_when_below_does_not_fit() { - let display = bounds(0.0, 0.0, 1920.0, 1080.0); - let area = bounds(300.0, 600.0, 800.0, 440.0); - - let position = calculate_area_recording_controls_position(display, area); - - assert_eq!(position, (540.0, 446.0)); - } - - #[test] - fn area_recording_controls_keep_the_existing_inside_fallback() { - let display = bounds(0.0, 0.0, 1920.0, 1080.0); - let area = bounds(300.0, 40.0, 800.0, 1000.0); - - let position = calculate_area_recording_controls_position(display, area); - - assert_eq!(position, (540.0, 842.0)); - } - - #[test] - fn area_recording_controls_stay_within_horizontal_display_edges() { - let display = bounds(100.0, 200.0, 1920.0, 1080.0); - let left_area = bounds(0.0, 100.0, 150.0, 300.0); - let right_area = bounds(1770.0, 100.0, 150.0, 300.0); - - let left_position = calculate_area_recording_controls_position(display, left_area); - let right_position = calculate_area_recording_controls_position(display, right_area); - - assert_eq!(left_position.0, 100.0); - assert_eq!(right_position.0, 1700.0); - } -} 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/frame_ws.rs b/apps/desktop/src-tauri/src/frame_ws.rs deleted file mode 100644 index a6e6503d1b5..00000000000 --- a/apps/desktop/src-tauri/src/frame_ws.rs +++ /dev/null @@ -1,1260 +0,0 @@ -use axum::body::Bytes; -use serde::Deserialize; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Instant; -use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::sync::{broadcast, watch}; -use tokio_util::sync::CancellationToken; - -const NV12_VIDEO_FORMAT_MAGIC: u32 = 0x4e563132; -const NV12_FULL_FORMAT_MAGIC: u32 = 0x4e563146; - -fn pack_frame_data( - mut data: Vec, - stride: u32, - height: u32, - width: u32, - frame_number: u32, - target_time_ns: u64, -) -> Vec { - data.reserve_exact(24); - data.extend_from_slice(&stride.to_le_bytes()); - data.extend_from_slice(&height.to_le_bytes()); - data.extend_from_slice(&width.to_le_bytes()); - data.extend_from_slice(&frame_number.to_le_bytes()); - data.extend_from_slice(&target_time_ns.to_le_bytes()); - data -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum WSFrameFormat { - Rgba, - Nv12 { full_range: bool }, -} - -#[derive(Clone)] -pub enum WSFrameData { - Raw(Arc>), - Packed(Bytes), -} - -impl From>> for WSFrameData { - fn from(data: Arc>) -> Self { - Self::Raw(data) - } -} - -#[derive(Clone)] -pub struct WSFrame { - pub data: WSFrameData, - pub width: u32, - pub height: u32, - pub stride: u32, - pub frame_number: u32, - pub target_time_ns: u64, - pub format: WSFrameFormat, - #[allow(dead_code)] - pub created_at: Instant, -} - -impl WSFrame { - pub fn into_packed(mut self) -> Self { - if let WSFrameData::Raw(data) = self.data { - self.data = WSFrameData::Packed(pack_frame_payload( - Arc::unwrap_or_clone(data), - self.stride, - self.height, - self.width, - self.frame_number, - self.target_time_ns, - self.format, - )); - } - self - } -} - -fn pack_ws_frame(frame: &WSFrame) -> Bytes { - let data = match &frame.data { - WSFrameData::Raw(data) => data, - WSFrameData::Packed(data) => return data.clone(), - }; - let metadata_size = match frame.format { - WSFrameFormat::Nv12 { .. } => 28usize, - WSFrameFormat::Rgba => 24, - }; - let mut buf = Vec::with_capacity(data.len() + metadata_size); - buf.extend_from_slice(data); - pack_frame_payload( - buf, - frame.stride, - frame.height, - frame.width, - frame.frame_number, - frame.target_time_ns, - frame.format, - ) -} - -fn pack_frame_payload( - mut data: Vec, - stride: u32, - height: u32, - width: u32, - frame_number: u32, - target_time_ns: u64, - format: WSFrameFormat, -) -> Bytes { - if matches!(format, WSFrameFormat::Nv12 { .. }) { - data.reserve_exact(28); - } - let mut buf = pack_frame_data(data, stride, height, width, frame_number, target_time_ns); - if let WSFrameFormat::Nv12 { full_range } = format { - let magic = if full_range { - NV12_FULL_FORMAT_MAGIC - } else { - NV12_VIDEO_FORMAT_MAGIC - }; - buf.extend_from_slice(&magic.to_le_bytes()); - } - - buf.into() -} - -fn duration_ns(duration: std::time::Duration) -> u64 { - duration.as_nanos().min(u128::from(u64::MAX)) as u64 -} - -#[derive(Default)] -struct WsFrameStats { - total_bytes_sent: u64, - total_frames_sent: u32, - last_log_time_ms: u64, - total_pack_ns: u64, - max_pack_ns: u64, - total_send_ns: u64, - max_send_ns: u64, - total_created_to_sent_ns: u64, - max_created_to_sent_ns: u64, -} - -impl WsFrameStats { - fn record( - &mut self, - packed_len: usize, - pack_duration: std::time::Duration, - send_duration: std::time::Duration, - created_to_sent: std::time::Duration, - ) { - self.total_bytes_sent += packed_len as u64; - self.total_frames_sent += 1; - let pack_ns = duration_ns(pack_duration); - let send_ns = duration_ns(send_duration); - let created_to_sent_ns = duration_ns(created_to_sent); - self.total_pack_ns += pack_ns; - self.max_pack_ns = self.max_pack_ns.max(pack_ns); - self.total_send_ns += send_ns; - self.max_send_ns = self.max_send_ns.max(send_ns); - self.total_created_to_sent_ns += created_to_sent_ns; - self.max_created_to_sent_ns = self.max_created_to_sent_ns.max(created_to_sent_ns); - } - - fn reset_window(&mut self, now_ms: u64) -> WsFrameStatsWindow { - self.last_log_time_ms = now_ms; - WsFrameStatsWindow { - total_bytes_sent: std::mem::take(&mut self.total_bytes_sent), - total_frames_sent: std::mem::take(&mut self.total_frames_sent), - total_pack_ns: std::mem::take(&mut self.total_pack_ns), - max_pack_ns: std::mem::take(&mut self.max_pack_ns), - total_send_ns: std::mem::take(&mut self.total_send_ns), - max_send_ns: std::mem::take(&mut self.max_send_ns), - total_created_to_sent_ns: std::mem::take(&mut self.total_created_to_sent_ns), - max_created_to_sent_ns: std::mem::take(&mut self.max_created_to_sent_ns), - } - } -} - -struct WsFrameStatsWindow { - total_bytes_sent: u64, - total_frames_sent: u32, - total_pack_ns: u64, - max_pack_ns: u64, - total_send_ns: u64, - max_send_ns: u64, - total_created_to_sent_ns: u64, - max_created_to_sent_ns: u64, -} - -struct SubscriberCountGuard { - subscribers: Arc, - instant_subscribers: Option>, -} - -impl Drop for SubscriberCountGuard { - fn drop(&mut self) { - self.subscribers.fetch_sub(1, Ordering::AcqRel); - if let Some(instant_subscribers) = &self.instant_subscribers { - instant_subscribers.fetch_sub(1, Ordering::AcqRel); - } - } -} - -#[derive(Deserialize)] -struct WatchFrameQuery { - #[serde(default)] - instant: bool, -} - -fn is_normal_socket_disconnect(error: &impl std::fmt::Debug) -> bool { - let error = format!("{error:?}"); - error.contains("BrokenPipe") - || error.contains("Broken pipe") - || error.contains("ConnectionReset") - || error.contains("Connection reset by peer") -} - -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 -} - -pub async fn create_watch_frame_ws_with_instant_tracking( - frame_rx: watch::Receiver>>, - subscribers: Arc, - instant_subscribers: Arc, -) -> (u16, CancellationToken) { - create_watch_frame_ws_inner(frame_rx, subscribers, Some(instant_subscribers)).await -} - -async fn create_watch_frame_ws_inner( - frame_rx: watch::Receiver>>, - subscribers: Arc, - instant_subscribers: Option>, -) -> (u16, CancellationToken) { - use axum::{ - extract::{ - Query, State, - ws::{Message, WebSocket, WebSocketUpgrade}, - }, - response::IntoResponse, - routing::get, - }; - - type RouterState = ( - watch::Receiver>>, - Arc, - Option>, - CancellationToken, - ); - - #[axum::debug_handler] - async fn ws_handler( - ws: WebSocketUpgrade, - Query(query): Query, - State((state, subscribers, instant_subscribers, shutdown)): State, - ) -> impl IntoResponse { - let instant_subscribers = query.instant.then_some(instant_subscribers).flatten(); - ws.read_buffer_size(4096) - .on_upgrade(move |socket| async move { - tokio::select! { - biased; - _ = shutdown.cancelled() => {}, - _ = handle_socket(socket, state, subscribers, instant_subscribers) => {}, - } - }) - } - - async fn handle_socket( - mut socket: WebSocket, - mut camera_rx: watch::Receiver>>, - subscribers: Arc, - instant_subscribers: Option>, - ) { - tracing::info!("Socket connection established"); - let now = std::time::Instant::now(); - let mut stats = WsFrameStats::default(); - - subscribers.fetch_add(1, Ordering::AcqRel); - if let Some(instant_subscribers) = &instant_subscribers { - instant_subscribers.fetch_add(1, Ordering::AcqRel); - } - let _subscriber_guard = SubscriberCountGuard { - subscribers, - instant_subscribers, - }; - - { - let packed = { - let borrowed = camera_rx.borrow(); - borrowed - .as_deref() - .map(|frame| (pack_ws_frame(frame), frame.created_at.elapsed())) - }; - match packed { - Some((packed, frame_age)) => { - if let Err(e) = socket.send(Message::Binary(packed)).await { - if is_normal_socket_disconnect(&e) { - tracing::debug!( - "Initial frame send skipped because socket closed: {:?}", - e - ); - } else { - tracing::error!("Failed to send initial frame to socket: {:?}", e); - } - return; - } - tracing::info!( - frame_age_ms = frame_age.as_millis() as u64, - "Editor open: initial frame delivered to new socket" - ); - } - None => { - tracing::info!("Editor open: socket connected before any frame was rendered"); - } - } - } - - loop { - tokio::select! { - msg = socket.recv() => { - match msg { - Some(Ok(Message::Close(_))) | None => { - tracing::info!("WebSocket closed"); - break; - } - Some(Ok(_)) => {} - Some(Err(e)) => { - if is_normal_socket_disconnect(&e) { - tracing::debug!("WebSocket closed by client: {:?}", e); - } else { - tracing::error!("WebSocket error: {:?}", e); - } - break; - } - } - }, - changed = camera_rx.changed() => { - if changed.is_err() { - break; - } - let frame_arc = camera_rx.borrow_and_update().clone(); - if let Some(ref frame) = frame_arc { - let width = frame.width; - let height = frame.height; - let format_label = match frame.format { - WSFrameFormat::Nv12 { full_range: false } => "NV12", - WSFrameFormat::Nv12 { full_range: true } => "NV12-full", - WSFrameFormat::Rgba => "RGBA", - }; - - let pack_start = Instant::now(); - let packed = pack_ws_frame(frame); - let pack_duration = pack_start.elapsed(); - let packed_len = packed.len(); - - let send_start = Instant::now(); - match socket.send(Message::Binary(packed)).await { - Ok(()) => { - let send_duration = send_start.elapsed(); - stats.record( - packed_len, - pack_duration, - send_duration, - frame.created_at.elapsed(), - ); - let now_ms = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis() as u64) - .unwrap_or_default(); - if now_ms.saturating_sub(stats.last_log_time_ms) > 2000 { - let window = stats.reset_window(now_ms); - let frames = window.total_frames_sent.max(1) as f64; - let mb_per_sec = window.total_bytes_sent as f64 / 1_000_000.0 / 2.0; - tracing::info!( - fps = window.total_frames_sent / 2, - mb_per_sec = format!("{:.1}", mb_per_sec), - avg_kb = format!("{:.1}", (window.total_bytes_sent as f64 / window.total_frames_sent.max(1) as f64) / 1024.0), - pack_avg_ms = format!("{:.3}", window.total_pack_ns as f64 / frames / 1_000_000.0), - pack_max_ms = format!("{:.3}", window.max_pack_ns as f64 / 1_000_000.0), - send_avg_ms = format!("{:.3}", window.total_send_ns as f64 / frames / 1_000_000.0), - send_max_ms = format!("{:.3}", window.max_send_ns as f64 / 1_000_000.0), - created_to_sent_avg_ms = format!("{:.3}", window.total_created_to_sent_ns as f64 / frames / 1_000_000.0), - created_to_sent_max_ms = format!("{:.3}", window.max_created_to_sent_ns as f64 / 1_000_000.0), - dims = format!("{}x{}", width, height), - format = format_label, - "WS frame stats" - ); - } - } - Err(e) => { - if is_normal_socket_disconnect(&e) { - tracing::debug!("Frame send stopped because socket closed: {:?}", e); - } else { - tracing::error!("Failed to send frame to socket: {:?}", e); - } - break; - } - } - } - } - } - } - - let elapsed = now.elapsed(); - tracing::info!("Websocket closing after {elapsed:.2?}"); - } - - let cancel_token = CancellationToken::new(); - let server_shutdown = cancel_token.child_token(); - let router = axum::Router::new().route("/", get(ws_handler)).with_state(( - frame_rx, - subscribers, - instant_subscribers, - server_shutdown.clone(), - )); - let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await { - Ok(listener) => listener, - Err(err) => { - tracing::error!("Failed to bind watch frame websocket listener: {err}"); - cancel_token.cancel(); - return (0, cancel_token); - } - }; - let port = match listener.local_addr() { - Ok(addr) => addr.port(), - Err(err) => { - tracing::error!("Failed to read watch frame websocket listener address: {err}"); - cancel_token.cancel(); - return (0, cancel_token); - } - }; - tracing::info!("WebSocket server listening on port {}", port); - - tokio::spawn(async move { - let _shutdown_guard = server_shutdown.clone().drop_guard(); - let server = axum::serve(listener, router.into_make_service()) - .with_graceful_shutdown(server_shutdown.clone().cancelled_owned()); - tokio::select! { - biased; - _ = server_shutdown.cancelled() => { - tracing::info!("WebSocket server shutting down"); - }, - _ = server => {}, - } - }); - - (port, cancel_token) -} - -pub async fn create_frame_ws(frame_tx: broadcast::Sender) -> (u16, CancellationToken) { - use axum::{ - extract::{ - State, - ws::{Message, WebSocket, WebSocketUpgrade}, - }, - response::IntoResponse, - routing::get, - }; - - type RouterState = (broadcast::Sender, CancellationToken); - - #[axum::debug_handler] - async fn ws_handler( - ws: WebSocketUpgrade, - State((state, shutdown)): State, - ) -> impl IntoResponse { - let rx = state.subscribe(); - ws.read_buffer_size(4096) - .on_upgrade(move |socket| async move { - tokio::select! { - biased; - _ = shutdown.cancelled() => {}, - _ = handle_socket(socket, rx) => {}, - } - }) - } - - async fn handle_socket(mut socket: WebSocket, mut camera_rx: broadcast::Receiver) { - tracing::info!("Socket connection established"); - let now = std::time::Instant::now(); - - loop { - tokio::select! { - msg = socket.recv() => { - match msg { - Some(Ok(Message::Close(_))) | None => { - tracing::info!("WebSocket closed"); - break; - } - Some(Ok(_)) => { - tracing::info!("Received message from socket (ignoring)"); - } - Some(Err(e)) => { - if is_normal_socket_disconnect(&e) { - tracing::debug!("WebSocket closed by client: {:?}", e); - } else { - tracing::error!("WebSocket error: {:?}", e); - } - break; - } - } - }, - incoming_frame = camera_rx.recv() => { - match incoming_frame { - Ok(frame) => { - let packed = pack_ws_frame(&frame.into_packed()); - - if let Err(e) = socket.send(Message::Binary(packed)).await { - if is_normal_socket_disconnect(&e) { - tracing::debug!("Frame send stopped because socket closed: {:?}", e); - } else { - tracing::error!("Failed to send frame to socket: {:?}", e); - } - break; - } - } - Err(broadcast::error::RecvError::Closed) => { - tracing::error!( - "Connection has been lost! Shutting down websocket server" - ); - break; - } - Err(broadcast::error::RecvError::Lagged(_skipped)) => { - continue; - } - } - } - } - } - - let elapsed = now.elapsed(); - tracing::info!("Websocket closing after {elapsed:.2?}"); - } - - let cancel_token = CancellationToken::new(); - let server_shutdown = cancel_token.child_token(); - let router = axum::Router::new() - .route("/", get(ws_handler)) - .with_state((frame_tx, server_shutdown.clone())); - let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await { - Ok(listener) => listener, - Err(err) => { - tracing::error!("Failed to bind frame websocket listener: {err}"); - cancel_token.cancel(); - return (0, cancel_token); - } - }; - let port = match listener.local_addr() { - Ok(addr) => addr.port(), - Err(err) => { - tracing::error!("Failed to read frame websocket listener address: {err}"); - cancel_token.cancel(); - return (0, cancel_token); - } - }; - tracing::info!("WebSocket server listening on port {}", port); - - tokio::spawn(async move { - let _shutdown_guard = server_shutdown.clone().drop_guard(); - let server = axum::serve(listener, router.into_make_service()) - .with_graceful_shutdown(server_shutdown.clone().cancelled_owned()); - tokio::select! { - biased; - _ = server_shutdown.cancelled() => { - tracing::info!("WebSocket server shutting down"); - }, - _ = server => {}, - } - }); - - (port, cancel_token) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn frame(format: WSFrameFormat) -> WSFrame { - WSFrame { - data: Arc::new(vec![1, 2, 3, 4, 5, 6]).into(), - width: 2, - height: 2, - stride: 2, - frame_number: 7, - target_time_ns: 8, - format, - created_at: Instant::now(), - } - } - - #[test] - fn packs_rgba_frame_with_legacy_metadata_shape() { - let packed = pack_ws_frame(&frame(WSFrameFormat::Rgba)); - - assert_eq!(packed.len(), 30); - assert_eq!(&packed[..6], &[1, 2, 3, 4, 5, 6]); - assert_eq!(u32::from_le_bytes(packed[6..10].try_into().unwrap()), 2); - assert_eq!(u32::from_le_bytes(packed[18..22].try_into().unwrap()), 7); - assert_eq!(u64::from_le_bytes(packed[22..30].try_into().unwrap()), 8); - } - - #[test] - fn packs_nv12_frame_with_video_range_marker() { - let packed = pack_ws_frame(&frame(WSFrameFormat::Nv12 { full_range: false })); - - assert_eq!(packed.len(), 34); - assert_eq!( - u32::from_le_bytes(packed[30..34].try_into().unwrap()), - NV12_VIDEO_FORMAT_MAGIC - ); - } - - #[test] - fn owned_frame_packing_reuses_pixels_and_replay_shares_the_packet() { - for format in [ - WSFrameFormat::Rgba, - WSFrameFormat::Nv12 { full_range: false }, - WSFrameFormat::Nv12 { full_range: true }, - ] { - let mut frame = frame(format); - let mut pixels = Vec::with_capacity(6 + 28); - pixels.extend_from_slice(&[1, 2, 3, 4, 5, 6]); - let allocation = pixels.as_ptr(); - frame.data = Arc::new(pixels).into(); - let expected = pack_ws_frame(&frame); - let packed_frame = frame.into_packed(); - let first = pack_ws_frame(&packed_frame); - let replay = pack_ws_frame(&packed_frame.clone().into_packed()); - assert_eq!(first, expected); - assert_eq!(replay, expected); - assert_eq!(first.as_ptr(), allocation); - assert_eq!(replay.as_ptr(), allocation); - } - } - - #[test] - fn packing_shared_pixels_preserves_other_readers_and_camera_pool_contents() { - let pixels = Arc::new(vec![1, 2, 3, 4, 5, 6]); - let mut frame = frame(WSFrameFormat::Rgba); - frame.data = pixels.clone().into(); - let expected = pack_ws_frame(&frame); - let packed = pack_ws_frame(&frame.into_packed()); - assert_eq!(packed, expected); - assert_eq!(pixels.as_slice(), &[1, 2, 3, 4, 5, 6]); - assert_ne!(packed.as_ptr(), pixels.as_ptr()); - } - - #[test] - fn subscriber_guard_decrements_both_counts() { - let subscribers = Arc::new(AtomicUsize::new(1)); - let instant_subscribers = Arc::new(AtomicUsize::new(1)); - - drop(SubscriberCountGuard { - subscribers: subscribers.clone(), - instant_subscribers: Some(instant_subscribers.clone()), - }); - - assert_eq!(subscribers.load(Ordering::Acquire), 0); - assert_eq!(instant_subscribers.load(Ordering::Acquire), 0); - } -} - -#[cfg(test)] -mod shutdown_tests { - use super::*; - use std::time::Duration; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use tokio::net::{TcpListener, TcpStream}; - - fn frame() -> WSFrame { - WSFrame { - data: Arc::new(vec![1, 2, 3, 4]).into(), - width: 1, - height: 1, - stride: 4, - frame_number: 42, - target_time_ns: 1234, - format: WSFrameFormat::Rgba, - created_at: Instant::now(), - } - } - - async fn wait_until(condition: impl Fn() -> bool) { - tokio::time::timeout(Duration::from_secs(2), async { - while !condition() { - tokio::time::sleep(Duration::from_millis(5)).await; - } - }) - .await - .expect("shutdown condition did not settle"); - } - - async fn connect(port: u16, instant: bool) -> TcpStream { - assert_ne!(port, 0); - tokio::time::timeout(Duration::from_secs(2), async { - let mut stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); - let path = if instant { "/?instant=true" } else { "/" }; - let request = format!( - "GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n" - ); - stream.write_all(request.as_bytes()).await.unwrap(); - let mut response = Vec::new(); - while !response.ends_with(b"\r\n\r\n") { - assert!(response.len() < 4096); - response.push(stream.read_u8().await.unwrap()); - } - assert!(response.starts_with(b"HTTP/1.1 101")); - stream - }) - .await - .expect("websocket handshake timed out") - } - - async fn read_binary(stream: &mut TcpStream) -> Vec { - tokio::time::timeout(Duration::from_secs(2), async { - assert_eq!(stream.read_u8().await.unwrap(), 0x82); - let length = stream.read_u8().await.unwrap(); - assert!(length < 126); - let mut data = vec![0; usize::from(length)]; - stream.read_exact(&mut data).await.unwrap(); - data - }) - .await - .expect("frame delivery timed out") - } - - async fn assert_disconnected(stream: &mut TcpStream) { - tokio::time::timeout(Duration::from_secs(2), async { - let mut buffer = [0; 8192]; - loop { - match stream.read(&mut buffer).await { - Ok(0) | Err(_) => break, - Ok(_) => {} - } - } - }) - .await - .expect("connected websocket did not close"); - } - - async fn assert_listener_closed(port: u16) { - // Windows can take just over two seconds to report a refused loopback connection. - let timeout = Duration::from_secs(if cfg!(windows) { 5 } else { 2 }); - tokio::time::timeout(timeout, async { - while TcpStream::connect(("127.0.0.1", port)).await.is_ok() { - tokio::time::sleep(Duration::from_millis(5)).await; - } - }) - .await - .expect("websocket listener still accepts connections"); - } - - async fn assert_unused_port_released(port: u16) { - let listener = tokio::time::timeout(Duration::from_secs(2), async { - loop { - if let Ok(listener) = TcpListener::bind(("127.0.0.1", port)).await { - break listener; - } - tokio::time::sleep(Duration::from_millis(5)).await; - } - }) - .await - .expect("websocket listener still owns its port"); - drop(listener); - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn watch_listener_releases_port_after_cancel_before_server_poll() { - let (_tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - shutdown.cancel(); - assert_unused_port_released(port).await; - } - - #[tokio::test] - async fn broadcast_listener_releases_port_after_cancel_before_server_poll() { - let (tx, _rx) = broadcast::channel(2); - let (port, shutdown) = create_frame_ws(tx).await; - shutdown.cancel(); - assert_unused_port_released(port).await; - } - - #[tokio::test] - async fn watch_shutdown_drains_existing_clients_and_subscriber_counts() { - let (tx, rx) = watch::channel(None); - let subscribers = Arc::new(AtomicUsize::new(0)); - let instant = Arc::new(AtomicUsize::new(0)); - let (port, shutdown) = - create_watch_frame_ws_with_instant_tracking(rx, subscribers.clone(), instant.clone()) - .await; - let mut first = connect(port, false).await; - let mut second = connect(port, true).await; - wait_until(|| subscribers.load(Ordering::Acquire) == 2).await; - assert_eq!(instant.load(Ordering::Acquire), 1); - let frame = frame(); - let expected = pack_ws_frame(&frame); - tx.send(Some(Arc::new(frame))).unwrap(); - assert_eq!(read_binary(&mut first).await, expected); - assert_eq!(read_binary(&mut second).await, expected); - shutdown.cancel(); - assert_disconnected(&mut first).await; - assert_disconnected(&mut second).await; - wait_until(|| subscribers.load(Ordering::Acquire) == 0 && tx.receiver_count() == 0).await; - assert_eq!(instant.load(Ordering::Acquire), 0); - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn broadcast_shutdown_drains_existing_clients() { - let (tx, rx) = broadcast::channel(2); - drop(rx); - let (port, shutdown) = create_frame_ws(tx.clone()).await; - let mut first = connect(port, false).await; - let mut second = connect(port, false).await; - let frame = frame(); - let expected = pack_ws_frame(&frame); - assert!(matches!(tx.send(frame), Ok(2))); - assert_eq!(read_binary(&mut first).await, expected); - assert_eq!(read_binary(&mut second).await, expected); - shutdown.cancel(); - assert_disconnected(&mut first).await; - assert_disconnected(&mut second).await; - wait_until(|| tx.receiver_count() == 0).await; - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn packed_frames_replay_to_late_clients_and_deliver_replacements() { - let initial = frame(); - let expected_initial = pack_ws_frame(&initial); - let (tx, rx) = watch::channel(Some(Arc::new(initial.into_packed()))); - let subscribers = Arc::new(AtomicUsize::new(0)); - let (port, shutdown) = create_watch_frame_ws(rx, subscribers.clone()).await; - let mut first = connect(port, false).await; - assert_eq!(read_binary(&mut first).await, expected_initial); - let mut second = connect(port, false).await; - assert_eq!(read_binary(&mut second).await, expected_initial); - let mut replacement = frame(); - replacement.frame_number += 1; - replacement.target_time_ns += 1; - replacement.data = Arc::new(vec![5, 6, 7, 8]).into(); - let expected_replacement = pack_ws_frame(&replacement); - tx.send(Some(Arc::new(replacement.into_packed()))).unwrap(); - assert_eq!(read_binary(&mut first).await, expected_replacement); - assert_eq!(read_binary(&mut second).await, expected_replacement); - shutdown.cancel(); - assert_disconnected(&mut first).await; - assert_disconnected(&mut second).await; - wait_until(|| subscribers.load(Ordering::Acquire) == 0 && tx.receiver_count() == 0).await; - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn watch_source_close_does_not_resend_retained_frame() { - let frame = Arc::new(frame()); - let (tx, rx) = watch::channel(Some(frame.clone())); - let subscribers = Arc::new(AtomicUsize::new(0)); - let (port, shutdown) = create_watch_frame_ws(rx, subscribers.clone()).await; - let mut client = connect(port, false).await; - assert_eq!(read_binary(&mut client).await, pack_ws_frame(&frame)); - drop(tx); - wait_until(|| subscribers.load(Ordering::Acquire) == 0).await; - assert_disconnected(&mut client).await; - shutdown.cancel(); - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn watch_shutdown_interrupts_stalled_initial_send() { - let mut frame = frame(); - frame.width = 4096; - frame.height = 1024; - frame.stride = 4096 * 4; - let data = Arc::new(vec![0; frame.stride as usize * frame.height as usize]); - let weak_data = Arc::downgrade(&data); - frame.data = data.into(); - let (tx, rx) = watch::channel(Some(Arc::new(frame))); - let subscribers = Arc::new(AtomicUsize::new(0)); - let (port, shutdown) = create_watch_frame_ws(rx, subscribers.clone()).await; - let client = connect(port, false).await; - wait_until(|| subscribers.load(Ordering::Acquire) == 1).await; - tokio::time::sleep(Duration::from_millis(20)).await; - shutdown.cancel(); - wait_until(|| subscribers.load(Ordering::Acquire) == 0 && tx.receiver_count() == 0).await; - drop(tx); - assert!(weak_data.upgrade().is_none()); - drop(client); - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn cancelling_owner_child_does_not_stop_sibling_server() { - let (tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - shutdown.child_token().cancel(); - assert!(!shutdown.is_cancelled()); - let mut client = connect(port, false).await; - let frame = frame(); - let expected = pack_ws_frame(&frame); - tx.send(Some(Arc::new(frame))).unwrap(); - assert_eq!(read_binary(&mut client).await, expected); - shutdown.cancel(); - assert_disconnected(&mut client).await; - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn dropping_owner_token_preserves_app_scoped_camera_server() { - let (tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - drop(shutdown); - let mut client = connect(port, false).await; - let frame = frame(); - let expected = pack_ws_frame(&frame); - tx.send(Some(Arc::new(frame))).unwrap(); - assert_eq!(read_binary(&mut client).await, expected); - } - - #[tokio::test] - async fn aborted_construction_drop_guard_releases_listener() { - let (port_tx, port_rx) = tokio::sync::oneshot::channel(); - let construction = tokio::spawn(async move { - let (_tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - let _guard = shutdown.clone().drop_guard(); - port_tx.send(port).unwrap(); - std::future::pending::<()>().await; - }); - let port = port_rx.await.unwrap(); - construction.abort(); - assert!(construction.await.unwrap_err().is_cancelled()); - assert_unused_port_released(port).await; - } - - #[tokio::test] - async fn successful_construction_disarms_guard_until_owner_cancels() { - let (tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - let guard = shutdown.clone().drop_guard(); - drop(guard.disarm()); - let mut client = connect(port, false).await; - let frame = frame(); - let expected = pack_ws_frame(&frame); - tx.send(Some(Arc::new(frame))).unwrap(); - assert_eq!(read_binary(&mut client).await, expected); - shutdown.cancel(); - assert_disconnected(&mut client).await; - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn repeated_watch_open_cancel_releases_every_port() { - for _ in 0..20 { - let (_tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - let mut client = connect(port, false).await; - shutdown.cancel(); - assert_disconnected(&mut client).await; - assert_listener_closed(port).await; - } - } - - async fn connect_http_client(port: u16) -> TcpStream { - tokio::time::timeout(Duration::from_secs(2), async { - let mut client = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); - client - .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") - .await - .unwrap(); - let mut response = Vec::new(); - while !response.ends_with(b"\r\n\r\n") { - assert!(response.len() < 4096); - response.push(client.read_u8().await.unwrap()); - } - assert!(response.starts_with(b"HTTP/1.1 400")); - let headers = std::str::from_utf8(&response).unwrap(); - let length = headers - .lines() - .filter_map(|line| line.split_once(':')) - .find(|(name, _)| name.eq_ignore_ascii_case("content-length")) - .unwrap() - .1 - .trim() - .parse::() - .unwrap(); - assert!(length < 4096); - client.read_exact(&mut vec![0; length]).await.unwrap(); - client - }) - .await - .expect("HTTP connection did not become ready") - } - - #[tokio::test] - async fn watch_shutdown_drains_incomplete_http_upgrade() { - let (tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - let mut client = connect_http_client(port).await; - client - .write_all(b"GET / HTTP/1.1\r\nUpgrade: websocket\r\n") - .await - .unwrap(); - tokio::task::yield_now().await; - shutdown.cancel(); - assert_disconnected(&mut client).await; - wait_until(|| tx.receiver_count() == 0).await; - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn broadcast_shutdown_drains_incomplete_http_upgrade() { - let (tx, rx) = broadcast::channel(2); - drop(rx); - let (port, shutdown) = create_frame_ws(tx.clone()).await; - let mut client = connect_http_client(port).await; - client - .write_all(b"GET / HTTP/1.1\r\nUpgrade: websocket\r\n") - .await - .unwrap(); - tokio::task::yield_now().await; - shutdown.cancel(); - assert_disconnected(&mut client).await; - wait_until(|| tx.strong_count() == 1).await; - assert_listener_closed(port).await; - } - - #[tokio::test] - async fn watch_shutdown_drains_http_keep_alive_connection() { - let (tx, rx) = watch::channel(None); - let (port, shutdown) = create_watch_frame_ws(rx, Default::default()).await; - let mut client = connect_http_client(port).await; - shutdown.cancel(); - assert_disconnected(&mut client).await; - wait_until(|| tx.receiver_count() == 0).await; - assert_listener_closed(port).await; - } -} - -pub(crate) struct OwnedWatchFrameWs { - pub(crate) url: String, - shutdown: CancellationToken, - task: Option>>, -} - -impl OwnedWatchFrameWs { - pub(crate) fn is_finished(&self) -> bool { - self.task - .as_ref() - .is_none_or(tokio::task::JoinHandle::is_finished) - } - - pub(crate) async fn stop_and_wait(mut self) -> Result<(), String> { - self.shutdown.cancel(); - self.task - .take() - .ok_or_else(|| "Preparing frame transport already joined".to_string())? - .await - .map_err(|error| format!("Preparing frame transport join failed: {error}"))? - } -} - -impl Drop for OwnedWatchFrameWs { - fn drop(&mut self) { - self.shutdown.cancel(); - } -} - -pub(crate) async fn create_owned_watch_frame_ws( - frame_rx: watch::Receiver>>, - accepts: Arc bool + Send + Sync>, -) -> Result { - use futures::{SinkExt, StreamExt}; - use tokio_tungstenite::tungstenite::Message; - - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .map_err(|error| error.to_string())?; - let port = listener - .local_addr() - .map_err(|error| error.to_string())? - .port(); - let route = format!("/{}", uuid::Uuid::new_v4()); - let url = format!("ws://127.0.0.1:{port}{route}"); - let shutdown = CancellationToken::new(); - let cancelled = shutdown.clone(); - let task = tokio::spawn(async move { - let mut connections = tokio::task::JoinSet::new(); - let mut failure = None; - loop { - tokio::select! { - biased; - _ = cancelled.cancelled() => break, - joined = connections.join_next(), if !connections.is_empty() => { - if let Some(Err(error)) = joined { - failure = Some(format!("Preparing frame connection join failed: {error}")); - break; - } - } - accepted = listener.accept() => { - let (stream, _) = match accepted { - Ok(value) => value, - Err(error) => { - tracing::debug!(%error, "Preparing frame transport accept stopped"); - break; - } - }; - if connections.len() >= 8 { - drop(stream); - continue; - } - let expected = route.clone(); - let mut frames = frame_rx.clone(); - let accepts = accepts.clone(); - let cancelled = cancelled.clone(); - connections.spawn(async move { - let exchange = async move { - let Ok(mut socket) = tokio_tungstenite::accept_hdr_async( - stream, - move |request: &tokio_tungstenite::tungstenite::handshake::server::Request, response| { - if request.uri().path() == expected && request.uri().query().is_none() { - Ok(response) - } else { - Err(tokio_tungstenite::tungstenite::http::Response::builder().status(404).body(None).unwrap()) - } - }, - ).await else { return; }; - loop { - let frame = frames.borrow_and_update().clone(); - if let Some(frame) = frame - && accepts() - && socket.send(Message::Binary(pack_ws_frame(&frame).to_vec())).await.is_err() - { - return; - } - tokio::select! { - changed = frames.changed() => if changed.is_err() { return; }, - message = socket.next() => match message { - None | Some(Err(_)) | Some(Ok(Message::Close(_))) => return, - _ => {} - } - } - } - }; - tokio::select! { - biased; - _ = cancelled.cancelled() => {}, - _ = exchange => {}, - } - }); - } - } - } - cancelled.cancel(); - drop(listener); - while let Some(joined) = connections.join_next().await { - if let Err(error) = joined { - failure.get_or_insert_with(|| { - format!("Preparing frame connection join failed: {error}") - }); - } - } - failure.map_or(Ok(()), Err) - }); - Ok(OwnedWatchFrameWs { - url, - shutdown, - task: Some(task), - }) -} - -#[cfg(test)] -mod owned_preparing_tests { - use super::*; - use futures::StreamExt; - use tokio::io::AsyncWriteExt; - - #[tokio::test] - async fn owned_transport_joins_incomplete_handshake_and_releases_port() { - let (_frames, rx) = watch::channel(None); - let server = create_owned_watch_frame_ws(rx, Arc::new(|| true)) - .await - .unwrap(); - let address = server - .url - .strip_prefix("ws://") - .unwrap() - .split('/') - .next() - .unwrap() - .to_string(); - let mut client = tokio::net::TcpStream::connect(&address).await.unwrap(); - client.write_all(b"GET / HTTP/1.1\r\n").await.unwrap(); - tokio::task::yield_now().await; - tokio::time::timeout(std::time::Duration::from_secs(2), server.stop_and_wait()) - .await - .unwrap() - .unwrap(); - let listener = tokio::net::TcpListener::bind(address).await.unwrap(); - drop(listener); - } - - #[tokio::test] - async fn owned_transport_keeps_exact_frame_encoding_and_joins_live_client() { - let frame = Arc::new(WSFrame { - data: Arc::new(vec![1, 2, 3, 4]).into(), - width: 1, - height: 1, - stride: 4, - frame_number: 0, - target_time_ns: 0, - format: WSFrameFormat::Rgba, - created_at: Instant::now(), - }); - let weak = Arc::downgrade(&frame); - let expected = pack_ws_frame(&frame); - let (frames, rx) = watch::channel(Some(frame)); - let server = create_owned_watch_frame_ws(rx, Arc::new(|| true)) - .await - .unwrap(); - let (mut socket, _) = tokio_tungstenite::connect_async(&server.url).await.unwrap(); - assert_eq!(socket.next().await.unwrap().unwrap().into_data(), expected); - tokio::time::timeout(std::time::Duration::from_secs(2), server.stop_and_wait()) - .await - .unwrap() - .unwrap(); - drop(frames); - assert!(weak.upgrade().is_none()); - } - - #[tokio::test] - async fn owned_transport_cancellation_joins_stalled_large_frame_send() { - let frame = Arc::new(WSFrame { - data: Arc::new(vec![1; 16 * 1024 * 1024]).into(), - width: 2048, - height: 2048, - stride: 8192, - frame_number: 0, - target_time_ns: 0, - format: WSFrameFormat::Rgba, - created_at: Instant::now(), - }); - let weak = Arc::downgrade(&frame); - let (frames, rx) = watch::channel(Some(frame)); - let server = create_owned_watch_frame_ws(rx, Arc::new(|| true)) - .await - .unwrap(); - let (socket, _) = tokio_tungstenite::connect_async(&server.url).await.unwrap(); - tokio::time::sleep(std::time::Duration::from_millis(25)).await; - tokio::time::timeout(std::time::Duration::from_secs(2), server.stop_and_wait()) - .await - .unwrap() - .unwrap(); - drop(socket); - drop(frames); - assert!(weak.upgrade().is_none()); - } -} diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs deleted file mode 100644 index 5a1c296f1ff..00000000000 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ /dev/null @@ -1,999 +0,0 @@ -use crate::updates::UpdateChannel; -use crate::window_exclusion::WindowExclusion; -use scap_targets::DisplayId; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; -use specta::Type; -use std::collections::BTreeMap; -use std::io::Write; -use std::path::{Path, PathBuf}; -#[cfg(target_os = "macos")] -use tauri::Listener; -use tauri::{AppHandle, Manager, Wry}; -use tauri_plugin_store::StoreExt; -use tracing::{error, instrument}; -use uuid::Uuid; - -#[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum PostStudioRecordingBehaviour { - #[default] - OpenEditor, - ShowOverlay, -} - -#[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum MainWindowRecordingStartBehaviour { - #[default] - Close, - Minimise, -} - -#[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum PostDeletionBehaviour { - #[default] - DoNothing, - ReopenRecordingWindow, -} - -#[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum EditorPreviewQuality { - Quarter, - #[default] - Half, - Full, -} - -#[derive(Serialize, Deserialize, Type, Debug, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum StudioRecordingQuality { - Compatibility, - Balanced, - Ultra, -} - -impl Default for StudioRecordingQuality { - fn default() -> Self { - default_studio_recording_quality() - } -} - -impl From for StudioRecordingQuality { - fn from(value: cap_recording::StudioQuality) -> Self { - match value { - cap_recording::StudioQuality::Compatibility => Self::Compatibility, - cap_recording::StudioQuality::Balanced => Self::Balanced, - cap_recording::StudioQuality::Ultra => Self::Ultra, - } - } -} - -impl From for cap_recording::StudioQuality { - fn from(value: StudioRecordingQuality) -> Self { - match value { - StudioRecordingQuality::Compatibility => Self::Compatibility, - StudioRecordingQuality::Balanced => Self::Balanced, - StudioRecordingQuality::Ultra => Self::Ultra, - } - } -} - -pub fn default_studio_recording_quality() -> StudioRecordingQuality { - cap_recording::default_studio_recording_quality().into() -} - -impl MainWindowRecordingStartBehaviour { - pub fn perform(&self, window: &tauri::WebviewWindow) -> tauri::Result<()> { - match self { - Self::Close => { - // On Windows, hide() leaves the DirectComposition surface composited on screen as - // a white ghost box. minimize() releases the surface without leaving an artifact. - #[cfg(windows)] - return window.minimize(); - #[cfg(not(windows))] - { - crate::hide_main_window(window.app_handle()); - Ok(()) - } - } - Self::Minimise => window.minimize(), - } - } -} - -// NOTE: Do not add "Cap Target Select" here — on Windows, WDA_EXCLUDEFROMCAPTURE applied to that -// hidden window causes it to reappear as a ghost overlay after recording ends. -const DEFAULT_EXCLUDED_WINDOW_TITLES: &[&str] = &[ - "Cap", - "Cap Settings", - "Cap Recording Controls", - "Cap Camera", - "Cap Window Capture Occluder", - "Cap Capture Area", - "Cap Mode Selection", - "Cap Recordings Overlay", - "Cap Teleprompter", -]; - -pub fn default_excluded_windows() -> Vec { - DEFAULT_EXCLUDED_WINDOW_TITLES - .iter() - .map(|title| WindowExclusion { - bundle_identifier: None, - owner_name: None, - window_title: Some((*title).to_string()), - }) - .collect() -} - -fn append_missing_default_excluded_windows(excluded_windows: &mut Vec) -> bool { - let mut changed = false; - - for default in default_excluded_windows() { - if !excluded_windows.contains(&default) { - excluded_windows.push(default); - changed = true; - } - } - - changed -} - -// When adding fields here, #[serde(default)] defines the value to use for existing configurations, -// and `Default::default` defines the value to use for new configurations. -// Things that affect the user experience should only be enabled by default for new configurations. -#[derive(Serialize, Deserialize, Type, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WindowPosition { - pub x: f64, - pub y: f64, - #[serde(default)] - pub display_id: Option, -} - -#[derive(Serialize, Deserialize, Type, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct GeneralSettingsStore { - #[serde(default = "uuid::Uuid::new_v4")] - pub instance_id: Uuid, - #[serde(default)] - pub upload_individual_files: bool, - #[serde(default)] - pub hide_dock_icon: bool, - #[serde(default)] - pub auto_create_shareable_link: bool, - #[serde(default = "default_true")] - pub enable_notifications: bool, - #[serde(default)] - pub disable_auto_open_links: bool, - #[serde(default = "default_true")] - pub has_completed_startup: bool, - #[serde(default)] - pub theme: AppTheme, - #[serde(default)] - pub commercial_license: Option, - #[serde(default)] - pub last_version: Option, - #[serde(default)] - pub window_transparency: bool, - #[serde(default)] - pub post_studio_recording_behaviour: PostStudioRecordingBehaviour, - #[serde(default)] - pub main_window_recording_start_behaviour: MainWindowRecordingStartBehaviour, - #[serde( - default = "default_custom_cursor_capture", - rename = "custom_cursor_capture2" - )] - pub custom_cursor_capture: bool, - #[serde(default = "default_server_url")] - pub server_url: String, - #[serde(default)] - pub recording_countdown: Option, - #[serde( - default = "default_enable_native_camera_preview", - skip_serializing_if = "no" - )] - pub enable_native_camera_preview: bool, - #[serde(default = "default_true")] - pub auto_zoom_on_clicks: bool, - #[serde(default)] - pub default_zoom_amount: Option, - /// `None` until [`init`] seeds it from whether this machine has a notched - /// display. From then on it is the user's preference and nothing re-reads - /// the hardware, so moving between machines can't silently flip it. - #[serde(default)] - pub macbook_notch_overlay: Option, - #[serde(default = "default_capture_keyboard_events")] - pub capture_keyboard_events: bool, - #[serde(default)] - pub post_deletion_behaviour: PostDeletionBehaviour, - #[serde(default = "default_excluded_windows")] - pub excluded_windows: Vec, - #[serde(default)] - pub delete_instant_recordings_after_upload: bool, - #[serde(default = "default_instant_mode_max_resolution")] - pub instant_mode_max_resolution: u32, - #[serde(default)] - pub default_project_name_template: Option, - #[serde(default = "default_crash_recovery_recording")] - pub crash_recovery_recording: bool, - #[serde(default = "default_max_fps")] - pub max_fps: u32, - #[serde(default = "default_transcription_hints")] - pub transcription_hints: Vec, - #[serde(default)] - pub editor_preview_quality: EditorPreviewQuality, - #[serde(default)] - pub studio_recording_quality: StudioRecordingQuality, - #[serde(default)] - pub main_window_position: Option, - #[serde(default)] - pub camera_window_position: Option, - #[serde(default)] - pub camera_window_positions_by_monitor_name: BTreeMap, - #[serde(default = "default_true")] - pub has_completed_onboarding: bool, - #[serde(default = "default_true")] - pub enable_telemetry: bool, - #[serde(default)] - pub out_of_process_muxer: bool, - #[serde(default)] - pub recordings_path: Option, - /// Custom recordings folders that were used before; recordings left in - /// them stay visible in the library. Most recent last. - #[serde(default)] - pub previous_recordings_paths: Vec, - /// App version at which camera background blur was disabled after a crash - /// was attributed to the blur pipeline; `None` means blur is allowed. - /// Cleared automatically when the app version changes (one retry per - /// update, since a new ort/wgpu/driver stack may have fixed the crash). - #[serde(default)] - pub camera_blur_disabled_by_crash: Option, - #[serde(default)] - pub update_channel: UpdateChannel, - /// Run the experimental gpui-native app (`cap-gpui`) *instead of* this one: - /// while enabled, startup hands off to it and exits, and the native app's - /// own Experimental page hands back. See `gpui_app.rs`. - #[serde(default)] - pub enable_gpui_app: bool, -} - -fn default_enable_native_camera_preview() -> bool { - false -} - -fn no(_: &bool) -> bool { - false -} - -fn default_true() -> bool { - true -} - -fn default_instant_mode_max_resolution() -> u32 { - cap_recording::DEFAULT_INSTANT_MODE_MAX_RESOLUTION -} - -fn default_max_fps() -> u32 { - cap_recording::DEFAULT_STUDIO_MAX_FPS -} - -fn default_custom_cursor_capture() -> bool { - cap_recording::DEFAULT_CUSTOM_CURSOR_CAPTURE -} - -fn default_capture_keyboard_events() -> bool { - cap_recording::DEFAULT_CAPTURE_KEYBOARD_EVENTS -} - -fn default_crash_recovery_recording() -> bool { - cap_recording::DEFAULT_CRASH_RECOVERY_RECORDING -} - -fn default_transcription_hints() -> Vec { - vec![ - "Cap".to_string(), - "TypeScript".to_string(), - "My Brand Name".to_string(), - "mywebsite.com".to_string(), - ] -} - -fn default_server_url() -> String { - std::option_env!("VITE_SERVER_URL") - .unwrap_or("https://cap.so") - .to_string() -} - -#[derive(Serialize, Deserialize, Type, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct CommercialLicense { - license_key: String, - expiry_date: Option, - refresh: f64, - activated_on: f64, -} - -impl Default for GeneralSettingsStore { - fn default() -> Self { - Self { - instance_id: uuid::Uuid::new_v4(), - upload_individual_files: false, - hide_dock_icon: false, - auto_create_shareable_link: false, - enable_notifications: true, - disable_auto_open_links: false, - has_completed_startup: false, - theme: AppTheme::System, - commercial_license: None, - last_version: None, - window_transparency: false, - post_studio_recording_behaviour: PostStudioRecordingBehaviour::OpenEditor, - main_window_recording_start_behaviour: MainWindowRecordingStartBehaviour::Close, - custom_cursor_capture: cap_recording::DEFAULT_CUSTOM_CURSOR_CAPTURE, - server_url: default_server_url(), - recording_countdown: Some(3), - enable_native_camera_preview: default_enable_native_camera_preview(), - // Keep aligned with the field's serde `default_true`: auto zooms - // are on by default, matching configs that never stored the key. - auto_zoom_on_clicks: true, - default_zoom_amount: None, - macbook_notch_overlay: None, - capture_keyboard_events: cap_recording::DEFAULT_CAPTURE_KEYBOARD_EVENTS, - post_deletion_behaviour: PostDeletionBehaviour::DoNothing, - excluded_windows: default_excluded_windows(), - delete_instant_recordings_after_upload: false, - instant_mode_max_resolution: cap_recording::DEFAULT_INSTANT_MODE_MAX_RESOLUTION, - default_project_name_template: None, - crash_recovery_recording: cap_recording::DEFAULT_CRASH_RECOVERY_RECORDING, - max_fps: cap_recording::DEFAULT_STUDIO_MAX_FPS, - transcription_hints: default_transcription_hints(), - editor_preview_quality: EditorPreviewQuality::Half, - studio_recording_quality: default_studio_recording_quality(), - main_window_position: None, - camera_window_position: None, - camera_window_positions_by_monitor_name: BTreeMap::new(), - has_completed_onboarding: false, - enable_telemetry: true, - out_of_process_muxer: cap_recording::DEFAULT_OUT_OF_PROCESS_MUXER, - recordings_path: None, - previous_recordings_paths: Vec::new(), - camera_blur_disabled_by_crash: None, - update_channel: UpdateChannel::Stable, - enable_gpui_app: false, - } - } -} - -#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub enum AppTheme { - #[default] - System, - Light, - Dark, -} - -struct GeneralSettingsSnapshot { - original: Option, - settings: GeneralSettingsStore, - invalid_fields: Vec, -} - -impl GeneralSettingsSnapshot { - fn load(original: Option) -> Result { - let Some(raw) = original.as_ref() else { - return Ok(Self { - original, - settings: GeneralSettingsStore::default(), - invalid_fields: Vec::new(), - }); - }; - if raw.is_object() - && let Ok(settings) = serde_json::from_value(raw.clone()) - { - return Ok(Self { - original, - settings, - invalid_fields: Vec::new(), - }); - } - - let mut invalid_fields = Vec::new(); - let settings = if let Some(fields) = raw.as_object() { - let mut recovered = fields.clone(); - for (key, value) in fields { - let field = Value::Object([(key.clone(), value.clone())].into_iter().collect()); - if serde_json::from_value::(field).is_err() { - let _ = recovered.remove(key); - invalid_fields.push(key.clone()); - } - } - serde_json::from_value(Value::Object(recovered)) - .map_err(|_| "Could not recover general settings fields".to_string())? - } else { - invalid_fields.push("general_settings".to_string()); - GeneralSettingsStore::default() - }; - - Ok(Self { - original, - settings, - invalid_fields, - }) - } - - fn persist( - &self, - settings: &GeneralSettingsStore, - backup: impl FnOnce(&Value) -> Result<(), String>, - save: impl FnOnce(Value) -> Result<(), String>, - ) -> Result<(), String> { - let before = serde_json::to_value(&self.settings).map_err(|error| error.to_string())?; - let after = serde_json::to_value(settings).map_err(|error| error.to_string())?; - let mut original = self.original.clone().unwrap_or(Value::Null); - if let Some(fields) = original.as_object_mut() { - for key in &self.invalid_fields { - let _ = fields.remove(key); - } - } - let merged = merge_settings_changes(&original, &before, &after); - - if !self.invalid_fields.is_empty() - && let Some(raw) = self.original.as_ref() - { - backup(raw)?; - } - save(merged) - } -} - -fn merge_settings_changes(original: &Value, before: &Value, after: &Value) -> Value { - if let (Some(original), Some(before), Some(after)) = - (original.as_object(), before.as_object(), after.as_object()) - { - let mut merged = original.clone(); - for (key, value) in after { - let next = match (original.get(key), before.get(key)) { - (Some(original), Some(before)) if before == value => original.clone(), - (Some(original), Some(before)) => merge_settings_changes(original, before, value), - _ => value.clone(), - }; - let _ = merged.insert(key.clone(), next); - } - for key in before.keys() { - if !after.contains_key(key) { - let _ = merged.remove(key); - } - } - Value::Object(merged) - } else if let (Some(original), Some(before), Some(after)) = - (original.as_array(), before.as_array(), after.as_array()) - { - let mut used = vec![false; before.len()]; - Value::Array( - after - .iter() - .map(|value| { - let matching = before - .iter() - .enumerate() - .find(|(index, previous)| !used[*index] && *previous == value); - if let Some((index, _)) = matching { - used[index] = true; - original - .get(index) - .cloned() - .unwrap_or_else(|| value.clone()) - } else { - value.clone() - } - }) - .collect(), - ) - } else { - after.clone() - } -} - -fn backup_general_settings(directory: &Path, original: &Value) -> Result { - std::fs::create_dir_all(directory).map_err(|error| error.to_string())?; - let path = directory.join(format!("general-settings-recovery-{}.json", Uuid::new_v4())); - let mut options = std::fs::OpenOptions::new(); - options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o600); - } - let mut file = options.open(&path).map_err(|error| error.to_string())?; - let bytes = serde_json::to_vec_pretty(original).map_err(|error| error.to_string())?; - file.write_all(&bytes).map_err(|error| error.to_string())?; - file.sync_all().map_err(|error| error.to_string())?; - Ok(path) -} - -fn persist_general_settings( - app: &AppHandle, - store: &tauri_plugin_store::Store, - snapshot: &GeneralSettingsSnapshot, - settings: &GeneralSettingsStore, -) -> Result<(), String> { - snapshot.persist( - settings, - |original| { - let directory = app - .path() - .app_data_dir() - .map_err(|error| error.to_string())?; - let path = backup_general_settings(&directory, original).map_err(|error| { - format!("Could not back up malformed general settings: {error}") - })?; - tracing::warn!( - fields = ?snapshot.invalid_fields, - backup = %path.display(), - "Recovered malformed general settings fields" - ); - Ok(()) - }, - |value| { - store.set("general_settings", value); - store.save().map_err(|error| error.to_string()) - }, - ) -} - -impl GeneralSettingsStore { - pub fn recordings_dir(app: &AppHandle) -> std::path::PathBuf { - let custom = Self::get(app) - .map_err(|e| tracing::warn!("Failed to read general settings for recordings_dir: {e}")) - .ok() - .flatten() - .and_then(|s| s.recordings_path) - .and_then(|p| { - let path = std::path::PathBuf::from(&p); - if path.is_absolute() { Some(path) } else { None } - }); - - // A custom folder can become unavailable (unplugged drive, deleted - // path). Recording must keep working, so fall back to the default - // location instead of failing; the library lists recordings from - // every known folder, so nothing goes missing when this happens. - if let Some(path) = custom { - match std::fs::create_dir_all(&path) { - Ok(()) => return path, - Err(e) => { - tracing::warn!( - ?path, %e, - "Custom recordings directory unavailable; falling back to default" - ); - } - } - } - - let path = app.path().app_data_dir().unwrap().join("recordings"); - if let Err(e) = std::fs::create_dir_all(&path) { - tracing::warn!(?path, %e, "Failed to create recordings directory"); - } - path - } - - // The effective value: the native preview is macOS-only; it is not - // reliable on Windows, so the stored setting is ignored there and the - // websocket preview is always used. - pub fn native_camera_preview_enabled(app: &AppHandle) -> bool { - if cfg!(not(target_os = "macos")) { - return false; - } - Self::get(app) - .ok() - .flatten() - .map(|settings| settings.enable_native_camera_preview) - .unwrap_or_else(default_enable_native_camera_preview) - } - - pub fn get(app: &AppHandle) -> Result, String> { - match app.store("store").map(|s| s.get("general_settings")) { - Ok(Some(raw)) => { - GeneralSettingsSnapshot::load(Some(raw)).map(|snapshot| Some(snapshot.settings)) - } - _ => Ok(None), - } - } - - // i don't trust anyone to not overwrite the whole store lols - pub fn update(app: &AppHandle, update: impl FnOnce(&mut Self)) -> Result<(), String> { - let Ok(store) = app.store("store") else { - return Err("Store not found".to_string()); - }; - - let snapshot = GeneralSettingsSnapshot::load(store.get("general_settings"))?; - let mut settings = snapshot.settings.clone(); - update(&mut settings); - persist_general_settings(app, &store, &snapshot, &settings)?; - - crate::telemetry::set_telemetry_enabled(settings.enable_telemetry); - - #[cfg(target_os = "macos")] - crate::permissions::sync_macos_dock_visibility(app); - - Ok(()) - } - - fn save(&self, app: &AppHandle) -> Result<(), String> { - let Ok(store) = app.store("store") else { - return Err("Store not found".to_string()); - }; - - let snapshot = GeneralSettingsSnapshot::load(store.get("general_settings"))?; - persist_general_settings(app, &store, &snapshot, self) - } -} - -#[cfg(target_os = "macos")] -#[derive(Deserialize)] -struct StoreChangePayload { - key: String, -} - -#[cfg(target_os = "macos")] -fn sync_dock_visibility_on_general_settings_change(app: &AppHandle) { - let app_for_listener = app.clone(); - app.listen("store://change", move |event| { - let Ok(payload) = serde_json::from_str::(event.payload()) else { - return; - }; - - if payload.key == "general_settings" { - crate::permissions::schedule_macos_dock_visibility_sync(&app_for_listener); - } - }); -} - -/// Always false off macOS, so the overlay starts off and waits to be asked for. -fn machine_has_notched_display() -> bool { - scap_targets::Display::list() - .iter() - .any(|display| display.notch().is_some()) -} - -pub fn init(app: &AppHandle) { - let mut store = match GeneralSettingsStore::get(app) { - Ok(Some(store)) => store, - Ok(None) => GeneralSettingsStore::default(), - Err(e) => { - error!("Failed to deserialize general settings store: {}", e); - GeneralSettingsStore::default() - } - }; - - append_missing_default_excluded_windows(&mut store.excluded_windows); - - if store.macbook_notch_overlay.is_none() { - store.macbook_notch_overlay = Some(machine_has_notched_display()); - } - - const REMOVE_TARGET_SELECT_MIGRATION_KEY: &str = "remove_cap_target_select_exclusion_v1"; - if let Ok(raw_store) = app.store("store") - && raw_store.get(REMOVE_TARGET_SELECT_MIGRATION_KEY).is_none() - { - store - .excluded_windows - .retain(|w| w.window_title.as_deref() != Some("Cap Target Select")); - raw_store.set(REMOVE_TARGET_SELECT_MIGRATION_KEY, json!(true)); - } - - crate::telemetry::set_telemetry_enabled(store.enable_telemetry); - register_bundled_muxer_binary(app); - - #[cfg(target_os = "macos")] - { - const NATIVE_PREVIEW_MIGRATION_KEY: &str = "native_camera_preview_default_rollback_v1"; - if let Ok(raw_store) = app.store("store") - && raw_store.get(NATIVE_PREVIEW_MIGRATION_KEY).is_none() - { - store.enable_native_camera_preview = false; - raw_store.set(NATIVE_PREVIEW_MIGRATION_KEY, json!(true)); - } - } - - if let Err(e) = store.save(app) { - error!("Failed to save general settings: {}", e); - } - - #[cfg(target_os = "macos")] - sync_dock_visibility_on_general_settings_change(app); - - #[cfg(target_os = "macos")] - crate::permissions::sync_macos_dock_visibility(app); -} - -fn register_bundled_muxer_binary(_app: &AppHandle) { - if std::env::var_os(cap_recording::oop_muxer::ENV_BIN_PATH).is_some() { - return; - } - - if let Ok(exe) = std::env::current_exe() - && let Some(dir) = exe.parent() - { - let candidate = dir.join(bundled_muxer_bin_name()); - if candidate.is_file() { - match cap_recording::oop_muxer::set_muxer_binary_override(candidate.clone()) { - Ok(()) => { - tracing::info!( - path = %candidate.display(), - "Registered executable-adjacent cap-muxer binary for out-of-process muxer" - ); - } - Err(existing) => { - tracing::debug!( - existing = %existing.display(), - candidate = %candidate.display(), - "cap-muxer override already registered; keeping existing" - ); - } - } - } - } -} - -fn bundled_muxer_bin_name() -> &'static str { - if cfg!(windows) { - "cap-muxer.exe" - } else { - "cap-muxer" - } -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub fn get_default_excluded_windows() -> Vec { - default_excluded_windows() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn settings_with_preserved_values() -> Value { - json!({ - "instanceId": "123e4567-e89b-42d3-a456-426614174000", - "commercialLicense": { - "licenseKey": "test-license", - "expiryDate": null, - "refresh": 123.0, - "activatedOn": 100.0, - "futureLicenseField": { "preserve": true } - }, - "recordingsPath": "/Volumes/Test Recordings", - "previousRecordingsPaths": ["/Volumes/Previous"], - "theme": "dark", - "futureSetting": { "preserve": [1, 2, 3] }, - "mainWindowPosition": { "x": 20.0, "y": 30.0, "futurePositionField": true }, - "excludedWindows": [{ "windowTitle": "Custom", "futureExclusionField": true }] - }) - } - - #[test] - fn malformed_field_recovery_preserves_identity_license_paths_and_unknown_fields() { - let mut original = settings_with_preserved_values(); - original["theme"] = json!({ "system": {}, "dark": {} }); - original["maxFps"] = json!("invalid"); - let snapshot = GeneralSettingsSnapshot::load(Some(original.clone())).unwrap(); - assert_eq!(snapshot.invalid_fields, ["maxFps", "theme"]); - assert_eq!( - snapshot.settings.instance_id.to_string(), - original["instanceId"] - ); - assert_eq!( - snapshot.settings.recordings_path.as_deref(), - original["recordingsPath"].as_str() - ); - snapshot - .persist( - &snapshot.settings, - |backup| { - assert_eq!(backup, &original); - Ok(()) - }, - |saved| { - for key in [ - "instanceId", - "commercialLicense", - "recordingsPath", - "previousRecordingsPaths", - "futureSetting", - "mainWindowPosition", - "excludedWindows", - ] { - assert_eq!(saved[key], original[key], "{key}"); - } - assert_eq!(saved["theme"], "system"); - assert_eq!(saved["maxFps"], cap_recording::DEFAULT_STUDIO_MAX_FPS); - assert!(serde_json::from_value::(saved).is_ok()); - Ok(()) - }, - ) - .unwrap(); - } - - #[test] - fn settings_updates_preserve_unknown_nested_fields_and_unchanged_array_entries() { - let original = settings_with_preserved_values(); - let snapshot = GeneralSettingsSnapshot::load(Some(original.clone())).unwrap(); - let mut updated = snapshot.settings.clone(); - updated.hide_dock_icon = true; - updated.main_window_position.as_mut().unwrap().x = 90.0; - updated.commercial_license.as_mut().unwrap().refresh = 456.0; - append_missing_default_excluded_windows(&mut updated.excluded_windows); - snapshot - .persist( - &updated, - |_| panic!("valid settings must not need recovery"), - |saved| { - assert_eq!(saved["hideDockIcon"], true); - assert_eq!(saved["mainWindowPosition"]["x"], 90.0); - assert_eq!(saved["mainWindowPosition"]["futurePositionField"], true); - assert_eq!(saved["commercialLicense"]["refresh"], 456.0); - assert_eq!( - saved["commercialLicense"]["futureLicenseField"], - original["commercialLicense"]["futureLicenseField"] - ); - assert_eq!(saved["futureSetting"], original["futureSetting"]); - assert_eq!(saved["excludedWindows"][0], original["excludedWindows"][0]); - Ok(()) - }, - ) - .unwrap(); - } - - #[test] - fn healthy_settings_keep_existing_values_and_do_not_create_backups() { - let original = settings_with_preserved_values(); - let snapshot = GeneralSettingsSnapshot::load(Some(original.clone())).unwrap(); - snapshot - .persist( - &snapshot.settings, - |_| panic!("valid settings must not need recovery"), - |saved| { - for (key, value) in original.as_object().unwrap() { - assert_eq!(&saved[key], value, "{key}"); - } - Ok(()) - }, - ) - .unwrap(); - } - - #[test] - fn failed_backup_does_not_overwrite_malformed_section() { - let original = json!({ "theme": "future-theme", "recordingsPath": "/Volumes/Test" }); - let snapshot = GeneralSettingsSnapshot::load(Some(original.clone())).unwrap(); - let result = snapshot.persist( - &snapshot.settings, - |backup| { - assert_eq!(backup, &original); - Err("injected backup failure".into()) - }, - |_| panic!("the original section must survive a failed backup"), - ); - assert_eq!(result.unwrap_err(), "injected backup failure"); - } - - #[test] - fn non_object_section_is_backed_up_before_recovery() { - let original = json!(["unreadable", 123]); - let snapshot = GeneralSettingsSnapshot::load(Some(original.clone())).unwrap(); - let backed_up = std::cell::Cell::new(false); - snapshot - .persist( - &snapshot.settings, - |backup| { - assert_eq!(backup, &original); - backed_up.set(true); - Ok(()) - }, - |saved| { - assert!(backed_up.get()); - assert!(serde_json::from_value::(saved).is_ok()); - Ok(()) - }, - ) - .unwrap(); - } - - #[test] - fn missing_section_keeps_new_install_defaults_without_recovery() { - let snapshot = GeneralSettingsSnapshot::load(None).unwrap(); - assert!(!snapshot.settings.has_completed_onboarding); - assert!(!snapshot.settings.has_completed_startup); - assert_eq!(snapshot.settings.recording_countdown, Some(3)); - snapshot - .persist( - &snapshot.settings, - |_| panic!("new settings must not need recovery"), - |saved| { - assert_eq!(saved["hasCompletedOnboarding"], false); - assert_eq!(saved["recordingCountdown"], 3); - Ok(()) - }, - ) - .unwrap(); - } - - #[test] - fn recovery_backups_are_exact_unique_and_private() { - let directory = tempfile::tempdir().unwrap(); - let original = settings_with_preserved_values(); - let first = backup_general_settings(directory.path(), &original).unwrap(); - let second = backup_general_settings(directory.path(), &json!("another section")).unwrap(); - assert_ne!(first, second); - let saved: Value = serde_json::from_slice(&std::fs::read(&first).unwrap()).unwrap(); - assert_eq!(saved, original); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!( - std::fs::metadata(first).unwrap().permissions().mode() & 0o777, - 0o600 - ); - } - } - - #[test] - fn recovery_backup_reports_unwritable_destination() { - let directory = tempfile::tempdir().unwrap(); - let blocker = directory.path().join("file"); - std::fs::write(&blocker, "keep").unwrap(); - assert!(backup_general_settings(&blocker, &json!({ "theme": [] })).is_err()); - assert_eq!(std::fs::read_to_string(blocker).unwrap(), "keep"); - } - - fn title_exclusion(title: &str) -> WindowExclusion { - WindowExclusion { - bundle_identifier: None, - owner_name: None, - window_title: Some(title.to_string()), - } - } - - #[test] - fn appends_missing_default_excluded_windows() { - let mut excluded_windows = vec![ - title_exclusion("Cap"), - WindowExclusion { - bundle_identifier: None, - owner_name: Some("Preview".to_string()), - window_title: Some("Private Preview".to_string()), - }, - ]; - - let changed = append_missing_default_excluded_windows(&mut excluded_windows); - - assert!(changed); - assert!( - default_excluded_windows() - .iter() - .all(|default| excluded_windows.contains(default)) - ); - assert!(excluded_windows.iter().any(|entry| { - entry.owner_name.as_deref() == Some("Preview") - && entry.window_title.as_deref() == Some("Private Preview") - })); - } - - #[test] - fn does_not_duplicate_default_excluded_windows() { - let mut excluded_windows = default_excluded_windows(); - let len = excluded_windows.len(); - - let changed = append_missing_default_excluded_windows(&mut excluded_windows); - - assert!(!changed); - assert_eq!(excluded_windows.len(), len); - } -} diff --git a/apps/desktop/src-tauri/src/gpu_context.rs b/apps/desktop/src-tauri/src/gpu_context.rs deleted file mode 100644 index 9472df5d298..00000000000 --- a/apps/desktop/src-tauri/src/gpu_context.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, - time::Instant, -}; -use tokio::sync::OnceCell; - -#[derive(Clone)] -pub struct PendingScreenshot { - pub data: Vec, - pub width: u32, - pub height: u32, - pub channels: u32, - pub created_at: Instant, -} - -pub struct PendingScreenshots(pub Arc>>); - -impl Default for PendingScreenshots { - fn default() -> Self { - Self(Arc::new(RwLock::new(HashMap::new()))) - } -} - -impl PendingScreenshots { - pub fn insert(&self, key: String, screenshot: PendingScreenshot) { - let mut guard = self.0.write().unwrap(); - guard.retain(|_, v| v.created_at.elapsed() < std::time::Duration::from_secs(10)); - guard.insert(key, screenshot); - } - - pub fn remove(&self, key: &str) -> Option { - self.0.write().unwrap().remove(key) - } - - pub fn get(&self, key: &str) -> Option { - self.0.read().unwrap().get(key).cloned() - } -} - -pub struct SharedGpuContext { - health: crate::gpu_device_health::GpuDeviceHealth, - pub device: Arc, - pub queue: Arc, - pub adapter: Arc, - pub instance: Arc, - pub is_software_adapter: bool, - pub background_cache: Arc, -} - -static GPU: OnceCell> = OnceCell::const_new(); - -/// Marks the crash sentinel while GPU adapter/device initialisation is in flight, so -/// a process death inside that window is attributable to graphics bring-up. Dropping -/// the guard (including during unwind from a caught panic) disarms the marker — a -/// survivable failure is not a GPU crash. -struct GpuInitPhaseGuard; - -impl GpuInitPhaseGuard { - fn arm() -> Self { - crate::crash_sentinel::enter_gpu_init_phase(); - Self - } -} - -impl Drop for GpuInitPhaseGuard { - fn drop(&mut self) { - crate::crash_sentinel::exit_gpu_init_phase(); - } -} - -async fn init_gpu_inner() -> Option { - let _gpu_init_phase = GpuInitPhaseGuard::arm(); - - let instance = cap_rendering::create_wgpu_instance().await; - - let force_software_adapter = cap_rendering::force_software_wgpu_adapter(); - if force_software_adapter { - tracing::warn!("Forcing software WGPU adapter for shared context"); - } - - let hardware_adapter = if force_software_adapter { - None - } else { - instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - force_fallback_adapter: false, - compatible_surface: None, - }) - .await - .ok() - }; - - let (adapter, is_software_adapter) = if let Some(adapter) = hardware_adapter { - let adapter_info = adapter.get_info(); - let is_software_adapter = cap_rendering::is_software_wgpu_adapter(&adapter_info); - - if is_software_adapter { - tracing::warn!( - adapter_name = adapter_info.name, - adapter_backend = ?adapter_info.backend, - adapter_device_type = ?adapter_info.device_type, - "Selected shared-context adapter behaves like a software renderer" - ); - } else { - tracing::info!( - adapter_name = adapter_info.name, - adapter_backend = ?adapter_info.backend, - adapter_device_type = ?adapter_info.device_type, - "Using hardware GPU adapter for shared context" - ); - } - - (adapter, is_software_adapter) - } else { - tracing::warn!( - "No hardware GPU adapter found, attempting software fallback for shared context" - ); - let software_adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - force_fallback_adapter: true, - compatible_surface: None, - }) - .await - .ok()?; - - let adapter_info = software_adapter.get_info(); - - tracing::info!( - adapter_name = adapter_info.name, - adapter_backend = ?adapter_info.backend, - adapter_device_type = ?adapter_info.device_type, - "Using software adapter for shared context (CPU rendering - performance may be reduced)" - ); - (software_adapter, true) - }; - - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("cap-shared-gpu-device"), - required_features: wgpu::Features::empty(), - ..Default::default() - }) - .await - .ok()?; - - let health = crate::gpu_device_health::GpuDeviceHealth::track(&device, |reason, message| { - tracing::error!(?reason, %message, "Shared GPU device lost"); - if reason == wgpu::DeviceLostReason::Unknown { - sentry::capture_message("Shared GPU device lost", sentry::Level::Error); - } - }); - - Some(SharedGpuContext { - health, - device: Arc::new(device), - queue: Arc::new(queue), - adapter: Arc::new(adapter), - instance: Arc::new(instance), - is_software_adapter, - background_cache: Arc::new(cap_rendering::BackgroundTextureCache::default()), - }) -} - -pub async fn get_shared_gpu() -> Option<&'static SharedGpuContext> { - GPU.get_or_init(|| async { - let result = tokio::spawn(init_gpu_inner()).await; - - match result { - Ok(ctx) => ctx, - Err(e) => { - if e.is_panic() { - tracing::error!( - "GPU initialization panicked (wgpu internal error). \ - The app will continue without GPU acceleration." - ); - } else { - tracing::error!( - error = %e, - "GPU initialization task failed" - ); - } - None - } - } - }) - .await - .as_ref() -} - -pub fn prewarm_gpu() { - tokio::spawn(async { - get_shared_gpu().await; - }); -} - -// Every Tauri video editor uses this OnceCell's device when initialization succeeds. -// Its resources cannot recover by reopening the editor or replacing only the device. -pub fn ensure_shared_device_available() -> Result<(), String> { - if let Some(Some(gpu)) = GPU.get() { - gpu.health.ensure_available().map_err(str::to_string)?; - } - Ok(()) -} diff --git a/apps/desktop/src-tauri/src/gpu_device_health.rs b/apps/desktop/src-tauri/src/gpu_device_health.rs deleted file mode 100644 index 382faaa7b0a..00000000000 --- a/apps/desktop/src-tauri/src/gpu_device_health.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; - -#[derive(Clone)] -pub struct GpuDeviceHealth { - lost: Arc, -} - -impl GpuDeviceHealth { - pub fn track( - device: &wgpu::Device, - on_lost: impl Fn(wgpu::DeviceLostReason, String) + Send + 'static, - ) -> Self { - let lost = Arc::new(AtomicBool::new(false)); - let callback_lost = lost.clone(); - device.set_device_lost_callback(move |reason, message| { - callback_lost.store(true, Ordering::Release); - on_lost(reason, message); - }); - Self { lost } - } - - pub fn ensure_available(&self) -> Result<(), &'static str> { - if self.lost.load(Ordering::Acquire) { - return Err("The graphics device was lost. Restart Cap to generate an export preview."); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::atomic::AtomicUsize; - - fn device() -> (wgpu::Device, wgpu::Queue) { - wgpu::Device::noop(&wgpu::DeviceDescriptor::default()) - } - - fn preview_pipeline( - health: &GpuDeviceHealth, - device: &wgpu::Device, - ) -> Result { - health.ensure_available()?; - Ok(device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor::default())) - } - - #[test] - fn unguarded_repeated_previews_panic_after_device_loss() { - let (device, _queue) = device(); - device.destroy(); - device.poll(wgpu::PollType::Poll).unwrap(); - - for _ in 0..9 { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("NV12 Converter Pipeline Layout"), - ..Default::default() - }) - })); - assert!(result.is_err()); - } - } - - #[test] - fn repeated_previews_reject_a_lost_device_and_its_clones() { - let (device, _queue) = device(); - let reports = Arc::new(AtomicUsize::new(0)); - let callback_reports = reports.clone(); - let health = GpuDeviceHealth::track(&device, move |_, _| { - callback_reports.fetch_add(1, Ordering::Relaxed); - }); - let editor_device = device.clone(); - let editor_health = health.clone(); - let _retained_pipeline = preview_pipeline(&editor_health, &editor_device).unwrap(); - - device.destroy(); - device.poll(wgpu::PollType::Poll).unwrap(); - - for _ in 0..9 { - assert_eq!( - preview_pipeline(&editor_health, &editor_device).unwrap_err(), - "The graphics device was lost. Restart Cap to generate an export preview." - ); - } - assert_eq!(reports.load(Ordering::Relaxed), 1); - } - - #[test] - fn loss_does_not_disable_an_independent_device() { - let (lost_device, _lost_queue) = device(); - let health = GpuDeviceHealth::track(&lost_device, |_, _| {}); - lost_device.destroy(); - lost_device.poll(wgpu::PollType::Poll).unwrap(); - - let (independent_device, _queue) = device(); - let independent_health = GpuDeviceHealth::track(&independent_device, |_, _| {}); - preview_pipeline(&independent_health, &independent_device).unwrap(); - assert!(preview_pipeline(&health, &lost_device).is_err()); - } - - #[test] - fn healthy_device_validation_errors_still_panic() { - let (device, _queue) = device(); - let health = GpuDeviceHealth::track(&device, |_, _| {}); - health.ensure_available().unwrap(); - - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("invalid synthetic shader"), - source: wgpu::ShaderSource::Wgsl("invalid wgsl".into()), - }) - })); - assert!(result.is_err()); - health.ensure_available().unwrap(); - } -} diff --git a/apps/desktop/src-tauri/src/gpui_app.rs b/apps/desktop/src-tauri/src/gpui_app.rs deleted file mode 100644 index 89cba6c70af..00000000000 --- a/apps/desktop/src-tauri/src/gpui_app.rs +++ /dev/null @@ -1,1545 +0,0 @@ -//! Handing this app off to the experimental gpui-native app (`cap-gpui`). -//! -//! The two apps already share one settings store and recordings library -//! (`apps/desktop-gpui/src/store.rs` reads this app's), so the `enable_gpui_app` -//! setting is not "run both": whichever app is enabled owns the session. Turning -//! it on spawns `cap-gpui` and exits this app; turning it off inside `cap-gpui` -//! reopens this one. Startup honours the same flag, so the handoff survives a -//! relaunch. -//! -//! The binary is discovered in order: `CAP_GPUI_BIN` (explicit override), the -//! installed executable directory (where Tauri bundles and signs sidecars), the -//! legacy `gpui/` resource dir, and -- in debug builds only -- the sibling -//! `apps/desktop-gpui` target dir, so the toggle works from a source checkout. -//! -//! ## The handoff marker -//! -//! A handoff that spawns a `cap-gpui` which then dies immediately would leave -//! the user with no app at all, and with a setting that keeps redirecting away -//! from the app that does work. So this side writes `cap-gpui.handoff` next to -//! the shared store before spawning, and `cap-gpui` deletes it only on clean -//! shutdown. With no live GPUI process, a surviving marker means the previous -//! session exited unexpectedly: clear the flag, tell the user, and start normally. - -use std::path::PathBuf; - -use tauri::{AppHandle, Manager}; -use tauri_plugin_dialog::DialogExt; -use tracing::{info, warn}; - -use crate::general_settings::GeneralSettingsStore; - -#[cfg(windows)] -const BINARY_NAME: &str = "cap-gpui.exe"; -#[cfg(not(windows))] -const BINARY_NAME: &str = "cap-gpui"; - -#[cfg(any(target_os = "macos", windows, test))] -const MAX_FORWARDED_DEEP_LINK_BYTES: usize = 1024 * 1024; - -#[cfg(any(target_os = "macos", windows, test))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct GpuiForwardingEndpoint { - pid: u32, - port: u16, - secret: u64, -} - -#[cfg(any(target_os = "macos", test))] -#[derive(Default)] -pub(crate) struct StartupRedirectState { - phase: std::sync::atomic::AtomicU8, - reopen_pid: Option, -} - -#[cfg(any(target_os = "macos", test))] -impl StartupRedirectState { - fn with_reopen(pid: u32) -> Self { - Self { - reopen_pid: Some(pid), - ..Self::default() - } - } - - pub(crate) fn begin_forwarding(&self) -> bool { - self.transition(0, 1) - } - - pub(crate) fn exit_if_pending(&self) -> Option> { - self.transition(0, 2).then_some(self.reopen_pid) - } - - pub(crate) fn exit_after_forwarding(&self) -> bool { - self.transition(1, 2) - } - - fn transition(&self, from: u8, to: u8) -> bool { - self.phase - .compare_exchange( - from, - to, - std::sync::atomic::Ordering::AcqRel, - std::sync::atomic::Ordering::Acquire, - ) - .is_ok() - } -} - -fn existing_file(path: PathBuf) -> Option { - path.is_file().then_some(path) -} - -fn binary_path(app: &AppHandle) -> Option { - if let Some(path) = std::env::var_os("CAP_GPUI_BIN").map(PathBuf::from) - && path.is_file() - { - return Some(path); - } - - if let Ok(executable) = std::env::current_exe() - && let Some(parent) = executable.parent() - && let Some(path) = existing_file(parent.join(BINARY_NAME)) - { - return Some(path); - } - - if let Ok(resources) = app.path().resource_dir() - && let Some(path) = existing_file(resources.join("gpui").join(BINARY_NAME)) - { - return Some(path); - } - - #[cfg(debug_assertions)] - { - // Whichever profile was built most recently: a fixed preference here - // hands off to a days-old binary the moment the other profile is the - // one being actively rebuilt. - let target = - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../desktop-gpui/target"); - let newest = ["release", "debug"] - .into_iter() - .filter_map(|profile| { - let path = target.join(profile).join(BINARY_NAME); - let modified = std::fs::metadata(&path).ok()?.modified().ok()?; - Some((modified, path)) - }) - .max_by_key(|(modified, _)| *modified) - .map(|(_, path)| path); - if newest.is_some() { - return newest; - } - } - - None -} - -/// Mirror of `store::app_data_dir` in `apps/desktop-gpui`: the pidfile and the -/// handoff marker live under the shared production identifier -/// (`so.cap.desktop`), not this app's possibly-`.dev` one. -fn shared_data_dir() -> PathBuf { - #[cfg(target_os = "macos")] - let base = PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".into())) - .join("Library/Application Support/so.cap.desktop"); - #[cfg(target_os = "windows")] - let base = std::env::var_os("APPDATA") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(".")) - .join("so.cap.desktop"); - #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] - let base = std::env::var_os("XDG_DATA_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| { - PathBuf::from(std::env::var_os("HOME").unwrap_or_else(|| ".".into())) - .join(".local/share") - }) - .join("so.cap.desktop"); - base -} - -fn gpui_pidfile() -> PathBuf { - shared_data_dir().join("cap-gpui.pid") -} - -/// Whether this app's own store IS the shared one. It is for the production -/// bundle (`so.cap.desktop`), but a dev build stores under a `.dev` identifier -/// while `cap-gpui` always reads and writes the shared file -- two stores that -/// silently disagree. The handoff flag therefore lives in the shared store, -/// and this app's own copy of the setting is just what its settings page -/// displays. -fn own_store_is_shared(app: &AppHandle) -> bool { - app.path() - .app_data_dir() - .is_ok_and(|dir| dir == shared_data_dir()) -} - -fn shared_store_path() -> PathBuf { - // The store plugin's file for the production identifier -- the same file - // `store::set_store_setting` in apps/desktop-gpui operates on. - shared_data_dir().join("store") -} - -fn shared_store_flag() -> Option { - let raw = std::fs::read_to_string(shared_store_path()).ok()?; - let doc: serde_json::Value = serde_json::from_str(&raw).ok()?; - doc.get("general_settings")?.get("enableGpuiApp")?.as_bool() -} - -/// Single-key replace with everything else preserved, temp+rename atomic -- -/// the same contract as the gpui app's `set_store_setting`. A missing file is -/// created with just this key; an unparseable one is refused rather than -/// replaced. Only ever called when the shared store is NOT this app's own -/// (`own_store_is_shared` false): writing the file directly under the store -/// plugin's feet would race its in-memory copy. -fn write_shared_store_flag(value: bool) { - let path = shared_store_path(); - let mut doc = match std::fs::read_to_string(&path) { - Ok(raw) => match serde_json::from_str::(&raw) { - Ok(doc) => doc, - Err(error) => { - warn!(%error, "shared store is unparseable; leaving it untouched"); - return; - } - }, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => serde_json::json!({}), - Err(error) => { - warn!(%error, "could not read the shared store"); - return; - } - }; - let Some(root) = doc.as_object_mut() else { - warn!("shared store is not a JSON object; leaving it untouched"); - return; - }; - root.entry("general_settings") - .or_insert_with(|| serde_json::json!({})) - .as_object_mut() - .map(|section| section.insert("enableGpuiApp".into(), serde_json::Value::Bool(value))); - let pretty = serde_json::to_string_pretty(&doc).unwrap_or_else(|_| doc.to_string()); - let tmp = path.with_extension("tmp"); - let write = std::fs::create_dir_all(path.parent().unwrap_or(&path)) - .and_then(|()| std::fs::write(&tmp, pretty)) - .and_then(|()| std::fs::rename(&tmp, &path)); - if let Err(error) = write { - warn!(%error, "could not write the shared store flag"); - } -} - -fn handoff_marker() -> PathBuf { - shared_data_dir().join("cap-gpui.handoff") -} - -fn update_handoff_marker() -> PathBuf { - shared_data_dir().join("cap-gpui.update-handoff") -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct UpdateHandoff { - pid: u32, - simulated: bool, -} - -fn parse_update_handoff(contents: &str) -> Option { - let contents = contents.trim(); - let (pid, simulated) = match contents.strip_prefix("simulate:") { - Some(pid) if cfg!(debug_assertions) => (pid, true), - Some(_) => return None, - None => (contents, false), - }; - let pid = pid.parse().ok()?; - (pid != 0).then_some(UpdateHandoff { pid, simulated }) -} - -fn take_update_handoff() -> Option { - let marker = update_handoff_marker(); - let contents = match std::fs::read_to_string(&marker) { - Ok(contents) => contents, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return None, - Err(error) => { - warn!(%error, "could not read the Cap GPUI update hand-off"); - return None; - } - }; - - let Some(handoff) = parse_update_handoff(&contents) else { - warn!("discarding an invalid Cap GPUI update hand-off"); - let _ = std::fs::remove_file(&marker); - return None; - }; - - let expected_pid = std::fs::read_to_string(gpui_pidfile()) - .ok() - .and_then(|contents| contents.trim().parse::().ok()); - let stale = marker - .metadata() - .ok() - .and_then(|metadata| metadata.modified().ok()) - .and_then(|modified| modified.elapsed().ok()) - .is_some_and(|age| age > std::time::Duration::from_secs(15 * 60)); - if expected_pid.is_some_and(|pid| pid != handoff.pid) || stale { - warn!( - pid = handoff.pid, - "discarding a stale Cap GPUI update hand-off" - ); - let _ = std::fs::remove_file(&marker); - return None; - } - - match std::fs::remove_file(&marker) { - Ok(()) => { - info!( - pid = handoff.pid, - "taking ownership from Cap GPUI for an update check" - ); - Some(handoff.simulated) - } - Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, - Err(error) => { - warn!(%error, "could not consume the Cap GPUI update hand-off"); - None - } - } -} - -fn show_update_page_when_ready(app: AppHandle, simulated: bool) { - tauri::async_runtime::spawn(async move { - for _ in 0..300 { - if let Some(window) = app - .get_webview_window("main") - .or_else(|| app.get_webview_window("onboarding")) - { - match window.url() { - Ok(mut url) => { - url.set_path("/update"); - url.set_query(Some(if simulated { - "source=gpui&simulateUpdate=1" - } else { - "source=gpui" - })); - if let Err(error) = window.navigate(url) { - warn!(%error, "could not open the updater after the GPUI hand-off"); - } else { - let _ = window.show(); - let _ = window.set_focus(); - } - } - Err(error) => { - warn!(%error, "could not read the window URL for the GPUI updater"); - } - } - return; - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - warn!("no window became ready for the GPUI update hand-off"); - }); -} - -pub(crate) fn handle_update_handoff(app: &AppHandle) -> bool { - let Some(simulated) = take_update_handoff() else { - return false; - }; - show_update_page_when_ready(app.clone(), simulated); - true -} - -fn classic_pending() -> PathBuf { - shared_data_dir().join("cap-classic.pending") -} - -/// The pid of the live `cap-gpui` that owns the pidfile, if there is one. -fn running_instance_pid() -> Option { - let pid = std::fs::read_to_string(gpui_pidfile()) - .ok()? - .trim() - .parse::() - .ok()?; - - #[cfg(target_os = "linux")] - { - linux_gpui_process_is_running(pid).then_some(pid) - } - - #[cfg(all(unix, not(target_os = "linux")))] - { - let alive = std::process::Command::new("ps") - .args(["-p", &pid.to_string(), "-o", "comm="]) - .output() - .is_ok_and(|output| { - output.status.success() - && is_gpui_process_image(std::path::Path::new( - String::from_utf8_lossy(&output.stdout).trim(), - )) - }); - alive.then_some(pid) - } - - #[cfg(windows)] - { - let process_id = sysinfo::Pid::from_u32(pid); - let mut system = sysinfo::System::new(); - system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[process_id]), true); - system - .process(process_id) - .and_then(sysinfo::Process::exe) - .is_some_and(is_gpui_process_image) - .then_some(pid) - } -} - -#[cfg(target_os = "linux")] -fn linux_gpui_process_is_running(pid: u32) -> bool { - std::fs::read_link(format!("/proc/{pid}/exe")).is_ok_and(|path| is_gpui_process_image(&path)) -} - -fn is_gpui_process_image(path: &std::path::Path) -> bool { - let Some(name) = path.file_name().and_then(std::ffi::OsStr::to_str) else { - return false; - }; - #[cfg(target_os = "linux")] - let name = name.strip_suffix(" (deleted)").unwrap_or(name); - name.eq_ignore_ascii_case(BINARY_NAME) -} - -#[cfg(any(target_os = "macos", windows, test))] -fn parse_gpui_forwarding_endpoint(contents: &str) -> Option { - let mut parts = contents.trim().split(':'); - let pid = parts.next()?.parse().ok()?; - let port = parts.next()?.parse().ok()?; - let secret = u64::from_str_radix(parts.next()?, 16).ok()?; - (pid != 0 && port != 0 && parts.next().is_none()).then_some(GpuiForwardingEndpoint { - pid, - port, - secret, - }) -} - -#[cfg(any(target_os = "macos", windows, test))] -fn is_forwardable_gpui_deep_link(url: &str) -> bool { - !url.is_empty() - && url.len() <= MAX_FORWARDED_DEEP_LINK_BYTES - && reqwest::Url::parse(url) - .is_ok_and(|parsed| matches!(parsed.scheme(), "cap-desktop" | "cap")) -} - -#[cfg(any(target_os = "macos", windows, test))] -fn forwarded_gpui_argument(argument: &str) -> Option { - if is_forwardable_gpui_deep_link(argument) { - return Some(argument.to_string()); - } - - let path = match reqwest::Url::parse(argument) { - Ok(url) if url.scheme() == "file" => url.to_file_path().ok()?, - _ => PathBuf::from(argument), - }; - if !path - .extension() - .and_then(std::ffi::OsStr::to_str) - .is_some_and(|extension| extension.eq_ignore_ascii_case("cap")) - { - return None; - } - - let metadata = std::fs::symlink_metadata(&path).ok()?; - if metadata.file_type().is_symlink() || !metadata.is_dir() { - return None; - } - let path = path.canonicalize().ok()?; - let value = serde_json::json!({ "open_editor": { "project_path": path } }).to_string(); - let mut url = reqwest::Url::parse("cap-desktop://action").ok()?; - url.query_pairs_mut().append_pair("value", &value); - let url = url.to_string(); - is_forwardable_gpui_deep_link(&url).then_some(url) -} - -#[cfg(target_os = "macos")] -fn read_macos_gpui_metadata(path: &std::path::Path, private: bool) -> std::io::Result { - use nix::libc; - use std::{ - io::Read, - os::unix::fs::{MetadataExt, OpenOptionsExt}, - }; - - let file = std::fs::OpenOptions::new() - .read(true) - .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC) - .open(path)?; - let metadata = file.metadata()?; - let forbidden_permissions = if private { 0o077 } else { 0o022 }; - if !metadata.is_file() - || metadata.len() > 128 - || metadata.nlink() != 1 - || metadata.uid() != unsafe { libc::geteuid() } - || metadata.mode() & forbidden_permissions != 0 - { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "Cap GPUI instance metadata is not owner controlled", - )); - } - let mut contents = String::new(); - file.take(129).read_to_string(&mut contents)?; - if contents.len() > 128 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Cap GPUI instance metadata changed", - )); - } - Ok(contents) -} - -#[cfg(target_os = "macos")] -fn macos_reopen_incumbent(path: &std::path::Path, pid: u32) -> std::io::Result { - use nix::libc; - use std::os::unix::ffi::OsStrExt; - - if pid == 0 - || pid > i32::MAX as u32 - || pid == std::process::id() - || read_macos_gpui_metadata(path, false)? - .trim() - .parse::() - .ok() - != Some(pid) - { - return Ok(false); - } - let mut buffer = [0_u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize]; - let length = - unsafe { libc::proc_pidpath(pid as i32, buffer.as_mut_ptr().cast(), buffer.len() as u32) }; - Ok(length > 0 - && buffer.split(|byte| *byte == 0).next().is_some_and(|bytes| { - is_gpui_process_image(std::path::Path::new(std::ffi::OsStr::from_bytes(bytes))) - })) -} - -#[cfg(any(target_os = "macos", windows))] -fn reopen_time_remaining( - started: std::time::Instant, - timeout: std::time::Duration, -) -> std::io::Result { - timeout - .checked_sub(started.elapsed()) - .filter(|remaining| !remaining.is_zero()) - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::TimedOut, - "Cap GPUI reopen request timed out", - ) - }) -} - -#[cfg(any(target_os = "macos", windows))] -fn send_gpui_reopen( - endpoint: GpuiForwardingEndpoint, - started: std::time::Instant, - timeout: std::time::Duration, -) -> std::io::Result<()> { - use std::io::{Read, Write}; - - let address = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, endpoint.port)); - let mut stream = - std::net::TcpStream::connect_timeout(&address, reopen_time_remaining(started, timeout)?)?; - stream.set_nonblocking(true)?; - let mut payload = [0_u8; 12]; - payload[..8].copy_from_slice(&endpoint.secret.to_be_bytes()); - let mut pending = payload.as_slice(); - while !pending.is_empty() { - let remaining = reopen_time_remaining(started, timeout)?; - match stream.write(pending) { - Ok(0) => { - return Err(std::io::Error::new( - std::io::ErrorKind::WriteZero, - "Cap GPUI closed its forwarding connection", - )); - } - Ok(written) => pending = &pending[written..], - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - std::thread::sleep(remaining.min(std::time::Duration::from_millis(5))) - } - Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {} - Err(error) => return Err(error), - } - } - let mut acknowledgment = [0_u8; 1]; - loop { - let remaining = reopen_time_remaining(started, timeout)?; - match stream.read(&mut acknowledgment) { - Ok(1) if acknowledgment[0] == 1 => return Ok(()), - Ok(_) => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Cap GPUI did not acknowledge the reopen request", - )); - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - std::thread::sleep(remaining.min(std::time::Duration::from_millis(5))) - } - Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {} - Err(error) => return Err(error), - } - } -} - -#[cfg(target_os = "macos")] -pub(crate) fn request_gpui_reopen(pid: u32) -> Result<(), String> { - let request = || -> std::io::Result<()> { - let started = std::time::Instant::now(); - let timeout = std::time::Duration::from_secs(2); - let path = gpui_pidfile(); - loop { - reopen_time_remaining(started, timeout)?; - if !macos_reopen_incumbent(&path, pid)? { - return Err(std::io::Error::new( - std::io::ErrorKind::NotConnected, - "The Cap GPUI instance changed before reopening", - )); - } - match read_macos_gpui_metadata(&path.with_extension("ipc"), true) { - Ok(contents) => { - if let Some(endpoint) = parse_gpui_forwarding_endpoint(&contents) - .filter(|endpoint| endpoint.pid == pid) - { - return send_gpui_reopen(endpoint, started, timeout); - } - } - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => return Err(error), - } - std::thread::sleep( - reopen_time_remaining(started, timeout)?.min(std::time::Duration::from_millis(50)), - ); - } - }; - request().map_err(|error| format!("Could not confirm reopening the running Cap GPUI: {error}")) -} - -#[cfg(windows)] -fn request_gpui_reopen(pid: u32) -> Result<(), String> { - use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; - - let request = || -> std::io::Result<()> { - let started = std::time::Instant::now(); - let timeout = std::time::Duration::from_secs(2); - let endpoint_path = gpui_pidfile().with_extension("ipc"); - loop { - if let Some(endpoint) = std::fs::read_to_string(&endpoint_path) - .ok() - .and_then(|contents| parse_gpui_forwarding_endpoint(&contents)) - .filter(|endpoint| endpoint.pid == pid) - { - let _ = unsafe { AllowSetForegroundWindow(pid) }; - return send_gpui_reopen(endpoint, started, timeout); - } - std::thread::sleep( - reopen_time_remaining(started, timeout)?.min(std::time::Duration::from_millis(50)), - ); - } - }; - request().map_err(|error| format!("Could not confirm reopening the running Cap GPUI: {error}")) -} - -#[cfg(any(target_os = "macos", test))] -fn gpui_launch_needs_reopen(args: &[String]) -> bool { - !args - .iter() - .any(|argument| forwarded_gpui_argument(argument).is_some()) -} - -#[cfg(any(target_os = "macos", windows))] -fn forward_deep_links_to_gpui(pid: u32, args: &[String]) -> bool { - use std::io::Write; - - let urls = args - .iter() - .filter_map(|argument| forwarded_gpui_argument(argument)) - .collect::>(); - if urls.is_empty() { - return false; - } - - let endpoint_path = gpui_pidfile().with_extension("ipc"); - let endpoint = (0..40).find_map(|attempt| { - let endpoint = std::fs::read_to_string(&endpoint_path) - .ok() - .and_then(|contents| parse_gpui_forwarding_endpoint(&contents)) - .filter(|endpoint| endpoint.pid == pid); - if endpoint.is_none() && attempt < 39 { - std::thread::sleep(std::time::Duration::from_millis(50)); - } - endpoint - }); - let Some(endpoint) = endpoint else { - warn!(pid, "could not find the Cap GPUI deep-link endpoint"); - return false; - }; - - let mut forwarded = false; - for url in urls { - let result = std::net::TcpStream::connect((std::net::Ipv4Addr::LOCALHOST, endpoint.port)) - .and_then(|mut stream| { - stream.set_write_timeout(Some(std::time::Duration::from_secs(2)))?; - stream.write_all(&endpoint.secret.to_be_bytes())?; - stream.write_all(&(url.len() as u32).to_be_bytes())?; - stream.write_all(url.as_bytes()) - }); - match result { - Ok(()) => forwarded = true, - Err(error) => warn!(%error, "could not forward a deep link to Cap GPUI"), - } - } - - forwarded -} - -#[cfg(any(target_os = "macos", windows))] -pub(crate) fn forward_deep_links_to_active_gpui(app: &AppHandle, args: &[String]) -> bool { - let own = GeneralSettingsStore::get(app) - .ok() - .flatten() - .is_some_and(|settings| settings.enable_gpui_app); - let enabled = if own_store_is_shared(app) { - own - } else { - shared_store_flag().unwrap_or(false) - }; - if !enabled { - return false; - } - - let Some(pid) = running_instance_pid() else { - return false; - }; - if !forward_deep_links_to_gpui(pid, args) { - return false; - } - - activate_instance(pid); - true -} - -#[cfg(target_os = "macos")] -pub(crate) fn forward_deep_links_to_gpui_when_ready(args: &[String]) -> Option { - if !args - .iter() - .any(|argument| forwarded_gpui_argument(argument).is_some()) - { - return None; - } - - let pid = (0..40).find_map(|attempt| { - let pid = running_instance_pid(); - if pid.is_none() && attempt < 39 { - std::thread::sleep(std::time::Duration::from_millis(50)); - } - pid - })?; - - forward_deep_links_to_gpui(pid, args).then_some(pid) -} - -#[cfg(target_os = "macos")] -pub(crate) fn activate_instance(pid: u32) { - use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; - - if let Some(instance) = - unsafe { NSRunningApplication::runningApplicationWithProcessIdentifier(pid as _) } - { - unsafe { - instance.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); - } - } -} - -#[cfg(windows)] -fn activate_instance(pid: u32) { - use windows::{ - Win32::{ - Foundation::{HWND, LPARAM, TRUE}, - UI::WindowsAndMessaging::{ - EnumWindows, GetWindowThreadProcessId, IsWindowVisible, SW_RESTORE, - SetForegroundWindow, ShowWindow, - }, - }, - core::BOOL, - }; - - struct Activation { - pid: u32, - visible: Option, - fallback: Option, - } - - unsafe extern "system" fn find_window(window: HWND, data: LPARAM) -> BOOL { - let activation = unsafe { &mut *(data.0 as *mut Activation) }; - let mut owner = 0; - unsafe { GetWindowThreadProcessId(window, Some(&mut owner)) }; - if owner == activation.pid { - if activation.fallback.is_none() { - activation.fallback = Some(window); - } - if activation.visible.is_none() && unsafe { IsWindowVisible(window) }.as_bool() { - activation.visible = Some(window); - } - } - TRUE - } - - let mut activation = Activation { - pid, - visible: None, - fallback: None, - }; - let _ = unsafe { - EnumWindows( - Some(find_window), - LPARAM(std::ptr::addr_of_mut!(activation) as isize), - ) - }; - - if let Some(window) = activation.visible.or(activation.fallback) { - unsafe { - let _ = ShowWindow(window, SW_RESTORE); - let _ = SetForegroundWindow(window); - } - } -} - -#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] -fn activate_instance(_pid: u32) {} - -#[cfg(any(target_os = "linux", test))] -fn launch_linux_activation( - path: Option<&std::path::Path>, - launch: impl FnOnce(&std::path::Path, &[String]) -> Result<(), String>, -) -> Result<(), String> { - let path = path.ok_or_else(|| "Cap GPUI isn't included in this build".to_string())?; - launch(path, &[]) -} - -fn write_handoff_marker() { - let path = handoff_marker(); - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - let stamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|since| since.as_secs()) - .unwrap_or_default(); - if let Err(error) = std::fs::write(&path, stamp.to_string()) { - warn!(%error, "could not write the Cap GPUI handoff marker"); - } -} - -/// The child must never inherit this process's stdio. The handoff exits this -/// app, and whatever was reading those pipes (`bun run tauri dev`'s tooling chain) -/// goes with it; `cap-gpui`'s next log line then hits EPIPE, the write panics -/// inside an ObjC run-loop callback, and the process aborts about a tenth of a -/// second after launch. Its output goes to a file instead of `/dev/null` -/// because a handoff launch has no terminal, and a startup failure there is -/// only ever diagnosable from that file. -fn spawn_detached(path: &std::path::Path, args: &[String]) -> Result<(), String> { - use std::process::Stdio; - - let log_path = shared_data_dir().join("cap-gpui.log"); - if let Some(parent) = log_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - let (stdout, stderr) = match std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_path) - { - Ok(file) => match file.try_clone() { - Ok(clone) => (Stdio::from(file), Stdio::from(clone)), - Err(_) => (Stdio::from(file), Stdio::null()), - }, - Err(error) => { - warn!(%error, "could not open the Cap GPUI log; discarding its output"); - (Stdio::null(), Stdio::null()) - } - }; - - let mut command = std::process::Command::new(path); - command - .args(args) - .stdin(Stdio::null()) - .stdout(stdout) - .stderr(stderr); - // Its own process group, so signals aimed at the terminal this app was - // started from never reach the app that outlives it. - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - command.process_group(0); - } - - let mut child = command - .spawn() - .map_err(|error| format!("Failed to launch Cap GPUI: {error}"))?; - // Reap the child when it exits so it never lingers as a zombie. - std::thread::spawn(move || { - let _ = child.wait(); - }); - Ok(()) -} - -#[cfg(target_os = "macos")] -pub(crate) fn retire_foreground_parent_for_handoff(app: &AppHandle) { - if app - .webview_windows() - .values() - .any(|window| !window.is_fullscreen().is_ok_and(|fullscreen| !fullscreen)) - { - warn!( - "Keeping the handoff parent's activation policy because fullscreen state is active or unavailable" - ); - return; - } - - // A foreground parent exiting with a surviving child leaves a macOS background Dock tile. - match app.set_activation_policy(tauri::ActivationPolicy::Accessory) { - Ok(()) => info!("Requested retirement of Tauri's Dock presence for the GPUI handoff"), - Err(error) => warn!(%error, "Could not retire Tauri's Dock presence for the GPUI handoff"), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn gpui_app_available(app: AppHandle) -> bool { - binary_path(&app).is_some() -} - -/// Close this app and open the native one. The setting has already been written -/// by the caller, so a failure here has to be reported rather than swallowed: -/// the page reverts it. -#[tauri::command] -#[specta::specta] -pub async fn switch_to_gpui_app(app: AppHandle) -> Result<(), String> { - let path = - binary_path(&app).ok_or_else(|| "Cap GPUI isn't included in this build".to_string())?; - crate::prepare_app_exit(&app, || { - let uses_shared_store = own_store_is_shared(&app); - let previous_shared_flag = if uses_shared_store { - None - } else { - let previous = shared_store_flag(); - write_shared_store_flag(true); - previous - }; - match running_instance_pid() { - Some(pid) => { - #[cfg(target_os = "linux")] - if let Err(error) = launch_linux_activation(Some(&path), spawn_detached) { - if !uses_shared_store { - write_shared_store_flag(previous_shared_flag.unwrap_or(false)); - } - return Err(error); - } - #[cfg(target_os = "macos")] - if let Err(error) = request_gpui_reopen(pid) { - if !uses_shared_store { - write_shared_store_flag(previous_shared_flag.unwrap_or(false)); - } - return Err(error); - } - #[cfg(not(target_os = "linux"))] - activate_instance(pid); - info!(pid, "Cap GPUI is already running; requested its controls"); - } - None => { - write_handoff_marker(); - info!(path = %path.display(), "handing off to Cap GPUI"); - if let Err(error) = spawn_detached(&path, &[]) { - let _ = std::fs::remove_file(handoff_marker()); - if !uses_shared_store { - write_shared_store_flag(previous_shared_flag.unwrap_or(false)); - } - return Err(error); - } - } - } - Ok(()) - })?; - #[cfg(target_os = "macos")] - { - let handle = app.clone(); - let (sender, receiver) = tokio::sync::oneshot::channel(); - match app.run_on_main_thread(move || { - retire_foreground_parent_for_handoff(&handle); - let _ = sender.send(()); - }) { - Ok(()) => { - if !matches!( - tokio::time::timeout(std::time::Duration::from_secs(2), receiver).await, - Ok(Ok(())) - ) { - warn!( - "The GPUI handoff's Dock transition was not acknowledged before shutdown" - ); - } - } - Err(error) => warn!(%error, "Could not schedule the GPUI handoff's Dock transition"), - } - } - crate::complete_admitted_app_exit(app).await; - Ok(()) -} - -/// Whether this app should hand over to `cap-gpui` instead of starting. -/// -/// `true` means the caller must exit before any window is created. -pub fn redirect_at_startup_if_enabled(app: &AppHandle) -> Result { - #[cfg(debug_assertions)] - if crate::stop_editor_benchmark::enabled() { - return Ok(false); - } - let update_handoff = handle_update_handoff(app); - let redirect = !update_handoff && redirect_decision(app)?; - if !redirect { - let app = app.clone(); - tauri::async_runtime::spawn(async move { - loop { - handle_update_handoff(&app); - if classic_pending().try_exists().unwrap_or(false) { - let handle = app.clone(); - let (sender, receiver) = tokio::sync::oneshot::channel(); - if app - .run_on_main_thread(move || { - acknowledge_classic_window(&handle); - let _ = sender.send(()); - }) - .is_ok() - { - let _ = - tokio::time::timeout(std::time::Duration::from_secs(1), receiver).await; - } - } - tokio::time::sleep(std::time::Duration::from_millis(250)).await; - } - }); - } - Ok(redirect) -} - -fn acknowledge_classic_window(app: &AppHandle) { - if crate::app_is_exiting(app) { - return; - } - let ready = app - .get_webview_window("main") - .or_else(|| app.get_webview_window("onboarding")) - .is_some_and(|window| { - let frontend_ready = window.label() == "onboarding" - || window.url().is_ok_and(|url| url.path() == "/update") - || app - .try_state::() - .is_some_and(|state| state.is_ready()); - frontend_ready && window.is_visible().unwrap_or(false) - }); - if ready { - match std::fs::remove_file(classic_pending()) { - Ok(()) => info!("Acknowledged visible classic window for GPUI handoff"), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => warn!(%error, "Could not acknowledge the classic app handoff"), - } - } -} - -fn redirect_decision(app: &AppHandle) -> Result { - let own = GeneralSettingsStore::get(app) - .ok() - .flatten() - .is_some_and(|settings| settings.enable_gpui_app); - // In a dev build the shared store is the authority: `cap-gpui`'s - // switch-back writes false THERE, and honouring this app's own `.dev` - // copy instead would bounce the user straight back after every - // switch-back (the loop this replaced). - let enabled = if own_store_is_shared(app) { - own - } else { - shared_store_flag().unwrap_or(false) - }; - if !enabled { - if own { - // The stale display copy: without this the Experimental page - // shows the toggle on while the classic app owns the session. - if let Err(error) = GeneralSettingsStore::update(app, |settings| { - settings.enable_gpui_app = false; - }) { - warn!(%error, "could not reconcile the Cap GPUI setting"); - } - } - return Ok(false); - } - - // The marker exists throughout a live session. Check the process first so - // reopening Cap does not mistake a running native app for a crashed one. - if let Some(pid) = running_instance_pid() { - #[cfg(any(target_os = "macos", windows))] - { - let args = std::env::args().skip(1).collect::>(); - #[cfg(target_os = "macos")] - if gpui_launch_needs_reopen(&args) { - app.manage(StartupRedirectState::with_reopen(pid)); - } else { - forward_deep_links_to_gpui(pid, &args); - } - #[cfg(windows)] - if !forward_deep_links_to_gpui(pid, &args) { - match request_gpui_reopen(pid) { - Ok(()) => { - info!(pid, "Cap GPUI is already running; reopened its main window"); - return Ok(true); - } - Err(error) => warn!(%error, "could not reopen the running Cap GPUI"), - } - } - } - #[cfg(target_os = "linux")] - launch_linux_activation(binary_path(app).as_deref(), spawn_detached)?; - #[cfg(not(target_os = "linux"))] - activate_instance(pid); - info!( - pid, - "Cap GPUI is already running; preserving the existing instance" - ); - return Ok(true); - } - - let marker = handoff_marker(); - if marker.exists() { - warn!("the last Cap GPUI session exited unexpectedly; taking back over"); - let _ = std::fs::remove_file(&marker); - if let Err(error) = GeneralSettingsStore::update(app, |settings| { - settings.enable_gpui_app = false; - }) { - warn!(%error, "could not clear the Cap GPUI setting"); - } - if !own_store_is_shared(app) { - write_shared_store_flag(false); - } - app.dialog() - .message( - "The native Cap app exited unexpectedly last time, so the classic app has been restored.", - ) - .show(|_| {}); - return Ok(false); - } - - let Some(path) = binary_path(app) else { - warn!("Cap GPUI is enabled but its binary was not found; starting this app instead"); - return Ok(false); - }; - - write_handoff_marker(); - info!(path = %path.display(), "handing off to Cap GPUI at startup"); - #[cfg(any(target_os = "macos", windows))] - let args = std::env::args() - .skip(1) - .filter_map(|argument| forwarded_gpui_argument(&argument)) - .collect::>(); - #[cfg(not(any(target_os = "macos", windows)))] - let args = Vec::new(); - if let Err(error) = spawn_detached(&path, &args) { - warn!("{error}"); - let _ = std::fs::remove_file(handoff_marker()); - return Ok(false); - } - Ok(true) -} - -#[cfg(test)] -mod tests { - use super::{ - BINARY_NAME, GpuiForwardingEndpoint, MAX_FORWARDED_DEEP_LINK_BYTES, StartupRedirectState, - UpdateHandoff, forwarded_gpui_argument, is_forwardable_gpui_deep_link, - is_gpui_process_image, parse_gpui_forwarding_endpoint, parse_update_handoff, - }; - - #[test] - fn linux_activation_launches_the_resolved_binary_once_without_arguments() { - let path = std::path::Path::new("/opt/cap/cap-gpui"); - let mut launches = 0; - super::launch_linux_activation(Some(path), |selected, args| { - launches += 1; - assert_eq!(selected, path); - assert!(args.is_empty()); - Ok(()) - }) - .unwrap(); - assert_eq!(launches, 1); - } - - #[test] - fn linux_activation_requires_a_resolved_binary_before_launch() { - let result = super::launch_linux_activation(None, |_, _| { - panic!("a missing GPUI binary must not be launched") - }); - assert_eq!(result.unwrap_err(), "Cap GPUI isn't included in this build"); - } - - #[test] - fn linux_activation_preserves_launch_failure() { - let path = std::path::Path::new("/opt/cap/cap-gpui"); - let result = super::launch_linux_activation(Some(path), |_, _| { - Err("Failed to launch Cap GPUI: permission denied".into()) - }); - assert_eq!( - result.unwrap_err(), - "Failed to launch Cap GPUI: permission denied" - ); - } - - #[test] - fn startup_redirect_exits_once_when_no_open_event_arrives() { - let state = StartupRedirectState::default(); - - assert_eq!(state.exit_if_pending(), Some(None)); - assert!(state.exit_if_pending().is_none()); - assert!(!state.begin_forwarding()); - assert!(!state.exit_after_forwarding()); - } - - #[test] - fn startup_redirect_waits_for_forwarding_before_exiting_once() { - let state = StartupRedirectState::default(); - - assert!(state.begin_forwarding()); - assert!(!state.begin_forwarding()); - assert!(state.exit_if_pending().is_none()); - assert!(state.exit_after_forwarding()); - assert!(!state.exit_after_forwarding()); - } - - #[test] - fn startup_plain_launch_returns_its_reopen_intent_only_once() { - let state = StartupRedirectState::with_reopen(123); - - assert_eq!(state.exit_if_pending(), Some(Some(123))); - assert!(state.exit_if_pending().is_none()); - assert!(!state.begin_forwarding()); - assert!(!state.exit_after_forwarding()); - } - - #[test] - fn startup_open_event_suppresses_pending_plain_reopen() { - let state = StartupRedirectState::with_reopen(123); - - assert!(state.begin_forwarding()); - assert!(state.exit_if_pending().is_none()); - assert!(!state.begin_forwarding()); - assert!(state.exit_after_forwarding()); - assert!(state.exit_if_pending().is_none()); - assert!(!state.exit_after_forwarding()); - } - - #[test] - fn update_handoff_requires_a_valid_process_id() { - assert_eq!( - parse_update_handoff("1234"), - Some(UpdateHandoff { - pid: 1234, - simulated: false, - }) - ); - assert_eq!(parse_update_handoff("0"), None); - assert_eq!(parse_update_handoff("cap-gpui"), None); - assert_eq!(parse_update_handoff("simulate:0"), None); - } - - #[test] - fn simulated_update_handoffs_are_debug_only() { - let parsed = parse_update_handoff("simulate:4321"); - if cfg!(debug_assertions) { - assert_eq!( - parsed, - Some(UpdateHandoff { - pid: 4321, - simulated: true, - }) - ); - } else { - assert_eq!(parsed, None); - } - } - - #[test] - fn gpui_process_image_must_match_exactly() { - assert!(is_gpui_process_image(std::path::Path::new(BINARY_NAME))); - assert!(is_gpui_process_image(std::path::Path::new( - &BINARY_NAME.to_ascii_uppercase() - ))); - assert!(!is_gpui_process_image(std::path::Path::new("not-cap-gpui"))); - assert!(!is_gpui_process_image(std::path::Path::new( - "cap-gpui-helper" - ))); - } - - #[cfg(target_os = "linux")] - #[test] - fn linux_process_check_distinguishes_live_unlinked_and_exited_processes() { - let directory = tempfile::tempdir().unwrap(); - let binary = directory.path().join(BINARY_NAME); - std::fs::copy("/bin/sleep", &binary).unwrap(); - let mut child = std::process::Command::new(&binary) - .arg("30") - .spawn() - .unwrap(); - let pid = child.id(); - let started = std::time::Instant::now(); - // spawn can return before /proc stops exposing the child's pre-exec image. - while !std::fs::read_link(format!("/proc/{pid}/exe")).is_ok_and(|path| path == binary) - && started.elapsed() < std::time::Duration::from_secs(5) - { - std::thread::sleep(std::time::Duration::from_millis(1)); - } - let live = super::linux_gpui_process_is_running(pid); - std::fs::remove_file(&binary).unwrap(); - let unlinked = super::linux_gpui_process_is_running(pid); - child.kill().unwrap(); - let started = std::time::Instant::now(); - let mut zombie = false; - while started.elapsed() < std::time::Duration::from_secs(5) { - zombie = std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|stat| { - stat.rsplit_once(") ") - .is_some_and(|(_, state)| state.starts_with('Z')) - }); - if zombie { - break; - } - std::thread::sleep(std::time::Duration::from_millis(1)); - } - let exited = super::linux_gpui_process_is_running(pid); - child.wait().unwrap(); - - assert!(live); - assert!(unlinked); - assert!(zombie); - assert!(!exited); - assert!(!super::linux_gpui_process_is_running(pid)); - assert!(!super::linux_gpui_process_is_running(std::process::id())); - } - - #[test] - fn gpui_forwarding_endpoint_requires_the_owner_identity() { - assert_eq!( - parse_gpui_forwarding_endpoint("4321:49152:0123456789abcdef"), - Some(GpuiForwardingEndpoint { - pid: 4321, - port: 49152, - secret: 0x0123_4567_89ab_cdef, - }) - ); - assert_eq!(parse_gpui_forwarding_endpoint("0:49152:1234"), None); - assert_eq!(parse_gpui_forwarding_endpoint("4321:0:1234"), None); - assert_eq!(parse_gpui_forwarding_endpoint("4321:49152:xyz"), None); - assert_eq!( - parse_gpui_forwarding_endpoint("4321:49152:1234:extra"), - None - ); - } - - #[test] - fn forwarded_gpui_deep_links_are_scheme_and_size_limited() { - assert!(is_forwardable_gpui_deep_link( - "cap-desktop://signin?token=test" - )); - assert!(is_forwardable_gpui_deep_link( - "cap://action?value=%22stop_recording%22" - )); - assert!(!is_forwardable_gpui_deep_link("https://cap.so/signin")); - assert!(!is_forwardable_gpui_deep_link("cap-desktop-other://signin")); - assert!(!is_forwardable_gpui_deep_link(&format!( - "cap://action?value={}", - "x".repeat(MAX_FORWARDED_DEEP_LINK_BYTES) - ))); - } - - #[test] - fn project_arguments_become_encoded_open_editor_actions() { - let directory = tempfile::tempdir().unwrap(); - let project = directory.path().join("Recording #1 & draft.cap"); - std::fs::create_dir(&project).unwrap(); - - let forwarded = forwarded_gpui_argument(project.to_str().unwrap()).unwrap(); - let url = reqwest::Url::parse(&forwarded).unwrap(); - let value = url - .query_pairs() - .find_map(|(key, value)| (key == "value").then_some(value.into_owned())) - .unwrap(); - let action: serde_json::Value = serde_json::from_str(&value).unwrap(); - - assert_eq!(url.scheme(), "cap-desktop"); - assert_eq!(url.host_str(), Some("action")); - assert_eq!( - action["open_editor"]["project_path"], - project.canonicalize().unwrap().to_string_lossy().as_ref() - ); - } - - #[test] - fn forwarded_project_arguments_reject_missing_and_non_project_paths() { - let directory = tempfile::tempdir().unwrap(); - let ordinary = directory.path().join("notes.txt"); - std::fs::write(&ordinary, "notes").unwrap(); - - assert!(forwarded_gpui_argument(ordinary.to_str().unwrap()).is_none()); - assert!( - forwarded_gpui_argument(directory.path().join("missing.cap").to_str().unwrap()) - .is_none() - ); - assert_eq!( - forwarded_gpui_argument("cap-desktop://signin?token=secret"), - Some("cap-desktop://signin?token=secret".to_string()) - ); - } - - #[cfg(unix)] - #[test] - fn forwarded_project_arguments_reject_symlinks() { - let directory = tempfile::tempdir().unwrap(); - let project = directory.path().join("Recording.cap"); - let symlink = directory.path().join("Shortcut.cap"); - std::fs::create_dir(&project).unwrap(); - std::os::unix::fs::symlink(&project, &symlink).unwrap(); - - assert!(forwarded_gpui_argument(symlink.to_str().unwrap()).is_none()); - } - - #[test] - fn file_urls_become_open_editor_actions() { - let directory = tempfile::tempdir().unwrap(); - let project = directory.path().join("Recording.cap"); - std::fs::create_dir(&project).unwrap(); - let file_url = reqwest::Url::from_file_path(&project).unwrap(); - - assert!(forwarded_gpui_argument(file_url.as_str()).is_some()); - } - - #[test] - fn only_launches_without_forwardable_actions_request_reopening() { - assert!(super::gpui_launch_needs_reopen(&[])); - let directory = tempfile::tempdir().unwrap(); - let project = directory.path().join("Recording.cap"); - std::fs::create_dir(&project).unwrap(); - for argument in [ - "cap-desktop://auth?token=fixture".to_string(), - "cap://action?value=%22stop_recording%22".to_string(), - project.to_str().unwrap().to_string(), - reqwest::Url::from_file_path(&project).unwrap().to_string(), - ] { - assert!(!super::gpui_launch_needs_reopen(&[argument])); - } - } - - #[cfg(target_os = "macos")] - #[test] - fn macos_reopen_sends_private_packet_and_requires_acknowledgment() { - use std::{ - io::{Read, Write}, - time::{Duration, Instant}, - }; - - for reply in [0_u8, 1] { - let listener = std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap(); - listener.set_nonblocking(true).unwrap(); - let endpoint = GpuiForwardingEndpoint { - pid: 123, - port: listener.local_addr().unwrap().port(), - secret: 456, - }; - let server = std::thread::spawn(move || { - let started = Instant::now(); - let mut stream = loop { - match listener.accept() { - Ok((stream, _)) => break stream, - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - assert!(started.elapsed() < Duration::from_secs(5)); - std::thread::sleep(Duration::from_millis(5)); - } - Err(error) => panic!("Reopen listener failed: {error}"), - } - }; - stream.set_nonblocking(false).unwrap(); - stream - .set_read_timeout(Some(Duration::from_secs(2))) - .unwrap(); - stream - .set_write_timeout(Some(Duration::from_secs(2))) - .unwrap(); - let mut packet = [0_u8; 12]; - stream.read_exact(&mut packet).unwrap(); - assert_eq!(&packet[..8], &endpoint.secret.to_be_bytes()); - assert_eq!(&packet[8..], &0_u32.to_be_bytes()); - stream.write_all(&[reply]).unwrap(); - }); - let result = super::send_gpui_reopen(endpoint, Instant::now(), Duration::from_secs(2)); - server.join().unwrap(); - if reply == 1 { - result.unwrap(); - } else { - assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData); - } - } - } - - #[cfg(target_os = "macos")] - #[test] - fn macos_reopen_deadline_also_bounds_missing_acknowledgment() { - use std::time::{Duration, Instant}; - - let listener = std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap(); - let endpoint = GpuiForwardingEndpoint { - pid: 123, - port: listener.local_addr().unwrap().port(), - secret: 456, - }; - assert_eq!( - super::send_gpui_reopen(endpoint, Instant::now(), Duration::from_millis(100)) - .unwrap_err() - .kind(), - std::io::ErrorKind::TimedOut - ); - let expired = Instant::now().checked_sub(Duration::from_secs(2)).unwrap(); - assert_eq!( - super::send_gpui_reopen(endpoint, expired, Duration::from_secs(2)) - .unwrap_err() - .kind(), - std::io::ErrorKind::TimedOut - ); - } - - #[cfg(target_os = "macos")] - #[test] - fn macos_reopen_rejects_shared_or_linked_endpoint_and_changed_pid() { - use std::os::unix::fs::{PermissionsExt, symlink}; - - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("cap-gpui.ipc"); - std::fs::write(&path, "123:456:789").unwrap(); - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); - assert_eq!( - super::read_macos_gpui_metadata(&path, true).unwrap(), - "123:456:789" - ); - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap(); - assert_eq!( - super::read_macos_gpui_metadata(&path, true) - .unwrap_err() - .kind(), - std::io::ErrorKind::PermissionDenied - ); - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); - let linked = directory.path().join("linked.ipc"); - symlink(&path, &linked).unwrap(); - assert!(super::read_macos_gpui_metadata(&linked, true).is_err()); - std::fs::remove_file(&linked).unwrap(); - std::fs::hard_link(&path, &linked).unwrap(); - assert!(super::read_macos_gpui_metadata(&path, true).is_err()); - let pidfile = directory.path().join("cap-gpui.pid"); - std::fs::write(&pidfile, "123").unwrap(); - assert!(!super::macos_reopen_incumbent(&pidfile, 456).unwrap()); - assert_eq!(std::fs::read_to_string(&path).unwrap(), "123:456:789"); - assert_eq!(std::fs::read_to_string(&pidfile).unwrap(), "123"); - } -} diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-tauri/src/hotkeys.rs deleted file mode 100644 index 257a8f59b49..00000000000 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ /dev/null @@ -1,854 +0,0 @@ -use crate::{ - App, ArcLock, RequestOpenRecordingPicker, RequestStartRecording, recording, - recording_settings::{RecordingSettingsStore, RecordingTargetMode}, - tray, - windows::ShowCapWindow, -}; -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::future::Future; -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)] -pub struct Hotkey { - #[specta(type = String)] - code: Code, - 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; - } - if hotkey.ctrl { - modifiers |= Modifiers::CONTROL; - } - if hotkey.alt { - modifiers |= Modifiers::ALT; - } - if hotkey.shift { - modifiers |= Modifiers::SHIFT; - } - - Shortcut::new(Some(modifiers), hotkey.code) - } -} - -#[derive(Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy, Debug)] -#[serde(rename_all = "camelCase")] -#[allow(clippy::enum_variant_names)] -pub enum HotkeyAction { - StartStudioRecording, - StartInstantRecording, - StopRecording, - RestartRecording, - TogglePauseRecording, - CycleRecordingMode, - OpenRecordingPicker, - OpenRecordingPickerDisplay, - OpenRecordingPickerWindow, - OpenRecordingPickerArea, - ScreenshotDisplay, - ScreenshotWindow, - ScreenshotArea, - #[serde(other)] - Other, -} - -#[derive(Serialize, Deserialize, Type, Default)] -pub struct HotkeysStore { - hotkeys: HashMap, -} - -impl HotkeysStore { - pub fn get(app: &AppHandle) -> Result, String> { - let Ok(Some(store)) = app.store("store").map(|s| s.get("hotkeys")) else { - return Ok(None); - }; - - serde_json::from_value(store).map_err(|e| e.to_string()) - } -} - -#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] -pub struct OnEscapePress; - -pub type HotkeysState = Mutex; - -fn clean_capture_shortcut() -> Shortcut { - Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9) -} - -#[cfg(any(target_os = "linux", test))] -fn existing_clean_capture_stop(store: &HotkeysStore) -> Result { - let shortcut = clean_capture_shortcut(); - let mut existing_stop = false; - for (action, hotkey) in &store.hotkeys { - if Shortcut::from(*hotkey) == shortcut { - if !matches!(action, HotkeyAction::StopRecording) { - return Err("Ctrl+Shift+F9 is assigned to another Cap action. Change that shortcut before starting clean Studio capture.".into()); - } - existing_stop = true; - } - } - Ok(existing_stop) -} - -#[cfg(target_os = "linux")] -pub(crate) fn reserve_clean_capture_stop(app: &AppHandle) -> Result { - let shortcut = clean_capture_shortcut(); - let state = app.state::(); - let store = state.lock().unwrap(); - let existing_stop = existing_clean_capture_stop(&store)?; - let shortcuts = app.global_shortcut(); - if shortcuts.is_registered(shortcut) { - return if existing_stop { - Ok(false) - } else { - Err("Ctrl+Shift+F9 is already reserved. Release it before starting clean Studio capture.".into()) - }; - } - shortcuts.register(shortcut).map_err(|error| { - format!("Cannot reserve Ctrl+Shift+F9 to stop recording: {error}. Change the conflicting system shortcut and try again.") - })?; - Ok(true) -} - -pub(crate) fn release_clean_capture_stop(app: &AppHandle, owned: bool) { - if owned && let Err(error) = app.global_shortcut().unregister(clean_capture_shortcut()) { - tracing::warn!(%error, "Could not release temporary recording shortcut"); - } -} - -const RECORDING_START_SAFETY_STORE_KEY: &str = "recording_start_safety"; - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase", default)] -struct RecordingStartSafetySettings { - confirm_before_recording_without_microphone: bool, -} - -impl Default for RecordingStartSafetySettings { - fn default() -> Self { - Self { - confirm_before_recording_without_microphone: true, - } - } -} - -fn should_confirm_without_microphone(enabled: bool, microphone_available: bool) -> bool { - enabled && !microphone_available -} - -#[cfg(any(target_os = "macos", test))] -fn microphone_available_for_confirmation( - name: Option<&str>, - permission: impl FnOnce() -> Result<(), String>, - contains: impl FnOnce(&str) -> bool, -) -> Result { - let Some(name) = name else { - return Ok(false); - }; - permission().map_err(|error| { - format!("{error} To record without a microphone, turn the microphone Off in Cap.") - })?; - if !contains(name) { - return Err(format!( - "Selected microphone '{name}' is no longer available. Reconnect it, select another microphone, or turn the microphone Off to record without it." - )); - } - Ok(true) -} - -fn should_confirm_direct_recording(app: &AppHandle) -> Result { - let enabled = app - .store("store") - .ok() - .and_then(|store| store.get(RECORDING_START_SAFETY_STORE_KEY)) - .and_then(|value| serde_json::from_value::(value).ok()) - .unwrap_or_default() - .confirm_before_recording_without_microphone; - - #[cfg(not(target_os = "macos"))] - if !enabled { - return Ok(false); - } - - let microphone_name = RecordingSettingsStore::get(app) - .ok() - .flatten() - .and_then(|settings| settings.mic_name); - #[cfg(target_os = "macos")] - let microphone_available = microphone_available_for_confirmation( - microphone_name.as_deref(), - crate::permissions::check_microphone_access, - |name| { - MicrophoneFeed::list_names() - .iter() - .any(|device| device == name) - }, - )?; - #[cfg(not(target_os = "macos"))] - let microphone_available = microphone_name - .as_deref() - .is_some_and(|name| MicrophoneFeed::list().contains_key(name)); - - Ok(should_confirm_without_microphone( - enabled, - microphone_available, - )) -} - -async fn confirm_direct_recording_without_microphone(app: &AppHandle) -> bool { - match should_confirm_direct_recording(app) { - Ok(false) => return true, - Ok(true) => {} - Err(message) => { - app.dialog() - .message(message) - .title("Microphone unavailable") - .kind(MessageDialogKind::Warning) - .buttons(MessageDialogButtons::Ok) - .show(|_| {}); - return false; - } - } - - 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) -} - -async fn start_recording_from_hotkey( - app: AppHandle, - mode: cap_recording::RecordingMode, -) -> Result<(), String> { - if app - .state::>() - .read() - .await - .is_recording_active_or_pending() - { - let _ = RequestStartRecording { mode }.emit(&app); - return Ok(()); - } - - if confirm_direct_recording_without_microphone(&app).await { - let _ = RequestStartRecording { mode }.emit(&app); - } - - Ok(()) -} - -fn spawn_shortcut_task(task: F) -where - F: Future + Send + 'static, - F::Output: Send + 'static, -{ - drop(tauri::async_runtime::spawn(task)); -} - -pub fn init(app: &AppHandle) { - app.plugin( - tauri_plugin_global_shortcut::Builder::new() - .with_handler(|app, shortcut, event| { - if *shortcut == clean_capture_shortcut() - && crate::clean_capture::handle_shortcut( - app, - event.state() == HotKeyState::Pressed, - ) - { - return; - } - if !matches!(event.state(), HotKeyState::Pressed) { - return; - } - - if shortcut.key == Code::Escape { - // This handler runs on the main thread while the plugin - // holds its shortcut registry lock. Dismissing the picker - // ends the picker session, which unregisters Escape through - // that same lock, so doing it inline deadlocks the main - // thread (overlay stuck on screen, app frozen). Defer it. - let dismiss_app = app.clone(); - spawn_shortcut_task(async move { - crate::target_select_overlay::dismiss_picker_from_escape(&dismiss_app); - }); - OnEscapePress.emit(app).ok(); - } - - if shortcut.key == Code::Comma && shortcut.mods == Modifiers::META { - let app = app.clone(); - spawn_shortcut_task(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 { - spawn_shortcut_task(handle_hotkey(app.clone(), *action)); - } - } - }) - .build(), - ) - .unwrap(); - - let store = match HotkeysStore::get(app) { - Ok(Some(s)) => s, - Ok(None) => HotkeysStore::default(), - Err(e) => { - tracing::error!("Failed to load hotkeys store: {e}"); - HotkeysStore::default() - } - }; - - let global_shortcut = app.global_shortcut(); - for hotkey in store.hotkeys.values() { - global_shortcut.register(Shortcut::from(*hotkey)).ok(); - } - - app.manage(Mutex::new(store)); -} - -async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), String> { - match action { - HotkeyAction::StartStudioRecording => { - start_recording_from_hotkey(app, cap_recording::RecordingMode::Studio).await - } - HotkeyAction::StartInstantRecording => { - start_recording_from_hotkey(app, cap_recording::RecordingMode::Instant).await - } - HotkeyAction::StopRecording => recording::stop_recording(app.clone(), app.state()).await, - HotkeyAction::RestartRecording => recording::restart_recording(app.clone(), app.state()) - .await - .map(|_| ()), - HotkeyAction::TogglePauseRecording => { - recording::toggle_pause_recording(app.clone(), app.state()).await - } - HotkeyAction::CycleRecordingMode => { - let current = RecordingSettingsStore::get(&app) - .ok() - .flatten() - .and_then(|s| s.mode) - .unwrap_or_default(); - - let next = match current { - cap_recording::RecordingMode::Studio => cap_recording::RecordingMode::Instant, - cap_recording::RecordingMode::Instant => cap_recording::RecordingMode::Screenshot, - cap_recording::RecordingMode::Screenshot => cap_recording::RecordingMode::Studio, - }; - - RecordingSettingsStore::set_mode(&app, next) - .map_err(|e| format!("Failed to cycle mode: {e}"))?; - - tray::update_tray_icon_for_mode(&app, next); - - Ok(()) - } - HotkeyAction::OpenRecordingPicker => { - let _ = RequestOpenRecordingPicker { target_mode: None }.emit(&app); - Ok(()) - } - HotkeyAction::OpenRecordingPickerDisplay => { - let _ = RequestOpenRecordingPicker { - target_mode: Some(RecordingTargetMode::Display), - } - .emit(&app); - Ok(()) - } - HotkeyAction::OpenRecordingPickerWindow => { - let _ = RequestOpenRecordingPicker { - target_mode: Some(RecordingTargetMode::Window), - } - .emit(&app); - Ok(()) - } - HotkeyAction::OpenRecordingPickerArea => { - let _ = RequestOpenRecordingPicker { - target_mode: Some(RecordingTargetMode::Area), - } - .emit(&app); - Ok(()) - } - HotkeyAction::ScreenshotDisplay => { - 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(e) => Err(format!("Failed to take screenshot: {e}")), - } - } - HotkeyAction::ScreenshotWindow => { - use scap_targets::Window; - - let target = { - let window = Window::get_topmost_at_cursor() - .ok_or_else(|| "No window found under cursor".to_string())?; - ScreenCaptureTarget::Window { id: window.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(e) => Err(format!("Failed to take screenshot: {e}")), - } - } - HotkeyAction::ScreenshotArea => { - RecordingSettingsStore::set_mode(&app, cap_recording::RecordingMode::Screenshot) - .map_err(|e| format!("Failed to set screenshot mode: {e}"))?; - - tray::update_tray_icon_for_mode(&app, cap_recording::RecordingMode::Screenshot); - - let _ = RequestOpenRecordingPicker { - target_mode: Some(RecordingTargetMode::Area), - } - .emit(&app); - Ok(()) - } - HotkeyAction::Other => Ok(()), - } -} - -#[tauri::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(); - if crate::clean_capture::phase(&app).is_some() { - return Err(()); - } - - 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(); - } - - Ok(()) -} - -#[cfg(target_os = "linux")] -pub(crate) struct WaylandStop { - generation: u32, - cancel: tokio::sync::watch::Sender, - task: Option>>, - completed: Option>, -} - -#[cfg(target_os = "linux")] -pub(crate) async fn reserve_wayland_stop(app: &AppHandle, generation: u32) -> Result<(), String> { - let state = app.state::(); - let mut owned = state.portal_stop.lock().await; - if owned.is_some() { - return Err("Previous Wayland Stop session has not closed".into()); - } - let (cancel, receiver) = tokio::sync::watch::channel(false); - let handle = app.clone(); - let task = tauri::async_runtime::spawn(async move { - if run_wayland_tray(&handle, generation, receiver.clone()).await? { - Ok(()) - } else { - run_wayland_stop(&handle, generation, receiver).await - } - }); - *owned = Some(WaylandStop { - generation, - cancel, - task: Some(task), - completed: None, - }); - Ok(()) -} - -#[cfg(target_os = "linux")] -async fn run_wayland_stop( - app: &AppHandle, - generation: u32, - mut cancel: tokio::sync::watch::Receiver, -) -> Result<(), String> { - use ashpd::desktop::global_shortcuts::{GlobalShortcuts, NewShortcut}; - use futures::StreamExt; - let portal = tokio::select! { - _ = cancel.changed() => return Ok(()), - result = GlobalShortcuts::new() => match result { - Ok(portal) => portal, - Err(error) => { report_wayland_stop_loss(app, generation, &error.to_string()); return Ok(()); } - }, - }; - if *cancel.borrow() { - return Ok(()); - } - // Keep session creation owned even if Stop arrives while the portal replies; - // dropping CreateSession could lose the only handle needed to close it. - let session = match portal.create_session().await { - Ok(session) => session, - Err(error) => { - report_wayland_stop_loss(app, generation, &error.to_string()); - return Ok(()); - } - }; - let result = async { - let session_path = serde_json::to_value(&session).map_err(|error| error.to_string())?; - let session_path = session_path - .as_str() - .ok_or("Invalid Stop session identity")?; - let id = format!("cap-clean-stop-{generation}"); - let mut activated = portal - .receive_activated() - .await - .map_err(|error| error.to_string())?; - let mut deactivated = portal - .receive_deactivated() - .await - .map_err(|error| error.to_string())?; - let mut closed = session - .receive_closed() - .await - .map_err(|error| error.to_string())?; - let bound = portal - .bind_shortcuts( - &session, - &[NewShortcut::new(&id, "Start or stop Cap clean recording") - .preferred_trigger(Some("CTRL+SHIFT+F9"))], - None, - ) - .await - .map_err(|error| error.to_string())? - .response() - .map_err(|error| error.to_string())?; - let shortcut = bound - .shortcuts() - .iter() - .find(|shortcut| shortcut.id() == id) - .ok_or("Stop shortcut was not granted")?; - crate::clean_capture::describe_wayland_stop( - app, - generation, - format!("{} (desktop shortcut)", shortcut.trigger_description()), - ); - loop { - tokio::select! { - event = activated.next() => match event { - Some(event) if event.session_handle().as_str() == session_path && event.shortcut_id() == id => { - crate::clean_capture::handle_wayland_stop(app, generation, crate::clean_capture::StopRoute::Portal, true); - } - Some(_) => {} - None => return Err("Stop shortcut event stream closed".into()), - }, - event = deactivated.next() => match event { - Some(event) if event.session_handle().as_str() == session_path && event.shortcut_id() == id => { - crate::clean_capture::handle_wayland_stop(app, generation, crate::clean_capture::StopRoute::Portal, false); - } - Some(_) => {} - None => return Err("Stop shortcut event stream closed".into()), - }, - _ = closed.next() => return Err("Stop shortcut session was revoked".into()), - } - } - }; - let result: Result<(), String> = if *cancel.borrow() { - Ok(()) - } else { - tokio::select! { - _ = cancel.changed() => Ok(()), - result = result => result, - } - }; - if let Err(error) = &result { - report_wayland_stop_loss(app, generation, error); - } - tokio::time::timeout(std::time::Duration::from_secs(2), session.close()) - .await - .map_err(|_| "Stop session close timed out".to_string())? - .map_err(|error| error.to_string())?; - Ok(()) -} - -#[cfg(target_os = "linux")] -fn report_wayland_stop_loss(app: &AppHandle, generation: u32, error: &str) { - crate::clean_capture::wayland_stop_lost( - app, - generation, - crate::clean_capture::StopRoute::Portal, - error.to_string(), - ); -} - -#[cfg(target_os = "linux")] -pub(crate) async fn release_wayland_stop(app: &AppHandle, generation: u32) -> Result<(), String> { - let state = app.state::(); - let mut owned = state.portal_stop.lock().await; - let Some(stop) = owned.as_mut().filter(|stop| stop.generation == generation) else { - return Ok(()); - }; - if let Some(result) = &stop.completed { - return result.clone(); - } - let _ = stop.cancel.send(true); - let task = stop - .task - .as_mut() - .ok_or("Stop session worker ownership was lost")?; - let joined = tokio::time::timeout(std::time::Duration::from_secs(3), task) - .await - .map_err(|_| "Stop session worker has not joined".to_string())?; - let _finished = stop.task.take(); - let result = joined - .map_err(|error| error.to_string()) - .and_then(|result| result); - stop.completed = Some(result.clone()); - if result.is_ok() { - let _released = owned.take(); - } - result -} - -#[cfg(target_os = "linux")] -async fn run_wayland_tray( - app: &AppHandle, - generation: u32, - mut cancel: tokio::sync::watch::Receiver, -) -> Result { - use cap_utils::linux_recording_stop::{StopTray, StopTrayEvent}; - let icon = match crate::tray::clean_stop_icon() { - Ok(icon) => icon, - Err(error) => { - crate::clean_capture::wayland_stop_lost( - app, - generation, - crate::clean_capture::StopRoute::Tray, - error, - ); - return Ok(false); - } - }; - let tray = match StopTray::open(u64::from(generation), icon).await { - Ok(tray) => tray, - Err(error) => { - let can_fallback = error.can_fallback(); - let message = error.to_string(); - crate::clean_capture::wayland_stop_lost( - app, - generation, - crate::clean_capture::StopRoute::Tray, - message.clone(), - ); - if !can_fallback { - crate::clean_capture::wayland_stop_lost( - app, - generation, - crate::clean_capture::StopRoute::Portal, - message.clone(), - ); - return Err(message); - } - return Ok(false); - } - }; - let events = tray.events(); - let mut used = false; - let mut cancelled = false; - loop { - tokio::select! { - _ = cancel.changed() => { cancelled = true; break; }, - event = events.recv_async() => match event { - Ok(StopTrayEvent::Activated { generation: received }) if received == u64::from(generation) => { - used = true; - crate::clean_capture::handle_wayland_stop(app, generation, crate::clean_capture::StopRoute::Tray, true); - crate::clean_capture::handle_wayland_stop(app, generation, crate::clean_capture::StopRoute::Tray, false); - } - Ok(StopTrayEvent::Unavailable { generation: received }) if received == u64::from(generation) => { - crate::clean_capture::wayland_stop_lost(app, generation, crate::clean_capture::StopRoute::Tray, "The Stop tray host is unavailable".into()); - break; - } - Ok(_) => {} - Err(_) => { - crate::clean_capture::wayland_stop_lost(app, generation, crate::clean_capture::StopRoute::Tray, "The Stop tray event stream closed".into()); - break; - } - } - } - } - tray.close().await?; - Ok(used || cancelled) -} - -#[cfg(test)] -mod tests { - use super::{ - microphone_available_for_confirmation, should_confirm_without_microphone, - spawn_shortcut_task, - }; - use std::time::Duration; - - #[test] - fn microphone_confirmation_does_not_probe_without_permission_or_selection() { - assert!( - microphone_available_for_confirmation( - Some("Saved microphone"), - || Err("Permission denied".into()), - |_| panic!("denied microphone must not be enumerated"), - ) - .unwrap_err() - .contains("turn the microphone Off") - ); - assert!( - !microphone_available_for_confirmation( - None, - || panic!("disabled microphone does not need permission"), - |_| panic!("disabled microphone must not be enumerated"), - ) - .unwrap() - ); - } - - #[test] - fn microphone_confirmation_checks_only_the_requested_name_after_grant() { - assert!( - microphone_available_for_confirmation( - Some("Saved microphone"), - || Ok(()), - |name| name == "Saved microphone", - ) - .unwrap() - ); - let error = microphone_available_for_confirmation( - Some("Disconnected microphone"), - || Ok(()), - |_| false, - ) - .unwrap_err(); - assert!(error.contains("Reconnect it")); - assert!(error.contains("turn the microphone Off")); - } - - #[test] - fn clean_capture_reuses_only_an_existing_stop_binding() { - let hotkey = super::Hotkey { - code: super::Code::F9, - meta: false, - ctrl: true, - alt: false, - shift: true, - }; - let mut store = super::HotkeysStore::default(); - assert!(!super::existing_clean_capture_stop(&store).unwrap()); - store - .hotkeys - .insert(super::HotkeyAction::StopRecording, hotkey); - assert!(super::existing_clean_capture_stop(&store).unwrap()); - store - .hotkeys - .insert(super::HotkeyAction::StartStudioRecording, hotkey); - assert!(super::existing_clean_capture_stop(&store).is_err()); - } - - #[test] - fn shortcut_dispatch_runs_from_thread_without_tokio_runtime() { - let (sent, received) = std::sync::mpsc::channel(); - let caller = std::thread::spawn(move || { - assert!(tokio::runtime::Handle::try_current().is_err()); - let caller_id = std::thread::current().id(); - spawn_shortcut_task(async move { - assert!(tokio::runtime::Handle::try_current().is_ok()); - tokio::time::sleep(Duration::from_millis(1)).await; - sent.send((caller_id, std::thread::current().id())).unwrap(); - Ok::<(), String>(()) - }); - }); - caller.join().expect("OS callback thread must not panic"); - let (caller_id, task_id) = received - .recv_timeout(Duration::from_secs(2)) - .expect("Shortcut action must run on the application runtime"); - assert_ne!(caller_id, task_id); - } - - #[test] - fn shortcut_dispatch_keeps_detached_settings_task_alive() { - let (sent, received) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - assert!(tokio::runtime::Handle::try_current().is_err()); - spawn_shortcut_task(async move { - tokio::time::sleep(Duration::from_millis(1)).await; - sent.send(()).unwrap(); - }); - }) - .join() - .expect("OS callback thread must not panic"); - received - .recv_timeout(Duration::from_secs(2)) - .expect("Detached settings task must complete"); - } - - #[test] - fn confirms_when_enabled_without_microphone() { - assert!(should_confirm_without_microphone(true, false)); - } - - #[test] - fn skips_confirmation_with_selected_microphone() { - assert!(!should_confirm_without_microphone(true, true)); - } - - #[test] - fn skips_confirmation_when_disabled() { - assert!(!should_confirm_without_microphone(false, false)); - } -} diff --git a/apps/desktop/src-tauri/src/http_client.rs b/apps/desktop/src-tauri/src/http_client.rs deleted file mode 100644 index bebaf889669..00000000000 --- a/apps/desktop/src-tauri/src/http_client.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! We reuse clients so we get connection pooling and also so the retry policy can handle backing off across requests. - -use std::ops::Deref; - -use reqwest::StatusCode; - -pub struct HttpClient(reqwest::Client); - -impl Default for HttpClient { - fn default() -> Self { - Self( - reqwest::Client::builder() - .connect_timeout(std::time::Duration::from_secs(10)) - .timeout(std::time::Duration::from_secs(30)) - .build() - .expect("Failed to build HTTP client"), - ) - } -} - -impl Deref for HttpClient { - type Target = reqwest::Client; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct RetryableHttpClient(reqwest::Result); - -impl Default for RetryableHttpClient { - fn default() -> Self { - Self( - reqwest::Client::builder() - .connect_timeout(std::time::Duration::from_secs(10)) - .retry( - reqwest::retry::always() - .classify_fn(|req_rep| match req_rep.status() { - Some(s) - if s.is_server_error() || s == StatusCode::TOO_MANY_REQUESTS => - { - req_rep.retryable() - } - None => req_rep.retryable(), - _ => req_rep.success(), - }) - .max_retries_per_request(5) - .max_extra_load(5.0), - ) - .build(), - ) - } -} - -impl Deref for RetryableHttpClient { - type Target = reqwest::Result; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/apps/desktop/src-tauri/src/import.rs b/apps/desktop/src-tauri/src/import.rs deleted file mode 100644 index 3b517fcd546..00000000000 --- a/apps/desktop/src-tauri/src/import.rs +++ /dev/null @@ -1,2268 +0,0 @@ -use cap_enc_ffmpeg::{ - AudioEncoder, - h264::{H264EncoderBuilder, H264Preset}, - opus::OpusEncoder, - remux::{get_media_duration, probe_video_can_decode}, -}; -use cap_media_info::{AudioInfo, FFRational, Pixel, VideoInfo, ensure_even}; -use cap_project::{ - AudioMeta, ClipConfiguration, CursorEvents, CursorMeta, Cursors, InstantRecordingMeta, - MultipleSegment, MultipleSegments, Platform, ProjectConfiguration, RecordingMeta, - RecordingMetaInner, SingleSegment, StudioRecordingMeta, StudioRecordingStatus, - TimelineConfiguration, TimelineSegment, VideoMeta, XY, -}; -use ffmpeg::{ - ChannelLayout, - codec::{self as avcodec}, - format::{self as avformat}, -}; -use image::ImageEncoder; -use relative_path::{Component as RelativeComponent, RelativePathBuf}; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - str::FromStr, -}; -use tauri::{AppHandle, Manager, Window}; -use tauri_specta::Event; -use tracing::{debug, error, info}; - -use crate::{ - create_screenshot, - editor_window::EditorInstances, - windows::{CapWindowId, EditorWindowIds}, -}; - -const VIDEO_IMPORT_EXTENSIONS: &[&str] = &["mp4", "mov", "avi", "mkv", "webm", "wmv", "m4v", "flv"]; -const IMAGE_IMPORT_EXTENSIONS: &[&str] = - &["png", "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff"]; -const AUDIO_IMPORT_EXTENSIONS: &[&str] = &["ogg", "m4a", "mp3", "wav", "aac", "flac"]; -const KEYBOARD_IMPORT_EXTENSIONS: &[&str] = &["bin", "json"]; -const CURSOR_EVENTS_IMPORT_EXTENSIONS: &[&str] = &["json"]; -const MAX_IMAGE_DIMENSION: u32 = 16_384; - -#[derive(Serialize, Deserialize, Type, Clone, Debug)] -pub enum ImportStage { - Probing, - Converting, - Finalizing, - Complete, - Failed, -} - -#[derive(Serialize, Deserialize, Type, tauri_specta::Event, Clone, Debug)] -pub struct VideoImportProgress { - pub project_path: String, - pub stage: ImportStage, - pub progress: f64, - pub message: String, -} - -#[derive(Debug, thiserror::Error)] -pub enum ImportError { - #[error("Failed to open video file: {0}")] - OpenFailed(String), - #[error("No video stream found in file")] - NoVideoStream, - #[error("Failed to create decoder: {0}")] - DecoderFailed(String), - #[error("Failed to create encoder: {0}")] - EncoderFailed(String), - #[error("Failed to create project directory: {0}")] - DirectoryFailed(std::io::Error), - #[error("FFmpeg error: {0}")] - Ffmpeg(#[from] ffmpeg::Error), - #[error("Transcoding failed: {0}")] - TranscodeFailed(String), -} - -fn emit_progress( - app: &AppHandle, - project_path: &str, - stage: ImportStage, - progress: f64, - message: &str, -) { - let _ = VideoImportProgress { - project_path: project_path.to_string(), - stage, - progress, - message: message.to_string(), - } - .emit(app); -} - -fn check_project_exists(project_path: &Path) -> bool { - project_path.exists() && project_path.join("recording-meta.json").exists() -} - -fn generate_project_name(source_path: &Path) -> String { - let stem = source_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("Imported Video"); - - let now = chrono::Local::now(); - let date_str = now.format("%Y-%m-%d at %H.%M.%S").to_string(); - - format!("{stem} {date_str}") -} - -fn generate_image_project_name(source_path: &Path) -> String { - let stem = source_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("Imported Image"); - - let now = chrono::Local::now(); - let date_str = now.format("%Y-%m-%d at %H.%M.%S").to_string(); - - format!("{stem} {date_str}") -} - -fn sanitize_filename(name: &str) -> String { - name.chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', - _ => c, - }) - .collect() -} - -fn has_supported_extension(path: &Path, extensions: &[&str]) -> bool { - path.extension() - .and_then(|s| s.to_str()) - .is_some_and(|ext| { - extensions - .iter() - .any(|candidate| ext.eq_ignore_ascii_case(candidate)) - }) -} - -pub fn is_supported_video_import_path(path: &Path) -> bool { - path.is_file() && has_supported_extension(path, VIDEO_IMPORT_EXTENSIONS) -} - -pub fn is_supported_image_import_path(path: &Path) -> bool { - path.is_file() && has_supported_extension(path, IMAGE_IMPORT_EXTENSIONS) -} - -fn is_mp4_import_path(path: &Path) -> bool { - path.is_file() - && path - .extension() - .and_then(|s| s.to_str()) - .is_some_and(|ext| ext.eq_ignore_ascii_case("mp4")) -} - -fn is_cap_project_path(path: &Path) -> bool { - path.is_dir() && path.join("recording-meta.json").is_file() -} - -fn normalized_metadata_relative_path( - path: &RelativePathBuf, - asset_kind: &str, -) -> Result { - let normalized = path.as_str().replace('\\', "/"); - let path = RelativePathBuf::from(normalized); - let raw = path.as_str(); - if raw.is_empty() - || raw.starts_with('/') - || raw.contains(':') - || path - .components() - .any(|component| matches!(component, RelativeComponent::ParentDir)) - { - return Err(format!( - "Invalid {asset_kind} path in recording metadata: {raw}" - )); - } - - Ok(path) -} - -fn source_asset_path( - source_project_path: &Path, - source_relative_path: &RelativePathBuf, - asset_kind: &str, - allowed_extensions: &[&str], -) -> Result, String> { - let source_relative_path = normalized_metadata_relative_path(source_relative_path, asset_kind)?; - - if !has_supported_extension(Path::new(source_relative_path.as_str()), allowed_extensions) { - return Err(format!( - "Unsupported {asset_kind} file type: {}", - source_relative_path.as_str() - )); - } - - let source_path = source_relative_path.to_path(source_project_path); - if !source_path.is_file() { - return Ok(None); - } - - let source_root = source_project_path - .canonicalize() - .map_err(|e| format!("Failed to resolve source project path: {e}"))?; - let canonical_source_path = source_path - .canonicalize() - .map_err(|e| format!("Failed to resolve {asset_kind} path: {e}"))?; - - if !canonical_source_path.starts_with(&source_root) { - return Err(format!( - "{asset_kind} path escapes source project: {}", - source_relative_path.as_str() - )); - } - - Ok(Some(canonical_source_path)) -} - -fn required_source_asset_path( - source_project_path: &Path, - source_relative_path: &RelativePathBuf, - asset_kind: &str, - allowed_extensions: &[&str], -) -> Result { - source_asset_path( - source_project_path, - source_relative_path, - asset_kind, - allowed_extensions, - )? - .ok_or_else(|| { - format!( - "Missing {asset_kind} file: {}", - source_relative_path.to_path(source_project_path).display() - ) - }) -} - -fn legacy_cursor_relative_path(path: &str) -> Result { - normalized_metadata_relative_path(&RelativePathBuf::from(path), "cursor image") -} - -fn editor_project_path_from_window(window: &Window) -> Result { - let CapWindowId::Editor { id } = - CapWindowId::from_str(window.label()).map_err(|e| e.to_string())? - else { - return Err("Import can only be started from an editor window".to_string()); - }; - - let window_ids = EditorWindowIds::get(window.app_handle()); - let window_ids = window_ids - .ids - .lock() - .map_err(|_| "Editor window registry unavailable".to_string())?; - - window_ids - .iter() - .find(|(_, window_id)| *window_id == id) - .map(|(path, _)| path.clone()) - .ok_or_else(|| "Editor project path not found".to_string()) -} - -fn same_project_path(a: &Path, b: &Path) -> bool { - let a = a.canonicalize().unwrap_or_else(|_| a.to_path_buf()); - let b = b.canonicalize().unwrap_or_else(|_| b.to_path_buf()); - a == b -} - -fn ensure_multiple_segments(meta: &mut RecordingMeta) -> Result<&mut MultipleSegments, String> { - let RecordingMetaInner::Studio(studio_meta) = &mut meta.inner else { - return Err("Instant mode recordings cannot be edited".to_string()); - }; - - if let StudioRecordingMeta::SingleSegment { segment } = studio_meta.as_ref() { - let segment = segment.clone(); - **studio_meta = StudioRecordingMeta::MultipleSegments { - inner: MultipleSegments { - segments: vec![MultipleSegment { - display: segment.display, - camera: segment.camera, - mic: segment.audio, - system_audio: None, - cursor: segment.cursor, - keyboard: None, - display_notch: None, - }], - cursors: Cursors::default(), - status: Some(StudioRecordingStatus::Complete), - }, - }; - } - - match studio_meta.as_mut() { - StudioRecordingMeta::MultipleSegments { inner } => Ok(inner), - StudioRecordingMeta::SingleSegment { .. } => { - Err("Failed to normalize project recording segments".to_string()) - } - } -} - -fn get_video_duration_secs(path: &Path) -> Result { - get_media_duration(path) - .map(|duration| duration.as_secs_f64()) - .ok_or_else(|| format!("Could not determine video duration: {}", path.display())) -} - -fn full_timeline_for_segments( - project_path: &Path, - segments: &[MultipleSegment], -) -> Result, String> { - segments - .iter() - .enumerate() - .map(|(index, segment)| { - let duration = get_video_duration_secs(&segment.display.path.to_path(project_path))?; - Ok(TimelineSegment { - recording_clip: index as u32, - timescale: 1.0, - start: 0.0, - end: duration, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }) - }) - .collect() -} - -fn get_source_video_duration_secs( - source_meta: &RecordingMeta, - video: &VideoMeta, -) -> Result { - let source_path = required_source_asset_path( - &source_meta.project_path, - &video.path, - "video", - VIDEO_IMPORT_EXTENSIONS, - )?; - get_video_duration_secs(&source_path) -} - -fn full_timeline_for_source_segments( - source_meta: &RecordingMeta, - segments: &[MultipleSegment], -) -> Result, String> { - segments - .iter() - .enumerate() - .map(|(index, segment)| { - let duration = get_source_video_duration_secs(source_meta, &segment.display)?; - Ok(TimelineSegment { - recording_clip: index as u32, - timescale: 1.0, - start: 0.0, - end: duration, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }) - }) - .collect() -} - -fn ensure_project_timeline<'a>( - config: &'a mut ProjectConfiguration, - project_path: &Path, - segments: &[MultipleSegment], -) -> Result<&'a mut TimelineConfiguration, String> { - if config.timeline.is_none() { - config.timeline = Some(TimelineConfiguration { - segments: full_timeline_for_segments(project_path, segments)?, - transitions: Vec::new(), - zoom_segments: Vec::new(), - scene_segments: Vec::new(), - style_segments: Vec::new(), - image_segments: Vec::new(), - mask_segments: Vec::new(), - text_segments: Vec::new(), - caption_segments: Vec::new(), - keyboard_segments: Vec::new(), - audio_segments: Vec::new(), - camera3d_segments: Vec::new(), - }); - } - - config - .timeline - .as_mut() - .ok_or_else(|| "Failed to prepare project timeline".to_string()) -} - -fn add_clip_configs( - config: &mut ProjectConfiguration, - base_index: u32, - segments: &[MultipleSegment], -) { - for (offset, segment) in segments.iter().enumerate() { - let index = base_index + offset as u32; - let offsets = segment.calculate_audio_offsets(); - - if let Some(existing) = config.clips.iter_mut().find(|clip| clip.index == index) { - existing.offsets = offsets; - existing.offsets_auto_calculated = true; - } else { - config.clips.push(ClipConfiguration { - index, - offsets, - offsets_auto_calculated: true, - }); - } - } -} - -fn unique_segment_dir( - project_path: &Path, - index: u32, -) -> Result<(PathBuf, String), std::io::Error> { - let segments_root = project_path.join("content").join("segments"); - std::fs::create_dir_all(&segments_root)?; - - let mut counter = 0; - loop { - let name = if counter == 0 { - format!("segment-{index}") - } else { - format!("segment-{index}-import-{counter}") - }; - let path = segments_root.join(&name); - if !path.exists() { - std::fs::create_dir_all(&path)?; - return Ok((path, format!("content/segments/{name}"))); - } - counter += 1; - } -} - -fn relative_file_extension(path: &RelativePathBuf, fallback: &str) -> String { - Path::new(path.as_str()) - .extension() - .and_then(|ext| ext.to_str()) - .filter(|ext| !ext.is_empty()) - .unwrap_or(fallback) - .to_string() -} - -fn relative_file_name(path: &RelativePathBuf, fallback: &str) -> String { - Path::new(path.as_str()) - .file_name() - .and_then(|name| name.to_str()) - .filter(|name| !name.is_empty()) - .unwrap_or(fallback) - .to_string() -} - -fn unique_file_name(dir: &Path, preferred: &str) -> String { - let sanitized = sanitize_filename(preferred); - let sanitized = if sanitized.is_empty() { - "file".to_string() - } else { - sanitized - }; - - let path = Path::new(&sanitized); - let stem = path - .file_stem() - .and_then(|value| value.to_str()) - .filter(|value| !value.is_empty()) - .unwrap_or("file") - .to_string(); - let extension = path - .extension() - .and_then(|value| value.to_str()) - .filter(|value| !value.is_empty()) - .map(|value| value.to_string()); - - let mut counter = 0; - loop { - let candidate = if counter == 0 { - sanitized.clone() - } else if let Some(extension) = &extension { - format!("{stem}-{counter}.{extension}") - } else { - format!("{stem}-{counter}") - }; - - if !dir.join(&candidate).exists() { - return candidate; - } - - counter += 1; - } -} - -fn copy_file_to_relative_path( - source_path: &Path, - target_project_path: &Path, - target_relative_path: &RelativePathBuf, -) -> Result<(), String> { - let target_path = target_relative_path.to_path(target_project_path); - - if let Some(parent) = target_path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create import directory: {e}"))?; - } - - std::fs::copy(source_path, &target_path) - .map(|_| ()) - .map_err(|e| format!("Failed to copy {}: {e}", source_path.display())) -} - -fn copy_video_meta( - source_project_path: &Path, - target_project_path: &Path, - source: &VideoMeta, - target_relative_dir: &str, - name: &str, - required: bool, -) -> Result, String> { - let Some(source_path) = source_asset_path( - source_project_path, - &source.path, - "video", - VIDEO_IMPORT_EXTENSIONS, - )? - else { - if required { - return Err(format!( - "Missing video file: {}", - source.path.to_path(source_project_path).display() - )); - } - return Ok(None); - }; - - let can_decode = probe_video_can_decode(&source_path) - .map_err(|e| format!("Cannot decode video {}: {e}", source_path.display()))?; - if !can_decode { - if required { - return Err(format!("Unsupported video file: {}", source_path.display())); - } - return Ok(None); - } - - let extension = relative_file_extension(&source.path, "mp4"); - let target_relative_path = - RelativePathBuf::from(format!("{target_relative_dir}/{name}.{extension}")); - copy_file_to_relative_path(&source_path, target_project_path, &target_relative_path)?; - - let mut copied = source.clone(); - copied.path = target_relative_path; - Ok(Some(copied)) -} - -fn copy_audio_meta( - source_project_path: &Path, - target_project_path: &Path, - source: &AudioMeta, - target_relative_dir: &str, - name: &str, -) -> Result, String> { - let Some(source_path) = source_asset_path( - source_project_path, - &source.path, - "audio", - AUDIO_IMPORT_EXTENSIONS, - )? - else { - return Ok(None); - }; - - let extension = relative_file_extension(&source.path, "ogg"); - let target_relative_path = - RelativePathBuf::from(format!("{target_relative_dir}/{name}.{extension}")); - copy_file_to_relative_path(&source_path, target_project_path, &target_relative_path)?; - - let mut copied = source.clone(); - copied.path = target_relative_path; - Ok(Some(copied)) -} - -fn copy_keyboard_path( - source_meta: &RecordingMeta, - source_segment: &MultipleSegment, - target_project_path: &Path, - target_relative_dir: &str, -) -> Result, String> { - if let Some(source_relative_path) = &source_segment.keyboard { - let file_name = - relative_file_name(source_relative_path, cap_project::KEYBOARD_EVENTS_FILE_NAME); - let Some(source_path) = source_asset_path( - &source_meta.project_path, - source_relative_path, - "keyboard events", - KEYBOARD_IMPORT_EXTENSIONS, - )? - else { - return Ok(None); - }; - - let target_relative_path = RelativePathBuf::from(format!( - "{target_relative_dir}/{}", - sanitize_filename(&file_name) - )); - copy_file_to_relative_path(&source_path, target_project_path, &target_relative_path)?; - - return Ok(Some(target_relative_path)); - }; - - let Some(display_dir) = source_segment.display.path.parent() else { - return Ok(None); - }; - - for file_name in [ - cap_project::KEYBOARD_EVENTS_FILE_NAME, - cap_project::LEGACY_KEYBOARD_EVENTS_FILE_NAME, - ] { - let source_relative_path = display_dir.join(file_name); - let Some(source_path) = source_asset_path( - &source_meta.project_path, - &source_relative_path, - "keyboard events", - KEYBOARD_IMPORT_EXTENSIONS, - )? - else { - continue; - }; - - let target_relative_path = RelativePathBuf::from(format!( - "{target_relative_dir}/{}", - sanitize_filename(file_name) - )); - copy_file_to_relative_path(&source_path, target_project_path, &target_relative_path)?; - - return Ok(Some(target_relative_path)); - } - - Ok(None) -} - -fn normalize_cursors_to_correct(cursors: &mut Cursors) -> &mut HashMap { - if let Cursors::Old(old) = cursors { - let converted = old - .iter() - .map(|(id, path)| { - ( - id.clone(), - CursorMeta { - image_path: RelativePathBuf::from(path.as_str()), - hotspot: XY::new(0.0, 0.0), - shape: None, - }, - ) - }) - .collect(); - *cursors = Cursors::Correct(converted); - } - - match cursors { - Cursors::Correct(map) => map, - Cursors::Old(_) => unreachable!(), - } -} - -fn unique_cursor_id( - cursors: &HashMap, - import_token: &str, - source_id: &str, -) -> String { - let source_id = if source_id.is_empty() { - "cursor" - } else { - source_id - }; - let base = format!("{import_token}-{source_id}"); - if !cursors.contains_key(&base) { - return base; - } - - let mut counter = 1; - loop { - let candidate = format!("{base}-{counter}"); - if !cursors.contains_key(&candidate) { - return candidate; - } - counter += 1; - } -} - -fn copy_source_cursor_images( - source_meta: &RecordingMeta, - source_cursors: &Cursors, - target_project_path: &Path, - target_cursors: &mut Cursors, - import_token: &str, -) -> Result, String> { - let target_cursor_dir = target_project_path.join("content").join("cursors"); - std::fs::create_dir_all(&target_cursor_dir) - .map_err(|e| format!("Failed to create cursor directory: {e}"))?; - - let target_cursors = normalize_cursors_to_correct(target_cursors); - let mut id_map = HashMap::new(); - - match source_cursors { - Cursors::Correct(source_map) => { - for (source_id, cursor) in source_map { - let Some(source_path) = source_asset_path( - &source_meta.project_path, - &cursor.image_path, - "cursor image", - IMAGE_IMPORT_EXTENSIONS, - )? - else { - continue; - }; - - let new_id = unique_cursor_id(target_cursors, import_token, source_id); - let source_file_name = relative_file_name(&cursor.image_path, "cursor.png"); - let target_file_name = - unique_file_name(&target_cursor_dir, &format!("{new_id}-{source_file_name}")); - let target_relative_path = - RelativePathBuf::from(format!("content/cursors/{target_file_name}")); - - copy_file_to_relative_path( - &source_path, - target_project_path, - &target_relative_path, - )?; - - target_cursors.insert( - new_id.clone(), - CursorMeta { - image_path: target_relative_path, - hotspot: cursor.hotspot, - shape: cursor.shape, - }, - ); - id_map.insert(source_id.clone(), new_id); - } - } - Cursors::Old(source_map) => { - for (source_id, source_path) in source_map { - let source_relative_path = legacy_cursor_relative_path(source_path)?; - let Some(source_path) = source_asset_path( - &source_meta.project_path, - &source_relative_path, - "cursor image", - IMAGE_IMPORT_EXTENSIONS, - )? - else { - continue; - }; - - let new_id = unique_cursor_id(target_cursors, import_token, source_id); - let source_file_name = relative_file_name(&source_relative_path, "cursor.png"); - let target_file_name = - unique_file_name(&target_cursor_dir, &format!("{new_id}-{source_file_name}")); - let target_relative_path = - RelativePathBuf::from(format!("content/cursors/{target_file_name}")); - - copy_file_to_relative_path( - &source_path, - target_project_path, - &target_relative_path, - )?; - - target_cursors.insert( - new_id.clone(), - CursorMeta { - image_path: target_relative_path, - hotspot: XY::new(0.0, 0.0), - shape: None, - }, - ); - id_map.insert(source_id.clone(), new_id); - } - } - } - - Ok(id_map) -} - -fn copy_cursor_events_path( - source_meta: &RecordingMeta, - source_relative_path: &RelativePathBuf, - target_project_path: &Path, - target_relative_dir: &str, - cursor_id_map: &HashMap, -) -> Result, String> { - let Some(source_path) = source_asset_path( - &source_meta.project_path, - source_relative_path, - "cursor events", - CURSOR_EVENTS_IMPORT_EXTENSIONS, - )? - else { - return Ok(None); - }; - - let target_relative_path = RelativePathBuf::from(format!("{target_relative_dir}/cursor.json")); - let target_path = target_relative_path.to_path(target_project_path); - if let Some(parent) = target_path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create cursor event directory: {e}"))?; - } - - if cursor_id_map.is_empty() { - std::fs::copy(&source_path, &target_path) - .map(|_| ()) - .map_err(|e| format!("Failed to copy cursor events: {e}"))?; - return Ok(Some(target_relative_path)); - } - - match CursorEvents::load_from_file(&source_path) { - Ok(mut events) => { - for event in &mut events.moves { - if let Some(new_id) = cursor_id_map.get(&event.cursor_id) { - event.cursor_id = new_id.clone(); - } - } - for event in &mut events.clicks { - if let Some(new_id) = cursor_id_map.get(&event.cursor_id) { - event.cursor_id = new_id.clone(); - } - } - - let file = std::fs::File::create(&target_path) - .map_err(|e| format!("Failed to create cursor event file: {e}"))?; - serde_json::to_writer_pretty(file, &events) - .map_err(|e| format!("Failed to write cursor event file: {e}"))?; - } - Err(_) => { - std::fs::copy(&source_path, &target_path) - .map(|_| ()) - .map_err(|e| format!("Failed to copy cursor events: {e}"))?; - } - } - - Ok(Some(target_relative_path)) -} - -fn single_segment_to_multiple(segment: &SingleSegment) -> MultipleSegment { - MultipleSegment { - display: segment.display.clone(), - camera: segment.camera.clone(), - mic: segment.audio.clone(), - system_audio: None, - cursor: segment.cursor.clone(), - keyboard: None, - display_notch: None, - } -} - -fn studio_segments_for_import(studio_meta: &StudioRecordingMeta) -> Vec { - match studio_meta { - StudioRecordingMeta::SingleSegment { segment } => { - vec![single_segment_to_multiple(segment)] - } - StudioRecordingMeta::MultipleSegments { inner } => inner.segments.clone(), - } -} - -fn source_timeline_segments_for_import( - source_meta: &RecordingMeta, - source_segments: &[MultipleSegment], -) -> Result, String> { - let source_config = ProjectConfiguration::load(&source_meta.project_path).unwrap_or_default(); - let Some(timeline) = source_config.timeline else { - return full_timeline_for_source_segments(source_meta, source_segments); - }; - - if timeline.segments.is_empty() { - return full_timeline_for_source_segments(source_meta, source_segments); - } - - let mut duration_cache = HashMap::new(); - let mut imported_segments = Vec::new(); - - for segment in timeline.segments { - let source_index = segment.recording_clip; - let Some(source_segment) = source_segments.get(source_index as usize) else { - continue; - }; - - let max_duration = if let Some(duration) = duration_cache.get(&source_index) { - *duration - } else { - let duration = get_source_video_duration_secs(source_meta, &source_segment.display)?; - duration_cache.insert(source_index, duration); - duration - }; - - if max_duration <= 0.0 { - continue; - } - - let raw_start = if segment.start.is_finite() { - segment.start - } else { - 0.0 - }; - let raw_end = if segment.end.is_finite() { - segment.end - } else { - max_duration - }; - let start = raw_start.clamp(0.0, max_duration); - let end = raw_end.clamp(start, max_duration); - if end <= start { - continue; - } - - imported_segments.push(TimelineSegment { - recording_clip: source_index, - timescale: if segment.timescale.is_finite() && segment.timescale > 0.0 { - segment.timescale - } else { - 1.0 - }, - start, - end, - name: None, - speed_audio_mode: segment.speed_audio_mode, - hide_cursor: segment.hide_cursor, - volume: segment.volume, - }); - } - - if imported_segments.is_empty() { - full_timeline_for_source_segments(source_meta, source_segments) - } else { - Ok(imported_segments) - } -} - -fn copy_source_segment( - source_meta: &RecordingMeta, - source_segment: &MultipleSegment, - target_project_path: &Path, - target_relative_dir: &str, - cursor_id_map: &HashMap, -) -> Result { - let display = copy_video_meta( - &source_meta.project_path, - target_project_path, - &source_segment.display, - target_relative_dir, - "display", - true, - )? - .ok_or_else(|| "Missing display video".to_string())?; - - let camera = source_segment - .camera - .as_ref() - .map(|camera| { - copy_video_meta( - &source_meta.project_path, - target_project_path, - camera, - target_relative_dir, - "camera", - false, - ) - }) - .transpose()? - .flatten(); - - let mic = source_segment - .mic - .as_ref() - .map(|mic| { - copy_audio_meta( - &source_meta.project_path, - target_project_path, - mic, - target_relative_dir, - "mic", - ) - }) - .transpose()? - .flatten(); - - let system_audio = source_segment - .system_audio - .as_ref() - .map(|system_audio| { - copy_audio_meta( - &source_meta.project_path, - target_project_path, - system_audio, - target_relative_dir, - "system-audio", - ) - }) - .transpose()? - .flatten(); - - let cursor = source_segment - .cursor - .as_ref() - .map(|cursor| { - copy_cursor_events_path( - source_meta, - cursor, - target_project_path, - target_relative_dir, - cursor_id_map, - ) - }) - .transpose()? - .flatten(); - - let keyboard = copy_keyboard_path( - source_meta, - source_segment, - target_project_path, - target_relative_dir, - )?; - - Ok(MultipleSegment { - display, - camera, - mic, - system_audio, - cursor, - keyboard, - display_notch: source_segment.display_notch, - }) -} - -fn get_video_stream_info( - input: &avformat::context::Input, -) -> Result<(usize, VideoInfo), ImportError> { - let stream = input - .streams() - .best(ffmpeg::media::Type::Video) - .ok_or(ImportError::NoVideoStream)?; - - let stream_index = stream.index(); - let decoder_ctx = avcodec::Context::from_parameters(stream.parameters()) - .map_err(|e| ImportError::DecoderFailed(e.to_string()))?; - let decoder = decoder_ctx - .decoder() - .video() - .map_err(|e| ImportError::DecoderFailed(e.to_string()))?; - - let rate = stream.avg_frame_rate(); - let time_base = stream.time_base(); - - let pixel_format = match decoder.format() { - ffmpeg::format::Pixel::YUV420P => Pixel::YUV420P, - ffmpeg::format::Pixel::NV12 => Pixel::NV12, - ffmpeg::format::Pixel::BGRA => Pixel::BGRA, - ffmpeg::format::Pixel::RGBA => Pixel::RGBA, - ffmpeg::format::Pixel::RGB24 => Pixel::RGB24, - ffmpeg::format::Pixel::BGR24 => Pixel::BGR24, - _ => Pixel::YUV420P, - }; - - Ok(( - stream_index, - VideoInfo { - pixel_format, - width: decoder.width(), - height: decoder.height(), - time_base: FFRational(time_base.numerator(), time_base.denominator()), - frame_rate: FFRational(rate.numerator(), rate.denominator()), - }, - )) -} - -fn get_audio_stream_info(input: &avformat::context::Input) -> Option<(usize, AudioInfo)> { - let stream = input.streams().best(ffmpeg::media::Type::Audio)?; - let stream_index = stream.index(); - - let decoder_ctx = avcodec::Context::from_parameters(stream.parameters()).ok()?; - let decoder = decoder_ctx.decoder().audio().ok()?; - - let audio_info = AudioInfo::from_decoder(&decoder).ok()?; - - Some((stream_index, audio_info)) -} - -fn reference_video_frame(frame: &ffmpeg::frame::Video) -> ffmpeg::frame::Video { - let mut referenced = ffmpeg::frame::Video::empty(); - let status = unsafe { ffmpeg::ffi::av_frame_ref(referenced.as_mut_ptr(), frame.as_ptr()) }; - - if status < 0 { - frame.clone() - } else { - referenced - } -} - -fn transcode_video( - app: &AppHandle, - source_path: &Path, - output_path: &Path, - audio_output_path: Option<&Path>, - project_path_str: &str, - project_path: &Path, -) -> Result<(u32, Option), ImportError> { - use std::time::Duration as StdDuration; - - let mut input = - avformat::input(source_path).map_err(|e| ImportError::OpenFailed(e.to_string()))?; - - let (video_stream_index, video_info) = get_video_stream_info(&input)?; - let audio_stream_info = get_audio_stream_info(&input); - - let output_width = ensure_even(video_info.width); - let output_height = ensure_even(video_info.height); - let fps = if video_info.frame_rate.1 > 0 { - ((video_info.frame_rate.0 as f64 / video_info.frame_rate.1 as f64).round() as u32) - .clamp(1, 120) - } else { - 30 - }; - - let duration = get_media_duration(source_path); - let total_frames = duration - .map(|d| (d.as_secs_f64() * fps as f64) as u64) - .unwrap_or(1000); - - let video_decoder_ctx = - avcodec::Context::from_parameters(input.stream(video_stream_index).unwrap().parameters()) - .map_err(|e| ImportError::DecoderFailed(e.to_string()))?; - let mut video_decoder = video_decoder_ctx - .decoder() - .video() - .map_err(|e| ImportError::DecoderFailed(e.to_string()))?; - - let video_time_base = input.stream(video_stream_index).unwrap().time_base(); - - let mut audio_decoder = audio_stream_info.as_ref().and_then(|(idx, _)| { - let stream = input.stream(*idx)?; - let decoder_ctx = avcodec::Context::from_parameters(stream.parameters()).ok()?; - let mut decoder = decoder_ctx.decoder().audio().ok()?; - if decoder.channel_layout().is_empty() { - decoder.set_channel_layout(ChannelLayout::default(decoder.channels() as i32)); - } - decoder.set_packet_time_base(stream.time_base()); - Some((*idx, decoder, stream.time_base())) - }); - - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent).map_err(ImportError::DirectoryFailed)?; - } - - let mut output = - avformat::output(output_path).map_err(|e| ImportError::EncoderFailed(e.to_string()))?; - - let encoder_video_info = VideoInfo { - pixel_format: Pixel::YUV420P, - width: output_width, - height: output_height, - time_base: video_info.time_base, - frame_rate: FFRational(fps as i32, 1), - }; - - let mut video_encoder = H264EncoderBuilder::new(encoder_video_info) - .with_preset(H264Preset::Medium) - .with_output_size(output_width, output_height) - .map_err(|e| ImportError::EncoderFailed(e.to_string()))? - .build(&mut output) - .map_err(|e| ImportError::EncoderFailed(e.to_string()))?; - - let mut audio_output: Option = None; - let mut audio_encoder: Option> = None; - let sample_rate = if let Some((_, audio_info)) = &audio_stream_info { - if let Some(audio_path) = audio_output_path { - let mut audio_out = avformat::output(audio_path).map_err(|e| { - ImportError::EncoderFailed(format!("Failed to create audio output: {e}")) - })?; - - audio_encoder = Some(Box::new( - OpusEncoder::init(*audio_info, &mut audio_out) - .map_err(|e| ImportError::EncoderFailed(e.to_string()))?, - )); - audio_out.write_header().map_err(|e| { - ImportError::EncoderFailed(format!("Failed to write audio header: {e}")) - })?; - audio_output = Some(audio_out); - } - Some(audio_info.sample_rate) - } else { - None - }; - - output - .write_header() - .map_err(|e| ImportError::EncoderFailed(format!("Failed to write header: {e}")))?; - - let mut video_frame = ffmpeg::frame::Video::empty(); - let mut audio_frame = ffmpeg::frame::Audio::empty(); - let mut frames_processed = 0u64; - let mut last_progress = 0.0; - - let mut scaler: Option = None; - - for (stream, packet) in input.packets() { - let stream_index = stream.index(); - - if stream_index == video_stream_index { - video_decoder.send_packet(&packet)?; - - while video_decoder.receive_frame(&mut video_frame).is_ok() { - let timestamp = video_frame.pts().unwrap_or(0); - let time_secs = timestamp as f64 * video_time_base.numerator() as f64 - / video_time_base.denominator().max(1) as f64; - let duration = StdDuration::from_secs_f64(time_secs.max(0.0)); - - let frame_to_encode = if video_frame.format() != ffmpeg::format::Pixel::YUV420P - || video_frame.width() != output_width - || video_frame.height() != output_height - { - if scaler.is_none() { - scaler = Some( - ffmpeg::software::scaling::Context::get( - video_frame.format(), - video_frame.width(), - video_frame.height(), - ffmpeg::format::Pixel::YUV420P, - output_width, - output_height, - ffmpeg::software::scaling::Flags::BILINEAR, - ) - .map_err(|e| { - ImportError::TranscodeFailed(format!( - "Failed to create scaler: {e}" - )) - })?, - ); - } - let scaler = scaler.as_mut().unwrap(); - - let mut scaled_frame = ffmpeg::frame::Video::empty(); - scaled_frame.set_format(ffmpeg::format::Pixel::YUV420P); - scaled_frame.set_width(output_width); - scaled_frame.set_height(output_height); - let ret = - unsafe { ffmpeg::ffi::av_frame_get_buffer(scaled_frame.as_mut_ptr(), 0) }; - if ret < 0 { - return Err(ImportError::TranscodeFailed( - "Failed to allocate frame buffer".to_string(), - )); - } - - scaler.run(&video_frame, &mut scaled_frame)?; - scaled_frame.set_pts(video_frame.pts()); - scaled_frame - } else { - reference_video_frame(&video_frame) - }; - - video_encoder - .queue_frame(frame_to_encode, duration, &mut output) - .map_err(|e| ImportError::TranscodeFailed(e.to_string()))?; - - frames_processed += 1; - - let progress = (frames_processed as f64 / total_frames as f64).min(0.99); - if progress - last_progress >= 0.01 { - last_progress = progress; - - if !check_project_exists(project_path) { - info!("Import cancelled: project directory was deleted"); - return Err(ImportError::TranscodeFailed("Import cancelled".to_string())); - } - - emit_progress( - app, - project_path_str, - ImportStage::Converting, - progress, - &format!("Converting video... {}%", (progress * 100.0) as u32), - ); - } - } - } else if let Some((audio_idx, decoder, _)) = audio_decoder.as_mut() - && stream_index == *audio_idx - && let (Some(encoder), Some(audio_out)) = - (audio_encoder.as_mut(), audio_output.as_mut()) - { - decoder.send_packet(&packet)?; - - while decoder.receive_frame(&mut audio_frame).is_ok() { - encoder.send_frame(audio_frame.clone(), audio_out); - } - } - } - - video_decoder.send_eof()?; - while video_decoder.receive_frame(&mut video_frame).is_ok() { - let timestamp = video_frame.pts().unwrap_or(0); - let time_secs = timestamp as f64 * video_time_base.numerator() as f64 - / video_time_base.denominator().max(1) as f64; - let duration = StdDuration::from_secs_f64(time_secs.max(0.0)); - - let frame_to_encode = if video_frame.format() != ffmpeg::format::Pixel::YUV420P - || video_frame.width() != output_width - || video_frame.height() != output_height - { - if let Some(scaler) = &mut scaler { - let mut scaled_frame = ffmpeg::frame::Video::empty(); - scaled_frame.set_format(ffmpeg::format::Pixel::YUV420P); - scaled_frame.set_width(output_width); - scaled_frame.set_height(output_height); - let ret = unsafe { ffmpeg::ffi::av_frame_get_buffer(scaled_frame.as_mut_ptr(), 0) }; - if ret < 0 { - return Err(ImportError::TranscodeFailed( - "Failed to allocate frame buffer".to_string(), - )); - } - scaler.run(&video_frame, &mut scaled_frame)?; - scaled_frame.set_pts(video_frame.pts()); - scaled_frame - } else { - reference_video_frame(&video_frame) - } - } else { - reference_video_frame(&video_frame) - }; - - video_encoder - .queue_frame(frame_to_encode, duration, &mut output) - .map_err(|e| ImportError::TranscodeFailed(e.to_string()))?; - } - - if let Some((_, decoder, _)) = audio_decoder.as_mut() { - decoder.send_eof()?; - while decoder.receive_frame(&mut audio_frame).is_ok() { - if let (Some(encoder), Some(audio_out)) = - (audio_encoder.as_mut(), audio_output.as_mut()) - { - encoder.send_frame(audio_frame.clone(), audio_out); - } - } - } - - video_encoder - .flush(&mut output) - .map_err(|e| ImportError::TranscodeFailed(format!("Failed to flush video: {e}")))?; - - if let (Some(encoder), Some(audio_out)) = (&mut audio_encoder, &mut audio_output) { - encoder - .flush(audio_out) - .map_err(|e| ImportError::TranscodeFailed(format!("Failed to flush audio: {e}")))?; - audio_out.write_trailer().map_err(|e| { - ImportError::TranscodeFailed(format!("Failed to write audio trailer: {e}")) - })?; - } - - output - .write_trailer() - .map_err(|e| ImportError::TranscodeFailed(format!("Failed to write trailer: {e}")))?; - - drop(output); - - if let Ok(file) = std::fs::File::open(output_path) { - let _ = file.sync_all(); - } - if let Some(audio_path) = audio_output_path - && let Ok(file) = std::fs::File::open(audio_path) - { - let _ = file.sync_all(); - } - - Ok((fps, sample_rate)) -} - -#[tauri::command] -#[specta::specta] -pub async fn start_video_import(app: AppHandle, source_path: PathBuf) -> Result { - info!("Starting video import from: {:?}", source_path); - - let recordings_dir = crate::general_settings::GeneralSettingsStore::recordings_dir(&app); - - let project_name = generate_project_name(&source_path); - let sanitized_name = sanitize_filename(&project_name); - let project_dir_name = format!("{sanitized_name}.cap"); - - let mut project_path = recordings_dir.join(&project_dir_name); - let mut counter = 1; - while project_path.exists() { - let new_name = format!("{sanitized_name} ({counter}).cap"); - project_path = recordings_dir.join(new_name); - counter += 1; - } - - let project_path_str = project_path.to_string_lossy().to_string(); - - emit_progress( - &app, - &project_path_str, - ImportStage::Probing, - 0.0, - "Analyzing video file...", - ); - - let can_decode = - probe_video_can_decode(&source_path).map_err(|e| format!("Cannot decode video: {e}"))?; - - if !can_decode { - emit_progress( - &app, - &project_path_str, - ImportStage::Failed, - 0.0, - "Video format not supported", - ); - return Err("Video format not supported or file is corrupted".to_string()); - } - - std::fs::create_dir_all(&project_path).map_err(|e| e.to_string())?; - - let segment_dir = project_path - .join("content") - .join("segments") - .join("segment-0"); - std::fs::create_dir_all(&segment_dir).map_err(|e| e.to_string())?; - - let output_video_path = segment_dir.join("display.mp4"); - let output_audio_path = segment_dir.join("audio.ogg"); - - let initial_meta = RecordingMeta { - platform: Some(Platform::default()), - project_path: project_path.clone(), - pretty_name: project_name.clone(), - sharing: None, - inner: RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::MultipleSegments { - inner: MultipleSegments { - segments: vec![MultipleSegment { - display: VideoMeta { - path: RelativePathBuf::from("content/segments/segment-0/display.mp4"), - fps: 30, - start_time: Some(0.0), - device_id: None, - }, - camera: None, - mic: None, - system_audio: None, - cursor: None, - keyboard: None, - display_notch: None, - }], - cursors: Cursors::default(), - status: Some(StudioRecordingStatus::InProgress), - }, - })), - upload: None, - }; - - initial_meta - .save_for_project() - .map_err(|e| format!("Failed to save initial metadata: {e:?}"))?; - - emit_progress( - &app, - &project_path_str, - ImportStage::Converting, - 0.0, - "Starting conversion...", - ); - - let return_path = project_path.clone(); - - tokio::spawn(async move { - let app_clone = app.clone(); - let project_path_str_clone = project_path_str.clone(); - let source_path_clone = source_path.clone(); - let output_path_clone = output_video_path.clone(); - let audio_path_clone = output_audio_path.clone(); - let project_path_clone = project_path.clone(); - - if !check_project_exists(&project_path) { - info!("Import aborted before start: project directory missing"); - return; - } - - let result = tokio::task::spawn_blocking(move || { - transcode_video( - &app_clone, - &source_path_clone, - &output_path_clone, - Some(&audio_path_clone), - &project_path_str_clone, - &project_path_clone, - ) - }) - .await; - - match result { - Ok(Ok((fps, sample_rate))) => { - emit_progress( - &app, - &project_path_str, - ImportStage::Finalizing, - 0.95, - "Creating project metadata...", - ); - - let audio_file_size = std::fs::metadata(&output_audio_path) - .map(|m| m.len()) - .unwrap_or(0); - const MIN_VALID_AUDIO_SIZE: u64 = 1000; - let system_audio = - if sample_rate.is_some() && audio_file_size > MIN_VALID_AUDIO_SIZE { - Some(AudioMeta { - path: RelativePathBuf::from("content/segments/segment-0/audio.ogg"), - start_time: Some(0.0), - device_id: None, - gap_summary: None, - digital_silence_ms: None, - }) - } else { - None - }; - - let meta = RecordingMeta { - platform: Some(Platform::default()), - project_path: project_path.clone(), - pretty_name: project_name, - sharing: None, - inner: RecordingMetaInner::Studio(Box::new( - StudioRecordingMeta::MultipleSegments { - inner: MultipleSegments { - segments: vec![MultipleSegment { - display: VideoMeta { - path: RelativePathBuf::from( - "content/segments/segment-0/display.mp4", - ), - fps, - start_time: Some(0.0), - device_id: None, - }, - camera: None, - mic: None, - system_audio, - cursor: None, - keyboard: None, - display_notch: None, - }], - cursors: Cursors::default(), - status: Some(StudioRecordingStatus::Complete), - }, - }, - )), - upload: None, - }; - - if let Err(e) = meta.save_for_project() { - error!("Failed to save metadata: {:?}", e); - emit_progress( - &app, - &project_path_str, - ImportStage::Failed, - 0.0, - &format!("Failed to save metadata: {e:?}"), - ); - return; - } - - let screenshots_dir = project_path.join("screenshots"); - if let Err(e) = std::fs::create_dir_all(&screenshots_dir) { - error!("Failed to create screenshots directory: {:?}", e); - } else { - let display_screenshot = screenshots_dir.join("display.jpg"); - let video_path = output_video_path.clone(); - tokio::spawn(async move { - if let Err(e) = - create_screenshot(video_path, display_screenshot, None).await - { - error!("Failed to create thumbnail for imported video: {}", e); - } - }); - } - - emit_progress( - &app, - &project_path_str, - ImportStage::Complete, - 1.0, - "Import complete!", - ); - - info!("Video import complete: {:?}", project_path); - } - Ok(Err(e)) => { - error!("Transcoding failed: {}", e); - emit_progress( - &app, - &project_path_str, - ImportStage::Failed, - 0.0, - &e.to_string(), - ); - } - Err(e) => { - error!("Transcoding task panicked: {}", e); - emit_progress( - &app, - &project_path_str, - ImportStage::Failed, - 0.0, - &format!("Transcoding task failed: {e}"), - ); - } - } - }); - - Ok(return_path) -} - -async fn append_mp4_to_editor_project( - app: AppHandle, - target_project_path: PathBuf, - source_path: PathBuf, -) -> Result { - if !is_mp4_import_path(&source_path) { - return Err("Select an MP4 video file to import".to_string()); - } - - let mut target_meta = RecordingMeta::load_for_project(&target_project_path) - .map_err(|e| format!("Failed to load target project metadata: {e}"))?; - let mut config = ProjectConfiguration::load(&target_project_path).unwrap_or_default(); - let existing_segments = { - let inner = ensure_multiple_segments(&mut target_meta)?; - inner.status = Some(StudioRecordingStatus::Complete); - inner.segments.clone() - }; - ensure_project_timeline(&mut config, &target_project_path, &existing_segments)?; - - let new_index = existing_segments.len() as u32; - let (_, target_relative_dir) = unique_segment_dir(&target_project_path, new_index) - .map_err(|e| format!("Failed to create imported segment directory: {e}"))?; - - let output_video_relative_path = - RelativePathBuf::from(format!("{target_relative_dir}/display.mp4")); - let output_audio_relative_path = - RelativePathBuf::from(format!("{target_relative_dir}/audio.ogg")); - let output_video_path = output_video_relative_path.to_path(&target_project_path); - let output_audio_path = output_audio_relative_path.to_path(&target_project_path); - let project_path_str = target_project_path.to_string_lossy().to_string(); - - emit_progress( - &app, - &project_path_str, - ImportStage::Probing, - 0.0, - "Analyzing video file...", - ); - - let can_decode = - probe_video_can_decode(&source_path).map_err(|e| format!("Cannot decode video: {e}"))?; - if !can_decode { - return Err("Video format not supported or file is corrupted".to_string()); - } - - emit_progress( - &app, - &project_path_str, - ImportStage::Converting, - 0.0, - "Starting conversion...", - ); - - let app_for_transcode = app.clone(); - let source_path_for_transcode = source_path.clone(); - let output_video_path_for_transcode = output_video_path.clone(); - let output_audio_path_for_transcode = output_audio_path.clone(); - let project_path_str_for_transcode = project_path_str.clone(); - let target_project_path_for_transcode = target_project_path.clone(); - - let (fps, sample_rate) = tokio::task::spawn_blocking(move || { - transcode_video( - &app_for_transcode, - &source_path_for_transcode, - &output_video_path_for_transcode, - Some(&output_audio_path_for_transcode), - &project_path_str_for_transcode, - &target_project_path_for_transcode, - ) - }) - .await - .map_err(|e| format!("Video import task failed: {e}"))? - .map_err(|e| e.to_string())?; - - let duration = get_video_duration_secs(&output_video_path)?; - let audio_file_size = std::fs::metadata(&output_audio_path) - .map(|metadata| metadata.len()) - .unwrap_or(0); - const MIN_VALID_AUDIO_SIZE: u64 = 1000; - let system_audio = if sample_rate.is_some() && audio_file_size > MIN_VALID_AUDIO_SIZE { - Some(AudioMeta { - path: output_audio_relative_path, - start_time: Some(0.0), - device_id: None, - gap_summary: None, - digital_silence_ms: None, - }) - } else { - None - }; - - let imported_segment = MultipleSegment { - display: VideoMeta { - path: output_video_relative_path, - fps, - start_time: Some(0.0), - device_id: None, - }, - camera: None, - mic: None, - system_audio, - cursor: None, - keyboard: None, - display_notch: None, - }; - - { - let inner = ensure_multiple_segments(&mut target_meta)?; - inner.status = Some(StudioRecordingStatus::Complete); - inner.segments.push(imported_segment.clone()); - } - - ensure_project_timeline(&mut config, &target_project_path, &existing_segments)? - .segments - .push(TimelineSegment { - recording_clip: new_index, - timescale: 1.0, - start: 0.0, - end: duration, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }); - add_clip_configs( - &mut config, - new_index, - std::slice::from_ref(&imported_segment), - ); - - target_meta - .save_for_project() - .map_err(|e| format!("Failed to save project metadata: {e:?}"))?; - config - .write(&target_project_path) - .map_err(|e| format!("Failed to save project config: {e}"))?; - - emit_progress( - &app, - &project_path_str, - ImportStage::Complete, - 1.0, - "Import complete!", - ); - - Ok(1) -} - -async fn append_cap_project_to_editor_project( - app: AppHandle, - target_project_path: PathBuf, - source_project_path: PathBuf, -) -> Result { - let source_meta = RecordingMeta::load_for_project(&source_project_path) - .map_err(|e| format!("Failed to load source project metadata: {e}"))?; - - let RecordingMetaInner::Studio(source_studio_meta) = &source_meta.inner else { - return match &source_meta.inner { - RecordingMetaInner::Instant(InstantRecordingMeta::Complete { .. }) => { - append_mp4_to_editor_project(app, target_project_path, source_meta.output_path()) - .await - } - RecordingMetaInner::Instant(InstantRecordingMeta::InProgress { .. }) => { - Err("Source Cap project is still recording".to_string()) - } - RecordingMetaInner::Instant(InstantRecordingMeta::Failed { error }) => { - Err(format!("Source Cap project failed: {error}")) - } - RecordingMetaInner::Studio(_) => unreachable!(), - }; - }; - - let source_segments = studio_segments_for_import(source_studio_meta); - if source_segments.is_empty() { - return Err("Source Cap project has no recording segments".to_string()); - } - - let source_timeline = source_timeline_segments_for_import(&source_meta, &source_segments)?; - let source_cursors = match source_studio_meta.as_ref() { - StudioRecordingMeta::MultipleSegments { inner } => Some(&inner.cursors), - StudioRecordingMeta::SingleSegment { .. } => None, - }; - - let mut target_meta = RecordingMeta::load_for_project(&target_project_path) - .map_err(|e| format!("Failed to load target project metadata: {e}"))?; - let mut config = ProjectConfiguration::load(&target_project_path).unwrap_or_default(); - let existing_segments = { - let inner = ensure_multiple_segments(&mut target_meta)?; - inner.status = Some(StudioRecordingStatus::Complete); - inner.segments.clone() - }; - ensure_project_timeline(&mut config, &target_project_path, &existing_segments)?; - - let (base_index, copied_segments, source_to_target_index) = { - let inner = ensure_multiple_segments(&mut target_meta)?; - inner.status = Some(StudioRecordingStatus::Complete); - let base_index = inner.segments.len() as u32; - let import_token = format!("import-{}", uuid::Uuid::new_v4().simple()); - let cursor_id_map = if let Some(source_cursors) = source_cursors { - copy_source_cursor_images( - &source_meta, - source_cursors, - &target_project_path, - &mut inner.cursors, - &import_token, - )? - } else { - HashMap::new() - }; - - let mut copied_segments = Vec::new(); - let mut source_to_target_index = HashMap::new(); - - for (source_index, source_segment) in source_segments.iter().enumerate() { - let target_index = base_index + copied_segments.len() as u32; - let (_, target_relative_dir) = - unique_segment_dir(&target_project_path, target_index) - .map_err(|e| format!("Failed to create imported segment directory: {e}"))?; - let copied_segment = copy_source_segment( - &source_meta, - source_segment, - &target_project_path, - &target_relative_dir, - &cursor_id_map, - )?; - - inner.segments.push(copied_segment.clone()); - copied_segments.push(copied_segment); - source_to_target_index.insert(source_index as u32, target_index); - } - - (base_index, copied_segments, source_to_target_index) - }; - - if copied_segments.is_empty() { - return Err("Source Cap project has no importable recording segments".to_string()); - } - - { - let timeline = - ensure_project_timeline(&mut config, &target_project_path, &existing_segments)?; - for source_segment in source_timeline { - let Some(target_index) = source_to_target_index.get(&source_segment.recording_clip) - else { - continue; - }; - - timeline.segments.push(TimelineSegment { - recording_clip: *target_index, - timescale: source_segment.timescale, - start: source_segment.start, - end: source_segment.end, - name: None, - speed_audio_mode: source_segment.speed_audio_mode, - hide_cursor: source_segment.hide_cursor, - volume: source_segment.volume, - }); - } - } - - add_clip_configs(&mut config, base_index, &copied_segments); - - target_meta - .save_for_project() - .map_err(|e| format!("Failed to save project metadata: {e:?}"))?; - config - .write(&target_project_path) - .map_err(|e| format!("Failed to save project config: {e}"))?; - - Ok(copied_segments.len()) -} - -#[tauri::command] -#[specta::specta] -pub async fn add_existing_recording_to_editor( - window: Window, - source_path: PathBuf, -) -> Result { - let target_project_path = editor_project_path_from_window(&window)?; - - if same_project_path(&target_project_path, &source_path) { - return Err("Cannot import a recording into itself".to_string()); - } - - let app = window.app_handle().clone(); - let imported_count = if is_mp4_import_path(&source_path) { - append_mp4_to_editor_project(app, target_project_path, source_path).await? - } else if is_cap_project_path(&source_path) { - crate::wait_for_recording_ready(&app, &source_path).await?; - append_cap_project_to_editor_project(app, target_project_path, source_path).await? - } else { - return Err("Select an MP4 file or a Cap project folder".to_string()); - }; - let imported_count = - u32::try_from(imported_count).map_err(|_| "Too many recordings imported".to_string())?; - - EditorInstances::remove(window).await; - - Ok(imported_count) -} - -#[tauri::command] -#[specta::specta] -pub async fn start_image_import(app: AppHandle, source_path: PathBuf) -> Result { - info!("Starting image import from: {:?}", source_path); - - if !source_path.is_file() { - return Err("Image file does not exist".to_string()); - } - - let source_path_for_decode = source_path.clone(); - let (image_width, image_height, image_data) = - tokio::task::spawn_blocking(move || -> Result<(u32, u32, Vec), String> { - let image = image::ImageReader::open(&source_path_for_decode) - .map_err(|e| format!("Failed to open image: {e}"))? - .with_guessed_format() - .map_err(|e| format!("Failed to detect image format: {e}"))? - .decode() - .map_err(|e| format!("Failed to decode image: {e}"))?; - - let width = image.width(); - let height = image.height(); - - if width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION { - return Err(format!("Image dimensions exceed maximum: {width}x{height}")); - } - - if width - .checked_mul(height) - .and_then(|p| p.checked_mul(4)) - .is_none() - { - return Err(format!("Image dimensions overflow: {width}x{height}")); - } - - let rgba = image.to_rgba8(); - - Ok((width, height, rgba.into_raw())) - }) - .await - .map_err(|e| format!("Failed to import image: {e}"))??; - - let screenshots_dir = app - .path() - .app_data_dir() - .map_err(|e| e.to_string())? - .join("screenshots"); - std::fs::create_dir_all(&screenshots_dir) - .map_err(|e| format!("Failed to create screenshots directory: {e}"))?; - - let project_name = generate_image_project_name(&source_path); - let filename = project_name.replace(":", "."); - let filename = format!("{}.cap", sanitize_filename::sanitize(&filename)); - let project_path = screenshots_dir.join(cap_utils::ensure_unique_filename( - &filename, - &screenshots_dir, - )?); - std::fs::create_dir_all(&project_path) - .map_err(|e| format!("Failed to create screenshot project directory: {e}"))?; - - let image_filename = "original.png"; - let image_path = project_path.join(image_filename); - let image_path_for_write = image_path.clone(); - - tokio::task::spawn_blocking(move || -> Result<(), String> { - let file = std::fs::File::create(&image_path_for_write) - .map_err(|e| format!("Failed to create imported image file: {e}"))?; - let encoder = image::codecs::png::PngEncoder::new_with_quality( - std::io::BufWriter::new(file), - image::codecs::png::CompressionType::Default, - image::codecs::png::FilterType::Adaptive, - ); - - ImageEncoder::write_image( - encoder, - &image_data, - image_width, - image_height, - image::ColorType::Rgba8.into(), - ) - .map_err(|e| format!("Failed to encode imported image: {e}")) - }) - .await - .map_err(|e| format!("Failed to write imported image: {e}"))??; - - let video_meta = VideoMeta { - path: RelativePathBuf::from(image_filename), - fps: 0, - start_time: Some(0.0), - device_id: None, - }; - - let segment = SingleSegment { - display: video_meta, - camera: None, - audio: None, - cursor: None, - }; - - let meta = RecordingMeta { - platform: Some(Platform::default()), - project_path: project_path.clone(), - pretty_name: project_name, - sharing: None, - inner: RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::SingleSegment { segment })), - upload: None, - }; - - meta.save_for_project() - .map_err(|e| format!("Failed to save screenshot metadata: {e:?}"))?; - - ProjectConfiguration::default() - .write(&project_path) - .map_err(|e| format!("Failed to save screenshot project config: {e}"))?; - - let _ = crate::NewScreenshotAdded { - path: image_path.clone(), - } - .emit(&app); - - Ok(image_path) -} - -#[tauri::command] -#[specta::specta] -pub async fn check_import_ready(project_path: PathBuf) -> Result { - debug!("check_import_ready called for: {:?}", project_path); - - let meta = match RecordingMeta::load_for_project(&project_path) { - Ok(m) => m, - Err(e) => { - debug!("check_import_ready: meta load failed: {:?}", e); - return Ok(false); - } - }; - - let is_complete = match &meta.inner { - RecordingMetaInner::Studio(studio) => { - matches!(studio.status(), StudioRecordingStatus::Complete) - } - RecordingMetaInner::Instant(instant) => { - matches!(instant, InstantRecordingMeta::Complete { .. }) - } - }; - - if !is_complete { - debug!("check_import_ready: not complete yet"); - return Ok(false); - } - - let video_path = project_path - .join("content") - .join("segments") - .join("segment-0") - .join("display.mp4"); - - if !video_path.exists() { - debug!( - "check_import_ready: video path doesn't exist: {:?}", - video_path - ); - return Ok(false); - } - - let can_decode = probe_video_can_decode(&video_path); - debug!( - "check_import_ready: probe_video_can_decode result: {:?}", - can_decode - ); - if !can_decode.unwrap_or(false) { - return Ok(false); - } - - let duration = get_media_duration(&video_path); - debug!( - "check_import_ready: get_media_duration result: {:?}", - duration - ); - if duration.is_none() { - return Ok(false); - } - - debug!("check_import_ready: all checks passed, returning true"); - Ok(true) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn imported_video_frames_share_reference_counted_pixel_storage() { - let mut source = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV420P, 16, 12); - source.set_pts(Some(417)); - source.data_mut(0)[..4].copy_from_slice(&[11, 22, 33, 44]); - let original = source.data(0).as_ptr(); - - let referenced = reference_video_frame(&source); - - assert_eq!(referenced.data(0).as_ptr(), original); - assert_eq!(referenced.format(), ffmpeg::format::Pixel::YUV420P); - assert_eq!((referenced.width(), referenced.height()), (16, 12)); - assert_eq!(referenced.pts(), Some(417)); - let buffer = unsafe { (*source.as_ptr()).buf[0] }; - assert!(!buffer.is_null()); - assert_eq!(unsafe { ffmpeg::ffi::av_buffer_get_ref_count(buffer) }, 2); - - drop(source); - - assert_eq!(&referenced.data(0)[..4], &[11, 22, 33, 44]); - let retained = unsafe { (*referenced.as_ptr()).buf[0] }; - assert_eq!(unsafe { ffmpeg::ffi::av_buffer_get_ref_count(retained) }, 1); - } - - #[test] - fn imported_video_frame_references_preserve_non_yuv_formats() { - let mut source = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, 12, 8); - source.set_pts(Some(93)); - - let referenced = reference_video_frame(&source); - - assert_eq!(referenced.data(0).as_ptr(), source.data(0).as_ptr()); - assert_eq!(referenced.format(), ffmpeg::format::Pixel::BGRA); - assert_eq!((referenced.width(), referenced.height()), (12, 8)); - assert_eq!(referenced.pts(), Some(93)); - } - - #[test] - fn source_asset_path_allows_file_inside_source_project() { - let source_project = tempfile::tempdir().unwrap(); - let source_relative_path = RelativePathBuf::from("content/segments/segment-0/display.mp4"); - let source_path = source_relative_path.to_path(source_project.path()); - std::fs::create_dir_all(source_path.parent().unwrap()).unwrap(); - std::fs::write(&source_path, b"video").unwrap(); - - let resolved = source_asset_path( - source_project.path(), - &source_relative_path, - "video", - VIDEO_IMPORT_EXTENSIONS, - ) - .unwrap() - .unwrap(); - - assert_eq!(resolved, source_path.canonicalize().unwrap()); - } - - #[test] - fn source_asset_path_allows_backslash_separators() { - let source_project = tempfile::tempdir().unwrap(); - let source_relative_path = - RelativePathBuf::from("content\\segments\\segment-0\\display.mp4"); - let source_path = RelativePathBuf::from("content/segments/segment-0/display.mp4") - .to_path(source_project.path()); - std::fs::create_dir_all(source_path.parent().unwrap()).unwrap(); - std::fs::write(&source_path, b"video").unwrap(); - - let resolved = source_asset_path( - source_project.path(), - &source_relative_path, - "video", - VIDEO_IMPORT_EXTENSIONS, - ) - .unwrap() - .unwrap(); - - assert_eq!(resolved, source_path.canonicalize().unwrap()); - } - - #[test] - fn source_asset_path_rejects_parent_traversal() { - let source_project = tempfile::tempdir().unwrap(); - let source_relative_path = RelativePathBuf::from("../secret.mp4"); - - let error = source_asset_path( - source_project.path(), - &source_relative_path, - "video", - VIDEO_IMPORT_EXTENSIONS, - ) - .unwrap_err(); - - assert!(error.contains("Invalid video path")); - } - - #[test] - fn source_asset_path_rejects_absolute_path() { - let source_project = tempfile::tempdir().unwrap(); - let source_relative_path = RelativePathBuf::from("/tmp/secret.mp4"); - - let error = source_asset_path( - source_project.path(), - &source_relative_path, - "video", - VIDEO_IMPORT_EXTENSIONS, - ) - .unwrap_err(); - - assert!(error.contains("Invalid video path")); - } - - #[test] - fn legacy_cursor_relative_path_rejects_windows_absolute_path() { - let error = legacy_cursor_relative_path("C:\\Users\\me\\cursor.png").unwrap_err(); - - assert!(error.contains("Invalid cursor image path")); - } - - #[test] - fn source_asset_path_rejects_unsupported_extension() { - let source_project = tempfile::tempdir().unwrap(); - let source_relative_path = RelativePathBuf::from("content/segments/segment-0/display.txt"); - - let error = source_asset_path( - source_project.path(), - &source_relative_path, - "video", - VIDEO_IMPORT_EXTENSIONS, - ) - .unwrap_err(); - - assert!(error.contains("Unsupported video file type")); - } - - #[cfg(unix)] - #[test] - fn source_asset_path_rejects_symlink_escape() { - let source_project = tempfile::tempdir().unwrap(); - let external_dir = tempfile::tempdir().unwrap(); - let external_file = external_dir.path().join("cursor.png"); - std::fs::write(&external_file, b"cursor").unwrap(); - - let source_relative_path = RelativePathBuf::from("content/cursors/cursor.png"); - let source_path = source_relative_path.to_path(source_project.path()); - std::fs::create_dir_all(source_path.parent().unwrap()).unwrap(); - std::os::unix::fs::symlink(&external_file, &source_path).unwrap(); - - let error = source_asset_path( - source_project.path(), - &source_relative_path, - "cursor image", - IMAGE_IMPORT_EXTENSIONS, - ) - .unwrap_err(); - - assert!(error.contains("cursor image path escapes source project")); - } -} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs deleted file mode 100644 index 03eeb3a1776..00000000000 --- a/apps/desktop/src-tauri/src/lib.rs +++ /dev/null @@ -1,11117 +0,0 @@ -#![recursion_limit = "256"] - -mod api; -mod audio; -mod audio_library; -mod audio_meter; -mod auth; -mod automation; -mod camera; -mod camera_legacy; -#[cfg(target_os = "macos")] -mod camera_native; -mod captions; -mod clean_capture; -mod cli; -mod clip_thumbnails; -mod crash_sentinel; -mod deeplink_actions; -mod diagnostics; -mod editor_preparing; -mod editor_recording; -mod editor_window; -mod exit_shutdown; -mod export; -mod fake_window; -mod flags; -pub mod frame_ws; -mod general_settings; -mod gpui_app; -mod hotkeys; -mod http_client; -mod import; -pub mod linux_instant_camera; -mod logging; -#[cfg(target_os = "macos")] -mod macos_save_panel; -mod main_window_geometry; -mod notifications; -mod panel_manager; -mod permissions; -#[cfg(debug_assertions)] -mod picker_benchmark; -mod platform; -mod power_observer; -mod preparing_finalization; -mod presets; -mod recording; -mod recording_settings; -mod recording_telemetry; -mod recordings_locations; -mod recovery; -mod render_frame_event; -mod screenshot_editor; -mod startup; -#[cfg(debug_assertions)] -mod stop_editor_benchmark; -mod target_select_overlay; -mod telemetry; -mod thumbnails; -mod tray; -mod update_project_names; -mod updates; -mod upload; -pub mod web_api; -mod window_exclusion; -mod window_position_persistence; -mod windows; - -use audio::AppSounds; -use auth::{AuthStore, Plan}; -use camera::{CameraPreviewManager, CameraPreviewSender, CameraPreviewState}; -use cap_editor::{EditorInstance, EditorState}; -use cap_project::{ - InstantRecordingMeta, ProjectConfiguration, RecordingMeta, RecordingMetaInner, SharingMeta, - StudioRecordingMeta, StudioRecordingStatus, UploadMeta, VideoUploadInfo, XY, ZoomSegment, -}; -use cap_recording::{ - RecordingMode, - feeds::{ - self, - camera::{CameraFeed, DeviceOrModelID}, - microphone::{self, MicrophoneFeed}, - }, - sources::screen_capture::ScreenCaptureTarget, -}; -use cap_rendering::ProjectRecordingsMeta; -use clipboard_rs::Clipboard; -#[cfg(not(target_os = "linux"))] -use clipboard_rs::ClipboardContext; -#[cfg(target_os = "linux")] -use clipboard_rs::ClipboardContext as PlatformClipboardContext; -use clipboard_rs::common::RustImage; -use cpal::StreamError; -use editor_window::{EditorInstances, PendingEditorInstances, WindowEditorInstance}; -use ffmpeg::ffi::AV_TIME_BASE; -use general_settings::GeneralSettingsStore; -use kameo::{Actor, actor::ActorRef}; -use notifications::NotificationType; -use recording::{InProgressRecording, RecordingEvent, RecordingInputKind}; -use scap_targets::{Display, DisplayId, WindowId, bounds::LogicalBounds}; -use screenshot_editor::{ - PendingScreenshotEditorInstances, ScreenshotEditorInstances, WindowScreenshotEditorInstance, - create_screenshot_editor_instance, prewarm_screenshot_background, recognize_screenshot_text, - render_screenshot_for_export, render_screenshot_png, render_screenshot_project_for_export, - update_screenshot_config, -}; - -mod gpu_context; -mod gpu_device_health; -pub use gpu_context::{PendingScreenshot, PendingScreenshots}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use specta::Type; -use std::{ - collections::{BTreeMap, HashSet}, - future::Future, - marker::PhantomData, - path::{Path, PathBuf}, - process::Command, - str::FromStr, - sync::{ - Arc, - atomic::{AtomicBool, AtomicU64, Ordering}, - }, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, -}; -use tauri::Listener; -use tauri::{AppHandle, Emitter, Manager, State, Window, WindowEvent, ipc::Channel}; -#[cfg(target_os = "linux")] -use tauri_plugin_clipboard_manager::ClipboardExt; -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}; -use web_api::AuthedApiError; -use web_api::ManagerExt as WebManagerExt; -#[cfg(target_os = "macos")] -use windows::hide_overlay; -use windows::{ - CapWindowId, EditorRecordingTarget, EditorWindowIds, ScreenshotEditorWindowIds, ShowCapWindow, - set_window_transparent, show_overlay, -}; - -use crate::recording_settings::{RecordingSettingsStore, RecordingTargetMode}; -use crate::{recording::start_recording, upload::build_video_meta}; -use exit_shutdown::{ - AppExitAction, ExitBlocked, ExitRequestDecision, app_exit_action, collect_device_inventory, - handle_exit_requested, prepare_then_begin_exit, recording_start_allowed, run_while_active, - with_idle_recording_state, -}; -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 FinalizationResult = Option>; -const MAX_SETTLED_FINALIZATIONS: usize = 32; - -#[derive(Default)] -struct FinalizingRecordingsMap { - attempts: std::collections::HashMap>, - settled: std::collections::VecDeque<(ProjectObjectId, String)>, - last_preparing_generation: u64, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum FinalizationOrigin { - Recording, - Recovery, -} - -struct FinalizationAttempt { - id: String, - origin: FinalizationOrigin, - project: Arc, - result: watch::Sender, - preparing_generation: Option, - preparing_presentation: - watch::Sender, String>>>, - preparing: watch::Sender, -} - -pub(crate) struct FinalizationToken { - recordings: Arc>, - attempt: Arc, -} - -enum FinalizationRequest { - Started(FinalizationToken), - Existing(watch::Receiver), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum ProjectObjectId { - #[cfg(unix)] - Unix { device: u64, inode: u64 }, - #[cfg(windows)] - Windows { volume: u64, file: [u8; 16] }, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum FinalizationAccess { - Observe, - Write, -} - -pub(crate) struct FinalizationProject { - display_path: PathBuf, - work_path: PathBuf, - identity: ProjectObjectId, - access: FinalizationAccess, - _directory: std::fs::File, -} - -fn open_finalization_directory(path: &Path) -> std::io::Result { - fn validate(metadata: &std::fs::Metadata) -> std::io::Result<()> { - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - if metadata.file_attributes() & 0x400 != 0 { - return Err(std::io::Error::other( - "Recording directory is a reparse point", - )); - } - } - if metadata.file_type().is_symlink() || !metadata.is_dir() { - return Err(std::io::Error::other( - "Recording path is not an ordinary directory", - )); - } - Ok(()) - } - - validate(&path.symlink_metadata()?)?; - let mut options = std::fs::OpenOptions::new(); - options.read(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.custom_flags(nix::libc::O_DIRECTORY | nix::libc::O_NOFOLLOW | nix::libc::O_CLOEXEC); - } - #[cfg(windows)] - { - use std::os::windows::fs::OpenOptionsExt; - options - .custom_flags(0x0200_0000 | 0x0020_0000) - .share_mode(0x1 | 0x2 | 0x4); - } - let directory = options.open(path)?; - validate(&directory.metadata()?)?; - Ok(directory) -} - -fn finalization_directory_identity(directory: &std::fs::File) -> std::io::Result { - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - let metadata = directory.metadata()?; - Ok(ProjectObjectId::Unix { - device: metadata.dev(), - inode: metadata.ino(), - }) - } - #[cfg(windows)] - { - use ::windows::Win32::{ - Foundation::HANDLE, - Storage::FileSystem::{FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx}, - }; - use std::os::windows::io::AsRawHandle; - let mut information = FILE_ID_INFO::default(); - unsafe { - GetFileInformationByHandleEx( - HANDLE(directory.as_raw_handle()), - FileIdInfo, - (&mut information as *mut FILE_ID_INFO).cast(), - std::mem::size_of::() as u32, - ) - } - .map_err(|error| std::io::Error::other(error.to_string()))?; - Ok(ProjectObjectId::Windows { - volume: information.VolumeSerialNumber, - file: information.FileId.Identifier, - }) - } -} - -impl FinalizationProject { - fn capture(display_path: PathBuf, access: FinalizationAccess) -> Result, String> { - let capture = || -> std::io::Result { - let directory = match access { - FinalizationAccess::Observe => { - open_finalization_directory(&display_path.canonicalize()?)? - } - FinalizationAccess::Write => open_finalization_directory(&display_path)?, - }; - let identity = finalization_directory_identity(&directory)?; - let work_path = display_path.canonicalize()?; - let project = Self { - display_path: display_path.clone(), - work_path, - identity, - access, - _directory: directory, - }; - project.validate_identity()?; - Ok(project) - }; - capture() - .map(Arc::new) - .map_err(|error| Self::identity_error(&display_path, error)) - } - - pub(crate) async fn admit(display_path: PathBuf) -> Result, String> { - Self::capture_async(display_path, FinalizationAccess::Write).await - } - - pub(crate) async fn observe(display_path: PathBuf) -> Result, String> { - Self::capture_async(display_path, FinalizationAccess::Observe).await - } - - async fn capture_async( - display_path: PathBuf, - access: FinalizationAccess, - ) -> Result, String> { - let error_path = display_path.clone(); - tokio::task::spawn_blocking(move || Self::capture(display_path, access)) - .await - .map_err(|error| Self::identity_error(&error_path, error))? - } - - fn identity_error(path: &Path, error: impl std::fmt::Display) -> String { - recoverable_finalization_error( - path, - format!("Could not verify the recording directory: {error}"), - ) - } - - fn validate_identity(&self) -> std::io::Result<()> { - let observed_path = match self.access { - FinalizationAccess::Observe => self.display_path.canonicalize()?, - FinalizationAccess::Write => self.display_path.clone(), - }; - for path in [&observed_path, &self.work_path] { - let directory = open_finalization_directory(path)?; - if finalization_directory_identity(&directory)? != self.identity { - return Err(std::io::Error::other(format!( - "Recording directory changed at {}", - path.display() - ))); - } - } - Ok(()) - } - - pub(crate) fn validate(&self) -> Result<(), String> { - self.validate_identity() - .map_err(|error| Self::identity_error(&self.display_path, error)) - } - - pub(crate) async fn validate_async(self: &Arc) -> Result<(), String> { - let project = self.clone(); - tokio::task::spawn_blocking(move || project.validate()) - .await - .map_err(|error| Self::identity_error(&self.display_path, error))? - } - - pub(crate) fn work_path(&self) -> &Path { - &self.work_path - } - - pub(crate) fn display_path(&self) -> &Path { - &self.display_path - } -} - -const EDITOR_PREVIEW_FPS: u32 = 60; -const EDITOR_OUTPUT_SIZE: XY = XY::new(1920, 1080); -const DEFAULT_EDITOR_PREVIEW_SCALE_NUMERATOR: u32 = 65; -const DEFAULT_EDITOR_PREVIEW_SCALE_DENOMINATOR: u32 = 100; - -fn default_editor_preview_resolution() -> XY { - scaled_editor_preview_resolution( - EDITOR_OUTPUT_SIZE, - DEFAULT_EDITOR_PREVIEW_SCALE_NUMERATOR, - DEFAULT_EDITOR_PREVIEW_SCALE_DENOMINATOR, - ) -} - -fn scaled_editor_preview_resolution( - output_size: XY, - numerator: u32, - denominator: u32, -) -> XY { - XY::new( - scaled_editor_preview_dimension(output_size.x, numerator, denominator, 4, 4), - scaled_editor_preview_dimension(output_size.y, numerator, denominator, 2, 2), - ) -} - -fn scaled_editor_preview_dimension( - value: u32, - numerator: u32, - denominator: u32, - minimum: u32, - alignment: u32, -) -> u32 { - let denominator = denominator.max(1); - let alignment = alignment.max(1); - let scaled = ((u64::from(value) * u64::from(numerator)) + (u64::from(denominator) / 2)) - / u64::from(denominator); - let rounded = u32::try_from(scaled).unwrap_or(u32::MAX).max(minimum); - let aligned = u64::from(rounded).div_ceil(u64::from(alignment)) * u64::from(alignment); - - u32::try_from(aligned).unwrap_or(u32::MAX) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tauri_event_loop_runs_once_on_its_calling_thread() { - let thread = std::thread::current().id(); - let calls = std::cell::Cell::new(0); - let result = run_tauri_event_loop(|| { - assert_eq!(std::thread::current().id(), thread); - calls.set(calls.get() + 1); - 42 - }); - assert_eq!(calls.get(), 1); - assert_eq!(result, 42); - } - - #[cfg(target_os = "linux")] - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn linux_tauri_event_loop_allows_blocking_plugin_setup_and_shutdown() { - let thread = std::thread::current().id(); - run_tauri_event_loop(|| { - assert_eq!(std::thread::current().id(), thread); - let plugin_runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) - .enable_all() - .build() - .unwrap(); - assert_eq!(plugin_runtime.block_on(async { 1 }), 1); - let callback = tokio::spawn(async { 2 }); - assert_eq!(plugin_runtime.block_on(callback).unwrap(), 2); - assert_eq!(plugin_runtime.block_on(async { 3 }), 3); - }); - assert_eq!(tokio::spawn(async { 4 }).await.unwrap(), 4); - } - - #[test] - fn default_editor_preview_resolution_matches_frontend_defaults() { - assert_eq!(default_editor_preview_resolution(), XY::new(1248, 702)); - } - - #[test] - fn scaled_editor_preview_resolution_rounds_like_frontend() { - assert_eq!( - scaled_editor_preview_resolution(XY::new(1919, 1079), 65, 100), - XY::new(1248, 702) - ); - } - - #[cfg(target_os = "linux")] - #[test] - fn wayland_clipboard_fallback_requires_wayland_without_x11() { - use std::ffi::OsStr; - - assert!(uses_wayland_clipboard_fallback( - Some(OsStr::new("wayland-1")), - None - )); - assert!(!uses_wayland_clipboard_fallback( - Some(OsStr::new("wayland-1")), - Some(OsStr::new(":0")) - )); - assert!(!uses_wayland_clipboard_fallback(None, None)); - } - - #[test] - fn graphics_recovery_only_engages_for_gpu_init_deaths() { - use crash_sentinel::UnexpectedTermination; - - // Clean previous exit: never engage. - assert!(!should_engage_graphics_recovery(None)); - - // Died outside GPU init (force-quit, power loss, OS kill): a healthy GPU - // machine must keep hardware rendering. - assert!(!should_engage_graphics_recovery(Some( - UnexpectedTermination { - during_gpu_init: false, - in_graphics_recovery: false, - blur_active: false, - } - ))); - - // Died during GPU bring-up on hardware: engage software recovery. - assert!(should_engage_graphics_recovery(Some( - UnexpectedTermination { - during_gpu_init: true, - in_graphics_recovery: false, - blur_active: false, - } - ))); - - // A recovery-mode session that also died must not chain into recovery - // forever; retry hardware. - assert!(!should_engage_graphics_recovery(Some( - UnexpectedTermination { - during_gpu_init: true, - in_graphics_recovery: true, - blur_active: false, - } - ))); - assert!(!should_engage_graphics_recovery(Some( - UnexpectedTermination { - during_gpu_init: false, - in_graphics_recovery: true, - blur_active: false, - } - ))); - } - - #[test] - fn camera_blur_crash_recovery_disables_and_heals_on_update() { - use crash_sentinel::UnexpectedTermination; - - let died_with_blur = Some(UnexpectedTermination { - during_gpu_init: false, - in_graphics_recovery: false, - blur_active: true, - }); - let died_without_blur = Some(UnexpectedTermination { - during_gpu_init: false, - in_graphics_recovery: false, - blur_active: false, - }); - - // Clean exit, blur never disabled: stays enabled. - assert_eq!(next_camera_blur_disabled_version(None, None, "1.0"), None); - - // Deaths unrelated to blur must not disable it. - assert_eq!( - next_camera_blur_disabled_version(None, died_without_blur, "1.0"), - None - ); - - // A death with blur active disables it at the current version. - assert_eq!( - next_camera_blur_disabled_version(None, died_with_blur, "1.0"), - Some("1.0".into()) - ); - - // The disable persists across clean launches of the same version. - assert_eq!( - next_camera_blur_disabled_version(Some("1.0"), None, "1.0"), - Some("1.0".into()) - ); - - // An app update heals: the new stack gets one retry. - assert_eq!( - next_camera_blur_disabled_version(Some("1.0"), None, "1.1"), - None - ); - - // But a fresh blur-attributed death beats version optimism. - assert_eq!( - next_camera_blur_disabled_version(Some("1.0"), died_with_blur, "1.1"), - Some("1.1".into()) - ); - } -} - -#[derive(Default)] -pub struct FinalizingRecordings { - recordings: Arc>, -} - -pub struct CameraWindowCloseGate(AtomicBool); - -impl Default for CameraWindowCloseGate { - fn default() -> Self { - Self(AtomicBool::new(false)) - } -} - -impl CameraWindowCloseGate { - pub fn allow_close(&self) -> bool { - self.0.load(Ordering::Acquire) - } - - pub fn set_allow_close(&self, value: bool) { - self.0.store(value, Ordering::Release); - } -} - -pub struct AppExitState { - exiting: AtomicBool, - restarting: AtomicBool, - title_flush_pending: AtomicBool, - admission: std::sync::Mutex<()>, -} - -impl Default for AppExitState { - fn default() -> Self { - Self { - exiting: AtomicBool::new(false), - restarting: AtomicBool::new(false), - title_flush_pending: AtomicBool::new(false), - admission: std::sync::Mutex::new(()), - } - } -} - -impl AppExitState { - fn begin_title_flush(&self) -> Option> { - self.title_flush_pending - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .ok() - .map(|_| PendingTitleExit(self)) - } - - fn exit_pending(&self) -> bool { - self.is_exiting() || self.title_flush_pending.load(Ordering::Acquire) - } - - pub fn begin(&self) -> bool { - self.exiting - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - } - - pub fn begin_restart(&self) { - self.restarting.store(true, Ordering::Release); - self.exiting.store(true, Ordering::Release); - } - - pub fn is_exiting(&self) -> bool { - self.exiting.load(Ordering::Acquire) - } - - pub fn is_restarting(&self) -> bool { - self.restarting.load(Ordering::Acquire) - } -} - -struct PendingTitleExit<'a>(&'a AppExitState); - -impl Drop for PendingTitleExit<'_> { - fn drop(&mut self) { - self.0.title_flush_pending.store(false, Ordering::Release); - } -} - -pub struct MainWindowReadyState(AtomicBool); - -impl Default for MainWindowReadyState { - fn default() -> Self { - Self(AtomicBool::new(false)) - } -} - -impl MainWindowReadyState { - pub fn is_ready(&self) -> bool { - self.0.load(Ordering::Acquire) - } - - pub fn set_ready(&self, value: bool) { - self.0.store(value, Ordering::Release); - } -} - -const APP_EXIT_STEP_TIMEOUT: Duration = Duration::from_millis(750); -const APP_EXIT_CAMERA_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(1200); -const APP_EXIT_TOTAL_TIMEOUT: Duration = Duration::from_secs(3); -const APP_EXIT_FORCE_TIMEOUT: Duration = Duration::from_secs(8); -#[cfg(target_os = "macos")] -const APP_MENU_QUIT_ID: &str = "app_quit"; -#[cfg(target_os = "macos")] -static MACOS_NATIVE_TERMINATE_APP: std::sync::OnceLock = std::sync::OnceLock::new(); - -async fn await_exit_step(name: &'static str, timeout: Duration, fut: F) -> Option -where - E: std::fmt::Display, - F: Future>, -{ - let started = Instant::now(); - match tokio::time::timeout(timeout, fut).await { - Ok(Ok(value)) => { - info!( - step = name, - elapsed_ms = started.elapsed().as_millis(), - "Exit cleanup step completed" - ); - Some(value) - } - Ok(Err(err)) => { - warn!( - step = name, - elapsed_ms = started.elapsed().as_millis(), - error = %err, - "Exit cleanup step failed" - ); - None - } - Err(_) => { - warn!( - step = name, - timeout_ms = timeout.as_millis(), - elapsed_ms = started.elapsed().as_millis(), - "Exit cleanup step timed out" - ); - None - } - } -} - -fn log_process_memory_snapshot(stage: &'static str) { - let Ok(pid) = sysinfo::get_current_pid() else { - return; - }; - - let mut system = sysinfo::System::new(); - system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true); - - if let Some(process) = system.process(pid) { - info!( - stage = stage, - rss_mb = process.memory() / 1_048_576, - virtual_mb = process.virtual_memory() / 1_048_576, - "Process memory snapshot" - ); - } -} - -fn spawn_process_memory_sampler(app: AppHandle) { - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(60)); - - loop { - interval.tick().await; - - if app_is_exiting(&app) { - break; - } - - log_process_memory_snapshot("periodic"); - } - }); -} - -fn force_exit(code: i32) -> ! { - unsafe extern "C" { - fn _exit(code: i32) -> !; - } - unsafe { _exit(code) } -} - -fn spawn_exit_watchdog() { - std::thread::spawn(move || { - std::thread::sleep(APP_EXIT_FORCE_TIMEOUT); - error!( - timeout_ms = APP_EXIT_FORCE_TIMEOUT.as_millis(), - "Forcing process exit after shutdown deadline" - ); - force_exit(0); - }); -} - -pub(crate) fn app_is_exiting(app: &AppHandle) -> bool { - match app.try_state::() { - Some(state) => state.is_exiting(), - None => false, - } -} - -fn should_show_onboarding(app: &AppHandle) -> bool { - let settings = GeneralSettingsStore::get(app).ok().flatten(); - let startup_completed = settings - .as_ref() - .map(|s| s.has_completed_startup) - .unwrap_or(false); - let onboarding_completed = settings - .as_ref() - .map(|s| s.has_completed_onboarding) - .unwrap_or(false); - - !startup_completed - || !onboarding_completed - || !permissions::do_permissions_check(false).necessary_granted() -} - -#[cfg(target_os = "macos")] -fn build_macos_app_menu(app_handle: &AppHandle) -> tauri::Result> { - let pkg_info = app_handle.package_info(); - let config = app_handle.config(); - let about_metadata = AboutMetadata { - name: Some(pkg_info.name.clone()), - version: Some(pkg_info.version.to_string()), - copyright: config.bundle.copyright.clone(), - authors: config - .bundle - .publisher - .clone() - .map(|publisher| vec![publisher]), - ..Default::default() - }; - let quit_label = config - .product_name - .as_ref() - .map(|name| format!("Quit {name}")) - .unwrap_or_else(|| "Quit Cap".to_string()); - - let window_menu = Submenu::with_id_and_items( - app_handle, - WINDOW_SUBMENU_ID, - "Window", - true, - &[ - &PredefinedMenuItem::minimize(app_handle, None)?, - &PredefinedMenuItem::maximize(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::close_window(app_handle, None)?, - ], - )?; - - let help_menu = Submenu::with_id_and_items(app_handle, HELP_SUBMENU_ID, "Help", true, &[])?; - - Menu::with_items( - app_handle, - &[ - &Submenu::with_items( - app_handle, - pkg_info.name.clone(), - true, - &[ - &PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::services(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::hide(app_handle, None)?, - &PredefinedMenuItem::hide_others(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &MenuItem::with_id( - app_handle, - APP_MENU_QUIT_ID, - quit_label, - true, - Some("Cmd+Q"), - )?, - ], - )?, - &Submenu::with_items( - app_handle, - "File", - true, - &[&PredefinedMenuItem::close_window(app_handle, None)?], - )?, - &Submenu::with_items( - app_handle, - "Edit", - true, - &[ - &PredefinedMenuItem::undo(app_handle, None)?, - &PredefinedMenuItem::redo(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::cut(app_handle, None)?, - &PredefinedMenuItem::copy(app_handle, None)?, - &PredefinedMenuItem::paste(app_handle, None)?, - &PredefinedMenuItem::select_all(app_handle, None)?, - ], - )?, - &Submenu::with_items( - app_handle, - "View", - true, - &[&PredefinedMenuItem::fullscreen(app_handle, None)?], - )?, - &window_menu, - &help_menu, - ], - ) -} - -#[cfg(target_os = "macos")] -unsafe extern "C" fn macos_application_should_terminate( - _: &objc::runtime::Object, - _: objc::runtime::Sel, - _: cocoa::base::id, -) -> isize { - if let Some(app) = MACOS_NATIVE_TERMINATE_APP.get() { - let app = app.clone(); - tokio::spawn(async move { - request_app_exit(app).await; - }); - } else { - warn!("macOS native termination requested before app exit handler was installed"); - } - - 0 -} - -#[cfg(target_os = "macos")] -fn install_macos_native_terminate_handler(app: &AppHandle) { - use cocoa::base::{id, nil}; - use objc::{class, msg_send, sel, sel_impl}; - - let _ = MACOS_NATIVE_TERMINATE_APP.set(app.clone()); - - unsafe { - let ns_app: id = msg_send![class!(NSApplication), sharedApplication]; - let delegate: id = msg_send![ns_app, delegate]; - - if delegate == nil { - warn!("Unable to install macOS native termination handler without app delegate"); - return; - } - - let delegate_class = (*delegate).class() as *const _ as *mut objc::runtime::Class; - let imp = std::mem::transmute::< - unsafe extern "C" fn(&objc::runtime::Object, objc::runtime::Sel, id) -> isize, - objc::runtime::Imp, - >(macos_application_should_terminate); - let added = objc::runtime::class_addMethod( - delegate_class, - sel!(applicationShouldTerminate:), - imp, - c"q@:@".as_ptr(), - ); - - if added == objc::runtime::YES { - info!("Installed macOS native termination handler"); - } else { - warn!("macOS native termination handler was already installed"); - } - } -} - -fn now_millis() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0) -} - -#[derive(Debug)] -pub struct CameraWindowPositionGuard { - ignore_until_ms: AtomicU64, -} - -impl Default for CameraWindowPositionGuard { - fn default() -> Self { - Self { - ignore_until_ms: AtomicU64::new(0), - } - } -} - -impl CameraWindowPositionGuard { - pub fn ignore_for(&self, duration_ms: u64) { - let now = now_millis(); - let until = now.saturating_add(duration_ms); - self.ignore_until_ms.store(until, Ordering::Release); - } - - pub fn should_ignore(&self) -> bool { - let now = now_millis(); - now < self.ignore_until_ms.load(Ordering::Acquire) - } -} - -pub type CameraWindowOperationLock = Mutex<()>; - -impl FinalizingRecordings { - fn request( - &self, - project: Arc, - retry_failed: bool, - origin: FinalizationOrigin, - preparing_requested: bool, - ) -> Result { - if project.access != FinalizationAccess::Write { - return Err("Recording directory was not admitted for recovery.".into()); - } - let mut recordings = self - .recordings - .lock() - .expect("FinalizingRecordings mutex poisoned"); - if let Some(attempt) = recordings.attempts.get(&project.identity) - && attempt - .result - .borrow() - .as_ref() - .is_none_or(|result| !retry_failed && result.is_err()) - { - return Ok(FinalizationRequest::Existing(attempt.result.subscribe())); - } - let preparing_generation = if preparing_requested - && origin == FinalizationOrigin::Recording - && let Some(generation) = recordings.last_preparing_generation.checked_add(1) - { - recordings.last_preparing_generation = generation; - Some(generation) - } else { - None - }; - let attempt = Arc::new(FinalizationAttempt { - id: uuid::Uuid::new_v4().to_string(), - origin, - project, - result: watch::channel(None).0, - preparing_generation, - preparing_presentation: watch::channel(None).0, - preparing: watch::channel(Default::default()).0, - }); - recordings - .settled - .retain(|(identity, _)| *identity != attempt.project.identity); - let _ = recordings - .attempts - .insert(attempt.project.identity, attempt.clone()); - Ok(FinalizationRequest::Started(FinalizationToken { - recordings: self.recordings.clone(), - attempt, - })) - } - - fn start_with_origin( - &self, - project: Arc, - origin: FinalizationOrigin, - preparing_requested: bool, - ) -> Result { - match self.request(project, true, origin, preparing_requested)? { - FinalizationRequest::Started(token) => Ok(token), - FinalizationRequest::Existing(_) => Err( - "This recording is already being prepared. Please wait for it to finish.".into(), - ), - } - } - - pub(crate) fn start_finalizing( - &self, - project: Arc, - ) -> Result { - self.start_with_origin(project, FinalizationOrigin::Recording, true) - } - - pub(crate) fn start_recovering( - &self, - project: Arc, - ) -> Result { - self.start_with_origin(project, FinalizationOrigin::Recovery, false) - } - - pub(crate) async fn recovery_success(&self, path: &Path) -> Result, String> { - let project = FinalizationProject::observe(path.to_path_buf()).await?; - self.recovery_success_for_project(&project).await - } - - pub(crate) async fn recovery_success_for_project( - &self, - project: &Arc, - ) -> Result, String> { - project.validate_async().await?; - let attempt = { - let recordings = self.recordings.lock().unwrap(); - let Some(attempt) = recordings.attempts.get(&project.identity) else { - return Ok(None); - }; - if attempt.origin != FinalizationOrigin::Recovery { - return Ok(None); - } - attempt.clone() - }; - if await_finalization_result(attempt.result.subscribe()) - .await - .is_err() - { - return Ok(None); - } - project.validate_async().await?; - let recordings = self.recordings.lock().unwrap(); - Ok(recordings - .attempts - .get(&project.identity) - .filter(|current| Arc::ptr_eq(current, &attempt)) - .map(|_| attempt.id.clone())) - } - - fn is_finalizing( - &self, - project: &FinalizationProject, - ) -> Option> { - let recordings = self.recordings.lock().unwrap(); - recordings - .attempts - .get(&project.identity) - .filter(|attempt| !matches!(attempt.result.borrow().as_ref(), Some(Ok(())))) - .map(|attempt| attempt.result.subscribe()) - } -} - -fn recoverable_finalization_error(path: &Path, error: String) -> String { - if error.starts_with("Not enough space to finish this recording.") - || error - .to_ascii_lowercase() - .contains("may need to be recovered") - { - return error; - } - format!( - "This recording may need to be recovered. {error} Your recording files have been kept at {}. Try recovery again.", - path.display() - ) -} - -fn has_pending_finalizations(recordings: &FinalizingRecordingsMap) -> bool { - recordings - .attempts - .values() - .any(|attempt| attempt.result.borrow().is_none()) -} - -impl FinalizationToken { - fn publish(&self, result: Result<(), String>) -> bool { - let mut recordings = self.recordings.lock().unwrap(); - if recordings - .attempts - .get(&self.attempt.project.identity) - .is_none_or(|current| !Arc::ptr_eq(current, &self.attempt)) - { - return false; - } - let result = result.map_err(|error| { - recoverable_finalization_error(self.attempt.project.display_path(), error) - }); - let succeeded = result.is_ok(); - let published = self.attempt.result.send_if_modified(|current| { - if current.is_some() { - return false; - } - *current = Some(result); - true - }); - if published { - if succeeded && self.attempt.origin == FinalizationOrigin::Recording { - let _ = recordings.attempts.remove(&self.attempt.project.identity); - } else { - recordings - .settled - .push_back((self.attempt.project.identity, self.attempt.id.clone())); - while recordings.settled.len() > MAX_SETTLED_FINALIZATIONS { - if let Some((identity, id)) = recordings.settled.pop_front() - && recordings.attempts.get(&identity).is_some_and(|attempt| { - attempt.id == id && attempt.result.borrow().is_some() - }) - { - let _ = recordings.attempts.remove(&identity); - } - } - } - } - published - } - - pub(crate) fn finish(self, result: Result<(), String>) { - self.publish(result); - } -} - -impl Drop for FinalizationToken { - fn drop(&mut self) { - self.publish(Err("Preparing this recording was interrupted.".into())); - } -} - -async fn await_finalization_result( - mut result: watch::Receiver, -) -> Result<(), String> { - loop { - if let Some(result) = result.borrow_and_update().clone() { - return result; - } - result - .changed() - .await - .map_err(|_| "Recording finalization ended without a result.".to_string())?; - } -} - -pub(crate) async fn run_finalization_worker( - token: FinalizationToken, - work: impl FnOnce(&FinalizationProject) -> Result + Send + 'static, -) -> Result { - let path = token.attempt.project.display_path().to_path_buf(); - tokio::task::spawn_blocking(move || { - let project = &token.attempt.project; - let result = project - .validate() - .and_then(|()| work(project)) - .and_then(|value| project.validate().map(|()| value)) - .map_err(|error| recoverable_finalization_error(project.display_path(), error)); - token.finish(result.as_ref().map(|_| ()).map_err(Clone::clone)); - result - }) - .await - .map_err(|error| { - recoverable_finalization_error( - &path, - format!("Recording finalization task failed: {error}"), - ) - })? -} - -#[allow(clippy::large_enum_variant)] -pub enum RecordingState { - None, - Pending { - mode: RecordingMode, - target: ScreenCaptureTarget, - }, - Active(InProgressRecording), -} - -#[derive(Clone)] -pub(crate) struct RequestedInput { - value: Option, - revision: u64, - pending: bool, - error: Option, - configuration: Option, -} - -impl RequestedInput { - fn new(value: Option) -> Self { - Self { - value, - revision: 0, - pending: false, - error: None, - configuration: None, - } - } - - fn begin(&mut self, value: Option) -> u64 { - self.value = value; - self.revision = self.revision.wrapping_add(1); - self.pending = true; - self.error = None; - self.configuration = None; - self.revision - } - - fn begin_or_join( - &mut self, - value: Option, - configuration: serde_json::Value, - recording_starting: bool, - ) -> (u64, bool) - where - T: PartialEq, - { - if (self.pending || (recording_starting && self.error.is_none())) - && self.value == value - && self.configuration.as_ref() == Some(&configuration) - { - return (self.revision, true); - } - let revision = self.begin(value); - self.configuration = Some(configuration); - (revision, false) - } - - fn finish(&mut self, revision: u64, result: &Result<(), String>) { - if self.revision == revision { - self.pending = false; - self.error = result.as_ref().err().cloned(); - } - } - - fn prepare_restore(&mut self, revision: u64) -> bool { - if self.revision != revision || self.pending { - return false; - } - self.pending = true; - self.error = None; - true - } - - fn validate(&self, kind: &str) -> Result<(), String> { - if self.pending { - return Err(format!( - "The requested {kind} is still being configured. Wait before recording." - )); - } - if let Some(error) = &self.error { - return Err(format!( - "The requested {kind} could not be configured: {error}. Select it again or choose another input before recording." - )); - } - Ok(()) - } -} - -async fn wait_for_existing_input( - revision: u64, - kind: &str, - read: impl Fn() -> RequestedInput, -) -> Result<(), String> { - tokio::time::timeout(Duration::from_secs(30), async { - loop { - let input = read(); - if input.revision != revision { - return Err(format!( - "{kind} selection was superseded by a newer request" - )); - } - if !input.pending { - return input.error.map_or(Ok(()), Err); - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - .map_err(|_| format!("Timed out waiting for the selected {kind}"))? -} - -#[derive(Clone)] -pub(crate) struct RequestedInputs { - microphone: RequestedInput, - camera: RequestedInput, -} - -pub(crate) struct RequestedInputsState { - inner: Arc>, - operation: Mutex<()>, -} - -impl RequestedInputsState { - fn new(microphone: Option, camera: Option) -> Self { - Self { - inner: Arc::new(std::sync::Mutex::new(RequestedInputs { - microphone: RequestedInput::new(microphone), - camera: RequestedInput::new(camera), - })), - operation: Mutex::new(()), - } - } - - pub(crate) fn snapshot(&self) -> RequestedInputs { - self.inner.lock().unwrap().clone() - } - - pub(crate) fn ready_snapshot(&self) -> Result { - let snapshot = self.snapshot(); - snapshot.microphone.validate("microphone")?; - snapshot.camera.validate("camera")?; - Ok(snapshot) - } - - pub(crate) fn ensure_ready_for_resume(&self) -> Result<(), String> { - self.ready_snapshot().map(|_| ()).map_err(|error| { - format!("Cannot resume recording: {error} Finish configuring or reselect the requested inputs before resuming.") - }) - } - - pub(crate) fn try_resume_guard(&self) -> Result, String> { - let guard = self.operation.try_lock().map_err(|_| { - "Input change in progress. Finish configuring or reselect the requested inputs before resuming.".to_string() - })?; - self.ensure_ready_for_resume()?; - Ok(guard) - } - - pub(crate) fn is_current(&self, snapshot: &RequestedInputs) -> bool { - let current = self.inner.lock().unwrap(); - current.microphone.revision == snapshot.microphone.revision - && current.camera.revision == snapshot.camera.revision - } - - fn mic_is_current(&self, revision: u64) -> bool { - self.inner.lock().unwrap().microphone.revision == revision - } - - fn camera_is_current(&self, revision: u64) -> bool { - self.inner.lock().unwrap().camera.revision == revision - } - - fn publish_mic_if_current(&self, revision: u64, publish: impl FnOnce()) -> bool { - let current = self.inner.lock().unwrap(); - if current.microphone.revision != revision { - return false; - } - publish(); - true - } - - pub(crate) fn publish_camera_if_current(&self, revision: u64, publish: impl FnOnce()) -> bool { - let current = self.inner.lock().unwrap(); - if current.camera.revision != revision { - return false; - } - publish(); - true - } - - pub(crate) fn publish_if_current( - &self, - snapshot: &RequestedInputs, - publish: impl FnOnce(), - ) -> bool { - let current = self.inner.lock().unwrap(); - if current.microphone.revision != snapshot.microphone.revision - || current.camera.revision != snapshot.camera.revision - { - return false; - } - publish(); - true - } -} - -#[derive(Default)] -struct AppliedMicrophoneInput { - valid: bool, - generation: u64, - settings: Option, -} - -impl AppliedMicrophoneInput { - fn prepare_change( - &mut self, - active: bool, - requested: Option<&str>, - selected: Option<&str>, - ) -> bool { - if active && self.valid && requested.is_some() && requested == selected { - return false; - } - self.invalidate(); - true - } - - fn invalidate(&mut self) { - self.valid = false; - self.generation = self.generation.wrapping_add(1); - } - - fn confirm_if_current(&mut self, generation: u64) -> bool { - if self.generation != generation { - return false; - } - self.confirm(); - true - } - - fn confirm(&mut self) { - self.valid = true; - } -} - -pub struct App { - #[deprecated = "can be removed when native camera preview is ready"] - camera_ws_port: u16, - #[deprecated = "can be removed when native camera preview is ready"] - camera_ws_sender: flume::Sender, - #[cfg(target_os = "linux")] - pub camera_processing: linux_instant_camera::ProcessingFactory, - camera_preview: CameraPreviewManager, - camera_preview_state_tx: tokio::sync::watch::Sender, - handle: AppHandle, - recording_state: RecordingState, - recording_logging_handle: LoggingHandle, - mic_feed: ActorRef, - mic_meter_sender: flume::Sender, - selected_mic_label: Option, - applied_mic_input: AppliedMicrophoneInput, - selected_camera_id: Option, - camera_in_use: bool, - camera_cleanup_done: bool, - camera_feed: ActorRef, - server_url: String, - logs_dir: Option, - disconnected_inputs: HashSet, - was_camera_only_recording: bool, -} - -#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub enum VideoType { - Screen, - Output, - Camera, -} - -#[derive(Serialize, Deserialize, specta::Type, Debug)] -pub enum UploadResult { - Success(String), - NotAuthenticated, - PlanCheckFailed, - UpgradeRequired, -} - -#[derive(Serialize, Deserialize, specta::Type, Debug)] -pub struct VideoRecordingMetadata { - pub duration: f64, - pub size: f64, -} - -const CAMERA_PREVIEW_ERROR_EVENT: &str = "camera-preview-error"; -const CAMERA_PREVIEW_CLEAR_EVENT: &str = "camera-preview-clear"; - -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CameraPreviewErrorPayload { - title: String, - message: String, -} - -fn camera_preview_error_message(err: &str) -> String { - if err.contains("DeviceNotFound") { - return "This camera is no longer available. Check that it is connected and allowed by system permissions.".to_string(); - } - - if err.contains("CameraTimeout") { - return "No frames were received from this camera. It may be closed, disconnected, covered, or in use by another app.".to_string(); - } - - if err.contains("StartCapturing") { - return "The system could not start this camera. It may be unavailable or in use by another app.".to_string(); - } - - if err.contains("InvalidFormat") { - return "This camera did not report a usable capture format.".to_string(); - } - - "The selected camera could not be started. Choose another camera or reconnect this one." - .to_string() -} - -fn emit_camera_preview_error(app_handle: &AppHandle, message: String) { - let _ = app_handle.emit( - CAMERA_PREVIEW_ERROR_EVENT, - CameraPreviewErrorPayload { - title: "Camera unavailable".to_string(), - message, - }, - ); -} - -fn emit_camera_preview_clear(app_handle: &AppHandle) { - let _ = app_handle.emit(CAMERA_PREVIEW_CLEAR_EVENT, ()); -} - -async fn add_camera_preview_ws_sender( - camera_feed: &ActorRef, - sender: flume::Sender, -) { - add_camera_preview_sender(camera_feed, sender, "WebSocket").await; -} - -async fn add_camera_preview_sender( - camera_feed: &ActorRef, - sender: flume::Sender, - label: &str, -) { - let result = camera_feed.ask(feeds::camera::AddSender(sender)).await; - if let Err(err) = result { - warn!(error = %err, "Failed to add {label} camera sender"); - } -} - -async fn remove_camera_preview_sender( - camera_feed: &ActorRef, - sender: flume::Sender, - label: &str, -) { - let result = camera_feed.ask(feeds::camera::RemoveSender(sender)).await; - if let Err(err) = result { - warn!(error = %err, "Failed to remove {label} camera sender"); - } -} - -async fn sync_camera_preview_sender( - camera_feed: &ActorRef, - camera_ws_sender: flume::Sender, - camera_preview_sender: Option, - use_ws_preview: bool, -) { - if use_ws_preview { - if let Some(sender) = camera_preview_sender - && let Err(err) = sender.detach(camera_feed).await - { - warn!(error = %err, "Failed to remove native preview camera sender"); - } - - add_camera_preview_ws_sender(camera_feed, camera_ws_sender).await; - } else if let Some(sender) = camera_preview_sender { - if let Err(err) = sender.attach(camera_feed).await { - warn!(error = %err, "Failed to add native preview camera sender"); - } - remove_camera_preview_sender(camera_feed, camera_ws_sender, "WebSocket").await; - } else { - add_camera_preview_ws_sender(camera_feed, camera_ws_sender).await; - } -} - -impl App { - pub fn set_pending_recording( - &mut self, - mode: RecordingMode, - target: ScreenCaptureTarget, - ) -> Result<(), String> { - recording_start_allowed( - self.handle - .try_state::() - .is_some_and(|state| state.exit_pending()), - updates::recording_start_blocked(&self.handle), - )?; - #[cfg(target_os = "linux")] - if mode != RecordingMode::Instant - && recording::linux_instant::current(&self.handle).is_some() - { - return Err("Finish the pending Instant cleanup before recording".into()); - } - if !matches!(self.recording_state, RecordingState::None) { - return Err("Recording already in progress".to_string()); - } - - self.recording_state = RecordingState::Pending { mode, target }; - CurrentRecordingChanged.emit(&self.handle).ok(); - - Ok(()) - } - - pub fn set_current_recording(&mut self, actor: InProgressRecording) { - self.recording_state = RecordingState::Active(actor); - CurrentRecordingChanged.emit(&self.handle).ok(); - } - - pub fn clear_pending_recording(&mut self) -> bool { - #[cfg(target_os = "linux")] - if recording::linux_instant::blocks_cleanup(&self.handle) { - return false; - } - if !matches!(self.recording_state, RecordingState::Pending { .. }) { - return false; - } - - self.recording_state = RecordingState::None; - self.was_camera_only_recording = false; - self.close_occluder_windows(); - crate::windows::apply_content_protection(&self.handle, false); - if let Some(camera) = CapWindowId::Camera.get(&self.handle) { - let _ = camera.set_content_protected(false); - } - CurrentRecordingChanged.emit(&self.handle).ok(); - - true - } - - pub fn clear_current_recording(&mut self) -> Option { - #[cfg(target_os = "linux")] - if recording::linux_instant::blocks_cleanup(&self.handle) { - return None; - } - let previous = std::mem::replace(&mut self.recording_state, RecordingState::None); - match previous { - RecordingState::Active(recording) => { - self.close_occluder_windows(); - crate::windows::apply_content_protection(&self.handle, false); - Some(recording) - } - state => { - self.recording_state = state; - None - } - } - } - - pub fn clear_recording_state(&mut self) -> Option { - #[cfg(target_os = "linux")] - if recording::linux_instant::blocks_cleanup(&self.handle) { - return None; - } - let previous = std::mem::replace(&mut self.recording_state, RecordingState::None); - self.close_occluder_windows(); - crate::windows::apply_content_protection(&self.handle, false); - match previous { - RecordingState::Active(recording) => Some(recording), - _ => None, - } - } - - fn close_occluder_windows(&self) { - for window in self.handle.webview_windows() { - if window.0.starts_with("window-capture-occluder-") { - let _ = window.1.close(); - } - } - } - - fn microphone_settings_for_label( - &self, - label: &str, - ) -> Option { - recording_settings::RecordingSettingsStore::microphone_settings_for(&self.handle, label) - } - - fn camera_settings_for_id( - &self, - id: &DeviceOrModelID, - ) -> Option { - recording_settings::RecordingSettingsStore::camera_settings_for(&self.handle, id) - } - - async fn restart_mic_feed(&mut self) -> Result<(), String> { - self.applied_mic_input.invalidate(); - info!("Restarting microphone feed after actor shutdown"); - - let (error_tx, error_rx) = flume::bounded(1); - let mic_feed = MicrophoneFeed::spawn(MicrophoneFeed::new(error_tx)); - - spawn_mic_error_handler(self.handle.clone(), error_rx); - - mic_feed - .ask(microphone::AddSender(self.mic_meter_sender.clone())) - .await - .map_err(|e| e.to_string())?; - - if let Some(label) = self.selected_mic_label.clone() - && permissions::check_microphone_access().is_ok() - { - let settings = self.microphone_settings_for_label(&label); - match mic_feed.ask(microphone::SetInput { label, settings }).await { - Ok(ready) => { - if let Err(err) = ready.await { - if matches!(err, microphone::SetInputError::DeviceNotFound) { - warn!("Selected microphone not available while restarting feed"); - } else { - return Err(err.to_string()); - } - } - } - Err(kameo::error::SendError::HandlerError( - microphone::SetInputError::DeviceNotFound, - )) => { - warn!("Selected microphone not available while restarting feed"); - } - Err(err) => return Err(err.to_string()), - } - } - - self.mic_feed = mic_feed; - - Ok(()) - } - - async fn ensure_mic_feed_alive(&mut self) -> Result<(), String> { - if self.mic_feed.is_alive() { - return Ok(()); - } - - self.restart_mic_feed().await - } - - async fn add_recording_logging_handle(&mut self, path: &PathBuf) -> Result<(), String> { - let logfile = - std::fs::File::create(path).map_err(|e| format!("Failed to create logfile: {e}"))?; - - self.recording_logging_handle - .reload(Some(Box::new( - tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_target(true) - .with_writer(logfile), - ) as DynLoggingLayer)) - .map_err(|e| format!("Failed to reload logging layer: {e}"))?; - - Ok(()) - } - - pub fn current_recording(&self) -> Option<&InProgressRecording> { - match &self.recording_state { - RecordingState::Active(recording) => Some(recording), - _ => None, - } - } - - pub fn current_recording_mut(&mut self) -> Option<&mut InProgressRecording> { - match &mut self.recording_state { - RecordingState::Active(recording) => Some(recording), - _ => None, - } - } - - pub fn is_recording_active_or_pending(&self) -> bool { - !matches!(self.recording_state, RecordingState::None) - || clean_capture::blocks_idle_cleanup(&self.handle) - } - - async fn handle_input_disconnect(&mut self, kind: RecordingInputKind) -> Result<(), String> { - if matches!(kind, RecordingInputKind::Microphone) { - self.applied_mic_input.invalidate(); - } - if !self.disconnected_inputs.insert(kind) { - return Ok(()); - } - - let (title, body) = match kind { - RecordingInputKind::Microphone => ( - "Microphone disconnected", - "Recording continues. Silence will be used until the microphone reconnects.", - ), - RecordingInputKind::Camera => ( - "Camera disconnected", - "Recording continues without camera. Camera overlay will resume when the device reconnects.", - ), - }; - - let _ = NewNotification { - title: title.to_string(), - body: body.to_string(), - is_error: false, - } - .emit(&self.handle); - - let _ = RecordingEvent::InputLost { input: kind }.emit(&self.handle); - - Ok(()) - } - - async fn handle_input_restored(&mut self, kind: RecordingInputKind) -> Result<(), String> { - let app_handle = self.handle.clone(); - let requested = app_handle.state::(); - let camera_snapshot = requested.snapshot().camera; - if matches!(kind, RecordingInputKind::Camera) - && (self.selected_camera_id.is_none() - || camera_snapshot.pending - || camera_snapshot.value != self.selected_camera_id) - { - return Ok(()); - } - let camera_revision = camera_snapshot.revision; - let pending = match kind { - RecordingInputKind::Microphone => self.disconnected_inputs.remove(&kind), - RecordingInputKind::Camera => self.disconnected_inputs.contains(&kind), - }; - if !pending { - return Ok(()); - } - - match kind { - RecordingInputKind::Microphone => { - if let Err(error) = self.ensure_selected_mic_ready().await { - warn!(%error, "Failed to restore microphone; will retry when access and the device are available"); - self.disconnected_inputs - .insert(RecordingInputKind::Microphone); - return Ok(()); - } - } - RecordingInputKind::Camera => match self.ensure_selected_camera_ready().await { - Ok(()) => { - if !requested.publish_camera_if_current(camera_revision, || { - self.disconnected_inputs.remove(&RecordingInputKind::Camera); - info!("Camera reconnected and reinitialized successfully"); - let _ = NewNotification { - title: "Camera reconnected".to_string(), - body: "Camera overlay has been restored.".to_string(), - is_error: false, - } - .emit(&self.handle); - }) { - return Ok(()); - } - } - Err(e) => { - warn!(error = %e, "Failed to reinitialize camera after reconnect, will retry on next poll"); - return Ok(()); - } - }, - } - - if matches!(kind, RecordingInputKind::Camera) { - requested.publish_camera_if_current(camera_revision, || { - let _ = RecordingEvent::InputRestored { input: kind }.emit(&self.handle); - }); - } else { - let _ = RecordingEvent::InputRestored { input: kind }.emit(&self.handle); - } - - Ok(()) - } - - async fn ensure_selected_mic_ready(&mut self) -> Result<(), String> { - check_requested_microphone_permission( - self.selected_mic_label.as_deref(), - permissions::check_microphone_access, - )?; - self.applied_mic_input.invalidate(); - self.ensure_mic_feed_alive().await?; - - if let Some(label) = self.selected_mic_label.clone() { - let settings = self.microphone_settings_for_label(&label); - let ready = self - .mic_feed - .ask(feeds::microphone::SetInput { label, settings }) - .await - .map_err(|e| e.to_string())?; - - ready.await.map_err(|e| e.to_string())?; - } - - Ok(()) - } - - async fn ensure_selected_camera_ready(&mut self) -> Result<(), String> { - let app_handle = self.handle.clone(); - let requested = app_handle.state::(); - let snapshot = requested.snapshot(); - if snapshot.camera.pending || snapshot.camera.value != self.selected_camera_id { - return Err("Camera selection was superseded by a newer request".into()); - } - check_requested_camera_permission( - self.selected_camera_id.as_ref(), - permissions::check_camera_access, - )?; - if let Some(id) = self.selected_camera_id.clone() { - let settings = self.camera_settings_for_id(&id); - let ready = self - .camera_feed - .ask(feeds::camera::SetInput { - id: id.clone(), - settings, - }) - .await - .map_err(|e| e.to_string())?; - - await_current_camera_request(async { ready.await.map_err(|e| e.to_string()) }, || { - requested.camera_is_current(snapshot.camera.revision) - }) - .await??; - } - - Ok(()) - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app_handle, state))] -async fn set_mic_input( - app_handle: AppHandle, - state: MutableState<'_, App>, - label: Option, -) -> Result<(), String> { - let requested = app_handle.state::(); - let settings = label.as_ref().and_then(|label| { - recording_settings::RecordingSettingsStore::microphone_settings_for(&app_handle, label) - }); - let (revision, joined) = { - let state = state.read().await; - requested.inner.lock().unwrap().microphone.begin_or_join( - label.clone(), - serde_json::json!(settings), - matches!(state.recording_state, RecordingState::Pending { .. }), - ) - }; - if joined { - return wait_for_existing_input(revision, "Microphone", || { - requested.inner.lock().unwrap().microphone.clone() - }) - .await; - } - let result = async { - check_requested_microphone_permission( - label.as_deref(), - permissions::check_microphone_access, - )?; - let _operation = requested.operation.lock().await; - if !requested.mic_is_current(revision) { - return Err("Microphone selection was superseded by a newer request".into()); - } - apply_mic_input(&app_handle, state, label, revision).await - } - .await; - requested - .inner - .lock() - .unwrap() - .microphone - .finish(revision, &result); - result -} - -fn check_requested_microphone_permission( - label: Option<&str>, - check: impl FnOnce() -> Result<(), String>, -) -> Result<(), String> { - if label.is_some() { - check()?; - } - Ok(()) -} - -async fn finish_microphone_input_change( - change: impl std::future::Future>, - is_current: impl FnOnce() -> bool, - attach: impl std::future::Future>, -) -> Result<(), String> { - change.await?; - if !is_current() { - return Err("Microphone selection was superseded by a newer request".into()); - } - attach.await -} - -const MICROPHONE_UNLOCK_TIMEOUT: Duration = Duration::from_millis(500); -const MICROPHONE_UNLOCK_RETRY: Duration = Duration::from_millis(50); -const MICROPHONE_CHANGE_TIMEOUT: Duration = - microphone::SETUP_TIMEOUT.saturating_add(Duration::from_secs(1)); - -async fn wait_for_microphone_setup( - setup: impl std::future::Future>, -) -> Result<(), String> { - tokio::time::timeout(MICROPHONE_CHANGE_TIMEOUT, setup) - .await - .map_err(|_| { - "Timed out configuring the requested microphone. Select it again before recording." - .to_string() - })? -} - -enum MicrophoneRemovalError { - Locked, - Failed(String), -} - -async fn wait_for_microphone_removal(mut remove: impl FnMut() -> F) -> Result<(), String> -where - F: std::future::Future>, -{ - let deadline = tokio::time::Instant::now() + MICROPHONE_UNLOCK_TIMEOUT; - loop { - if tokio::time::Instant::now() >= deadline { - return Err("The microphone is still in use. Stop recording and try again.".into()); - } - match tokio::time::timeout_at(deadline, remove()).await { - Ok(Ok(())) => return Ok(()), - Ok(Err(MicrophoneRemovalError::Failed(error))) => return Err(error), - Ok(Err(MicrophoneRemovalError::Locked)) => {} - Err(_) => { - return Err( - "Timed out waiting for the microphone to close. Stop recording and try again." - .into(), - ); - } - } - let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); - if remaining.is_zero() { - return Err("The microphone is still in use. Stop recording and try again.".into()); - } - tokio::time::sleep(remaining.min(MICROPHONE_UNLOCK_RETRY)).await; - } -} - -async fn remove_microphone_input(feed: &ActorRef) -> Result<(), String> { - wait_for_microphone_removal(|| async { - match feed.ask(microphone::RemoveInput).await { - Ok(()) => Ok(()), - Err(kameo::error::SendError::HandlerError(microphone::FeedLockedError)) => { - Err(MicrophoneRemovalError::Locked) - } - Err(error) => Err(MicrophoneRemovalError::Failed(error.to_string())), - } - }) - .await -} - -async fn acknowledge_studio_microphone( - acknowledgement: impl std::future::Future>, - update_ownership: impl std::future::Future>, - action: &str, -) -> Result<(), String> { - tokio::time::timeout(MICROPHONE_UNLOCK_TIMEOUT, acknowledgement) - .await - .map_err(|_| { - format!("Timed out {action} the Studio microphone. Stop recording and try again.") - })??; - update_ownership.await -} - -fn replace_owned_microphone( - owner: &std::path::Path, - current: Option<(&std::path::Path, &mut Option)>, - replacement: Option, -) -> Result<(), String> { - let Some((current_owner, slot)) = current else { - return Err("The Studio recording stopped while changing microphone input.".into()); - }; - if current_owner != owner { - return Err("The Studio recording changed while changing microphone input.".into()); - } - *slot = replacement; - Ok(()) -} - -fn studio_microphone_owner_matches(app: &App, owner: &std::path::Path) -> bool { - matches!(app.current_recording(), Some(InProgressRecording::Studio { common, .. }) - if common.recording_dir == owner) -} - -fn replace_studio_microphone( - app: &mut App, - owner: &std::path::Path, - replacement: Option>, -) -> Result<(), String> { - let current = match app.current_recording_mut() { - Some(InProgressRecording::Studio { - common, mic_feed, .. - }) => Some((common.recording_dir.as_path(), mic_feed)), - _ => None, - }; - replace_owned_microphone(owner, current, replacement) -} - -async fn apply_mic_input( - app_handle: &AppHandle, - state: MutableState<'_, App>, - desired_label: Option, - revision: u64, -) -> Result<(), String> { - let requested = app_handle.state::(); - check_requested_microphone_permission( - desired_label.as_deref(), - permissions::check_microphone_access, - )?; - - let settings = desired_label.as_ref().and_then(|label| { - recording_settings::RecordingSettingsStore::microphone_settings_for(app_handle, label) - }); - let reusable = { - let app = state.read().await; - (!matches!(app.recording_state, RecordingState::Active(_)) - && app.applied_mic_input.valid - && app.applied_mic_input.settings == settings - && app.selected_mic_label == desired_label) - .then(|| (app.mic_feed.clone(), app.applied_mic_input.generation)) - }; - if let Some((feed, generation)) = reusable - && let Some(label) = &desired_label - && MicrophoneFeed::list_names().contains(label) - && feed - .ask(microphone::CheckInput(label.clone())) - .await - .unwrap_or(false) - { - let app = state.read().await; - if requested.mic_is_current(revision) - && app.applied_mic_input.valid - && app.applied_mic_input.generation == generation - { - return Ok(()); - } - } - - let (mic_feed, studio_handle, applied_generation) = { - let mut app = state.write().await; - if !requested.mic_is_current(revision) { - return Err("Microphone selection was superseded by a newer request".into()); - } - app.ensure_mic_feed_alive().await?; - let selected_label = app.selected_mic_label.clone(); - let active = matches!(app.recording_state, RecordingState::Active(_)); - let needs_change = app.applied_mic_input.prepare_change( - active, - desired_label.as_deref(), - selected_label.as_deref(), - ); - - if let Some(label) = desired_label.as_ref() - && !MicrophoneFeed::list_names().contains(label) - { - app.applied_mic_input.invalidate(); - return Err(format!( - "Selected microphone '{label}' is no longer available. Reconnect it or choose another microphone." - )); - } - if !needs_change { - return Ok(()); - } - - let handle = match app.current_recording() { - Some(InProgressRecording::Studio { handle, common, .. }) => { - Some((handle.clone(), common.recording_dir.clone())) - } - _ => None, - }; - - ( - app.mic_feed.clone(), - handle, - app.applied_mic_input.generation, - ) - }; - - let change = async { - if let Some((handle, owner)) = &studio_handle { - acknowledge_studio_microphone( - async { - handle - .set_mic_feed(None) - .await - .map_err(|error| error.to_string()) - }, - async { - let mut app = state.write().await; - replace_studio_microphone(&mut app, owner, None) - }, - "detaching", - ) - .await?; - remove_microphone_input(&mic_feed).await?; - } - - match desired_label.as_ref() { - None if studio_handle.is_none() => remove_microphone_input(&mic_feed).await?, - None => {} - Some(label) => { - if let Some((_, owner)) = &studio_handle - && !studio_microphone_owner_matches(&*state.read().await, owner) - { - return Err( - "The Studio recording stopped while changing microphone input.".into(), - ); - } - wait_for_microphone_setup(async { - mic_feed - .ask(feeds::microphone::SetInput { - label: label.clone(), - settings, - }) - .await - .map_err(|error| error.to_string())? - .await - .map(drop) - .map_err(|error| error.to_string()) - }) - .await?; - } - } - - Ok(()) - }; - let attach = async { - if let Some((handle, owner)) = &studio_handle - && let Some(label) = desired_label.as_ref() - { - let mic_lock = - tokio::time::timeout(MICROPHONE_UNLOCK_TIMEOUT, mic_feed.ask(microphone::Lock)) - .await - .map_err(|_| "Timed out locking the requested microphone.".to_string())? - .map_err(|error| error.to_string())?; - if mic_lock.device_name() != label { - return Err( - "Microphone input changed before Studio could attach it. Select it again." - .into(), - ); - } - let mic_lock = Arc::new(mic_lock); - acknowledge_studio_microphone( - async { - handle - .set_mic_feed(Some(mic_lock.clone())) - .await - .map_err(|error| error.to_string()) - }, - async { - let mut app = state.write().await; - replace_studio_microphone(&mut app, owner, Some(mic_lock.clone())) - }, - "attaching", - ) - .await?; - } - - Ok(()) - }; - let apply_result = - finish_microphone_input_change(change, || requested.mic_is_current(revision), attach).await; - - if let Some((_, owner)) = &studio_handle - && !studio_microphone_owner_matches(&*state.read().await, owner) - { - remove_microphone_input(&mic_feed).await?; - return Err("The Studio recording stopped while changing microphone input.".into()); - } - - match apply_result { - Ok(()) => { - let mut app = state.write().await; - if let Some((_, owner)) = &studio_handle - && !studio_microphone_owner_matches(&app, owner) - { - drop(app); - remove_microphone_input(&mic_feed).await?; - return Err("The Studio recording stopped while changing microphone input.".into()); - } - let mut cleared = false; - let mut confirmed = false; - if !requested.publish_mic_if_current(revision, || { - if !app.applied_mic_input.confirm_if_current(applied_generation) { - return; - } - confirmed = true; - app.applied_mic_input.settings = settings; - app.selected_mic_label = desired_label; - cleared = app - .disconnected_inputs - .remove(&RecordingInputKind::Microphone); - }) { - return Err("Microphone selection was superseded by a newer request".into()); - } - - if !confirmed { - return Err("Microphone changed or disconnected during setup. Select it again before recording.".into()); - } - if cleared { - let _ = RecordingEvent::InputRestored { - input: RecordingInputKind::Microphone, - } - .emit(&app.handle); - } - - Ok(()) - } - Err(err) => Err(err), - } -} - -#[tauri::command] -#[specta::specta] -async fn upload_logs(app_handle: AppHandle) -> Result<(), String> { - logging::upload_log_file(&app_handle).await -} - -#[tauri::command] -#[specta::specta] -fn get_system_diagnostics() -> cap_recording::diagnostics::SystemDiagnostics { - cap_recording::diagnostics::collect_diagnostics() -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app_handle, state))] -#[allow(unused_mut)] -async fn set_camera_input( - app_handle: AppHandle, - state: MutableState<'_, App>, - id: Option, - skip_camera_window: Option, -) -> Result<(), String> { - let requested = app_handle.state::(); - let settings = id.as_ref().and_then(|id| { - recording_settings::RecordingSettingsStore::camera_settings_for(&app_handle, id) - }); - let (revision, joined) = { - let state = state.read().await; - requested.inner.lock().unwrap().camera.begin_or_join( - id.clone(), - serde_json::json!((settings, skip_camera_window.unwrap_or(false))), - matches!(state.recording_state, RecordingState::Pending { .. }), - ) - }; - if joined { - return wait_for_existing_input(revision, "Camera", || { - requested.inner.lock().unwrap().camera.clone() - }) - .await; - } - let result = async { - check_requested_camera_permission(id.as_ref(), permissions::check_camera_access)?; - let _operation = requested.operation.lock().await; - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - apply_camera_input(&app_handle, state, id, skip_camera_window, revision).await - } - .await; - requested - .inner - .lock() - .unwrap() - .camera - .finish(revision, &result); - result -} - -fn check_requested_camera_permission( - id: Option<&DeviceOrModelID>, - check: impl FnOnce() -> Result<(), String>, -) -> Result<(), String> { - if id.is_some() { - check()?; - } - Ok(()) -} - -async fn await_current_camera_request( - request: impl std::future::Future, - is_current: impl FnOnce() -> bool, -) -> Result { - let result = request.await; - if !is_current() { - return Err("Camera selection was superseded by a newer request".into()); - } - Ok(result) -} - -async fn apply_camera_input( - app_handle: &AppHandle, - state: MutableState<'_, App>, - id: Option, - skip_camera_window: Option, - revision: u64, -) -> Result<(), String> { - check_requested_camera_permission(id.as_ref(), permissions::check_camera_access)?; - let requested = app_handle.state::(); - let operation_lock = app_handle.state::(); - let _operation_guard = operation_lock.lock().await; - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - let app = state.read().await; - let camera_feed = app.camera_feed.clone(); - let studio_handle = match app.current_recording() { - Some(InProgressRecording::Studio { handle, .. }) => Some(handle.clone()), - _ => None, - }; - let current_id = app.selected_camera_id.clone(); - let camera_in_use = app.camera_in_use; - let recording_active = matches!(app.recording_state, RecordingState::Active(_)); - drop(app); - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - - let skip_camera_window = skip_camera_window.unwrap_or(false); - let camera_window_is_visible = CapWindowId::Camera - .get(app_handle) - .and_then(|window| window.is_visible().ok()) - .unwrap_or(false); - - if id == current_id && camera_in_use && recording_active && !skip_camera_window { - let show_result = if camera_window_is_visible { - Ok(()) - } else { - show_requested_camera_window(app_handle, revision).await - }; - - show_result - .map_err(|err| error!("Failed to show camera preview window: {err}")) - .ok(); - - return Ok(()); - } - - if let Some(id) = &id - && !is_camera_available(id) - { - return Err(format!( - "Selected camera '{}' is no longer available. Reconnect it or choose another camera.", - recording::camera_id_label(id) - )); - } - - let settings = id.as_ref().and_then(|id| { - recording_settings::RecordingSettingsStore::camera_settings_for(app_handle, id) - }); - if !recording_active - && !skip_camera_window - && camera_in_use - && id == current_id - && let Some(id) = &id - && camera_feed - .ask(feeds::camera::CheckInput { - id: id.clone(), - settings, - }) - .await - .unwrap_or(false) - { - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - if !camera_window_is_visible { - show_requested_camera_window(app_handle, revision).await?; - } - return Ok(()); - } - - if let Some(handle) = &studio_handle { - handle - .set_camera_feed(None) - .await - .map_err(|e| e.to_string())?; - } - - match &id { - None => { - let shutdown_rx = { - let app = &mut *state.write().await; - let mut shutdown_rx = None; - if !requested.publish_camera_if_current(revision, || { - app.camera_in_use = false; - app.camera_cleanup_done = true; - if skip_camera_window { - shutdown_rx = app.camera_preview.begin_shutdown(); - } else { - app.camera_preview.pause(); - } - }) { - return Err("Camera selection was superseded by a newer request".into()); - } - shutdown_rx - }; - - camera_feed - .ask(feeds::camera::RemoveInput) - .await - .map_err(|e| e.to_string())?; - - if let Some(rx) = shutdown_rx { - let _ = tokio::time::timeout(Duration::from_millis(500), rx).await; - } - - if !skip_camera_window && let Some(window) = CapWindowId::Camera.get(app_handle) { - let _ = window.hide(); - } - } - Some(id) => { - if !requested.publish_camera_if_current(revision, || { - emit_camera_preview_clear(app_handle); - }) { - return Err("Camera selection was superseded by a newer request".into()); - } - let (camera_ws_sender, camera_preview_sender, use_ws_preview) = { - let app = &mut *state.write().await; - let use_ws_preview = !(camera_window_is_visible - && app.camera_preview.is_initialized() - && !app.camera_preview.is_paused()); - if !requested.publish_camera_if_current(revision, || { - app.camera_in_use = true; - app.camera_cleanup_done = false; - }) { - return Err("Camera selection was superseded by a newer request".into()); - } - #[allow(deprecated)] - ( - app.camera_ws_sender.clone(), - app.camera_preview.sender(), - use_ws_preview, - ) - }; - - sync_camera_preview_sender( - &camera_feed, - camera_ws_sender, - camera_preview_sender, - use_ws_preview, - ) - .await; - - let mut showed_camera_window = skip_camera_window; - let mut attempts = 0; - let init_result: Result<(), String> = loop { - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - attempts += 1; - - let request = camera_feed - .ask(feeds::camera::SetInput { - id: id.clone(), - settings, - }) - .await - .map_err(|e| e.to_string()); - - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - if !showed_camera_window { - showed_camera_window = true; - let show_result = show_requested_camera_window(app_handle, revision).await; - show_result - .map_err(|err| error!("Failed to show camera preview window: {err}")) - .ok(); - } - - let result = await_current_camera_request( - async { - match request { - Ok(future) => future.await.map_err(|e| e.to_string()), - Err(e) => Err(e), - } - }, - || requested.camera_is_current(revision), - ) - .await?; - - match result { - Ok(_) => { - if !requested.publish_camera_if_current(revision, || { - emit_camera_preview_clear(app_handle); - }) { - return Err("Camera selection was superseded by a newer request".into()); - } - break Ok(()); - } - Err(e) => { - if attempts == 1 - && !skip_camera_window - && !requested.publish_camera_if_current(revision, || { - emit_camera_preview_error( - app_handle, - camera_preview_error_message(&e), - ); - }) - { - return Err("Camera selection was superseded by a newer request".into()); - } - if attempts >= 3 { - break Err(format!( - "Failed to initialize camera after {attempts} attempts: {e}" - )); - } - debug!( - "Failed to set camera input (attempt {}): {}. Retrying...", - attempts, e - ); - await_current_camera_request( - tokio::time::sleep(Duration::from_millis(500)), - || requested.camera_is_current(revision), - ) - .await?; - } - } - }; - - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - if let Err(e) = init_result { - let message = camera_preview_error_message(&e); - let _ = camera_feed.ask(feeds::camera::RemoveInput).await; - let app = &mut *state.write().await; - if !requested.publish_camera_if_current(revision, || { - app.camera_in_use = false; - if app.disconnected_inputs.insert(RecordingInputKind::Camera) { - let _ = RecordingEvent::InputLost { - input: RecordingInputKind::Camera, - } - .emit(app_handle); - } - emit_camera_preview_error(app_handle, message.clone()); - let _ = NewNotification { - title: "Camera unavailable".to_string(), - body: message, - is_error: true, - } - .emit(app_handle); - }) { - return Err("Camera selection was superseded by a newer request".into()); - } - return Err(e); - } - } - } - - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - if let Some(handle) = studio_handle - && id.is_some() - { - let camera_lock = camera_feed - .ask(feeds::camera::Lock) - .await - .map_err(|e| e.to_string())?; - handle - .set_camera_feed(Some(Arc::new(camera_lock))) - .await - .map_err(|e| e.to_string())?; - } - - { - let app = &mut *state.write().await; - let mut cleared = false; - if !requested.publish_camera_if_current(revision, || { - app.selected_camera_id = id; - app.camera_in_use = app.selected_camera_id.is_some(); - app.camera_cleanup_done = !app.camera_in_use; - cleared = app.disconnected_inputs.remove(&RecordingInputKind::Camera); - }) { - return Err("Camera selection was superseded by a newer request".into()); - } - - if cleared { - let _ = RecordingEvent::InputRestored { - input: RecordingInputKind::Camera, - } - .emit(&app.handle); - } - } - - Ok(()) -} - -async fn show_requested_camera_window(app: &AppHandle, revision: u64) -> Result<(), String> { - let requested = app.state::(); - if !requested.camera_is_current(revision) { - return Err("Camera selection was superseded by a newer request".into()); - } - let result = ShowCapWindow::Camera { centered: false }.show(app).await; - if !requested.camera_is_current(revision) { - if let Some(window) = CapWindowId::Camera.get(app) { - let _ = window.hide(); - } - return Err("Camera selection was superseded by a newer request".into()); - } - result.map(|_| ()).map_err(|error| error.to_string()) -} - -pub(crate) async fn restore_requested_inputs(app_handle: &AppHandle) { - let requested = app_handle.state::(); - let snapshot = requested.snapshot(); - if snapshot.microphone.pending || snapshot.camera.pending { - return; - } - let microphone_permission = check_requested_microphone_permission( - snapshot.microphone.value.as_deref(), - permissions::check_microphone_access, - ); - let camera_permission = check_requested_camera_permission( - snapshot.camera.value.as_ref(), - permissions::check_camera_access, - ); - let _operation = requested.operation.lock().await; - let state = app_handle.state::>(); - if !requested.is_current(&snapshot) || state.read().await.is_recording_active_or_pending() { - return; - } - if !requested - .inner - .lock() - .unwrap() - .microphone - .prepare_restore(snapshot.microphone.revision) - { - return; - } - let mic_result = match microphone_permission { - Ok(()) => { - apply_mic_input( - app_handle, - state.clone(), - snapshot.microphone.value.clone(), - snapshot.microphone.revision, - ) - .await - } - Err(error) => Err(error), - }; - requested - .inner - .lock() - .unwrap() - .microphone - .finish(snapshot.microphone.revision, &mic_result); - if let Err(error) = mic_result { - warn!(%error, "Failed to restore requested microphone"); - } - - let restore_camera = { - let state = state.read().await; - requested.is_current(&snapshot) - && !state.is_recording_active_or_pending() - && !state.camera_cleanup_done - && !state.camera_in_use - && snapshot.camera.value.is_some() - }; - if !restore_camera { - return; - } - if !requested - .inner - .lock() - .unwrap() - .camera - .prepare_restore(snapshot.camera.revision) - { - return; - } - let camera_result = match camera_permission { - Ok(()) => { - apply_camera_input( - app_handle, - state, - snapshot.camera.value, - Some(false), - snapshot.camera.revision, - ) - .await - } - Err(error) => Err(error), - }; - requested - .inner - .lock() - .unwrap() - .camera - .finish(snapshot.camera.revision, &camera_result); - if let Err(error) = camera_result { - warn!(%error, "Failed to restore requested camera"); - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app_handle, state))] -async fn set_native_camera_preview_enabled( - app_handle: AppHandle, - state: MutableState<'_, App>, - enabled: bool, -) -> Result<(), String> { - if enabled && cfg!(not(target_os = "macos")) { - return Err("Native camera preview is only available on macOS".to_string()); - } - - let operation_lock = app_handle.state::(); - let _operation_guard = operation_lock.lock().await; - - GeneralSettingsStore::update(&app_handle, |settings| { - settings.enable_native_camera_preview = enabled; - })?; - - let window = CapWindowId::Camera.get(&app_handle); - - if enabled { - let Some(window) = window else { - return Ok(()); - }; - let is_visible = window.is_visible().unwrap_or(false); - if !is_visible { - return Ok(()); - } - - let init_result = { - let mut app = state.write().await; - windows::ensure_camera_input_active(&mut app).await; - if app.selected_camera_id.is_none() || !app.camera_in_use { - None - } else { - let camera_feed = app.camera_feed.clone(); - Some(app.camera_preview.init_window(window, camera_feed).await) - } - }; - - let Some(init_result) = init_result else { - return Ok(()); - }; - - if let Err(err) = init_result { - GeneralSettingsStore::update(&app_handle, |settings| { - settings.enable_native_camera_preview = false; - })?; - return Err(format!("Failed to enable native camera preview: {err}")); - } - - let (camera_feed, camera_ws_sender) = { - let app = state.read().await; - #[allow(deprecated)] - (app.camera_feed.clone(), app.camera_ws_sender.clone()) - }; - #[allow(deprecated)] - let result = camera_feed - .ask(feeds::camera::RemoveSender(camera_ws_sender)) - .await; - if let Err(err) = result { - warn!(error = %err, "Failed to remove camera WebSocket sender"); - } - - return Ok(()); - } - - let (shutdown_rx, camera_feed, camera_ws_sender, should_restore_ws_sender) = { - let mut app = state.write().await; - #[allow(deprecated)] - ( - app.camera_preview.begin_shutdown(), - app.camera_feed.clone(), - app.camera_ws_sender.clone(), - app.selected_camera_id.is_some() && app.camera_in_use, - ) - }; - - if let Some(rx) = shutdown_rx { - let _ = tokio::time::timeout(Duration::from_secs(2), rx).await; - } - - if should_restore_ws_sender { - #[allow(deprecated)] - let result = camera_feed - .ask(feeds::camera::AddSender(camera_ws_sender)) - .await; - if let Err(err) = result { - warn!(error = %err, "Failed to restore camera WebSocket sender"); - } - } - - Ok(()) -} - -fn display_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 = bounds.position().x(); - let y = bounds.position().y(); - let width = bounds.size().width(); - let height = bounds.size().height(); - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - Some(display) - } else { - None - } - }) -} - -// On Windows the per-monitor logical rects overlap in mixed-DPI layouts, so a -// window's display must be resolved from its physical position; logical -// containment (display_for_position) can match the wrong monitor there. -#[cfg(windows)] -fn display_for_physical_position(pos_x: f64, pos_y: f64) -> Option { - Display::list().into_iter().find(|display| { - display - .raw_handle() - .physical_bounds() - .is_some_and(|bounds| { - pos_x >= bounds.position().x() - && pos_x < bounds.position().x() + bounds.size().width() - && pos_y >= bounds.position().y() - && pos_y < bounds.position().y() + bounds.size().height() - }) - }) -} - -pub(crate) fn update_camera_window_position_settings( - settings: &mut GeneralSettingsStore, - x: f64, - y: f64, - display_id: Option, -) { - let display = display_id - .and_then(|id| Display::from_id(&id)) - .or_else(|| display_for_position(x, y)); - let display_id = display.as_ref().map(|display| display.id()); - let monitor_name = display - .as_ref() - .and_then(|display| display.name()) - .filter(|name| !name.trim().is_empty()); - let position = general_settings::WindowPosition { x, y, display_id }; - settings.camera_window_position = Some(position.clone()); - if let Some(monitor_name) = monitor_name { - settings - .camera_window_positions_by_monitor_name - .insert(monitor_name, position); - } -} - -fn spawn_mic_error_handler(app_handle: AppHandle, error_rx: flume::Receiver) { - tokio::spawn(async move { - let state = app_handle.state::>(); - let state = state.inner().clone(); - - let error_rx = error_rx; - - while let Ok(err) = error_rx.recv_async().await { - if app_is_exiting(&app_handle) { - break; - } - - error!("Mic feed actor error: {err}"); - - { - let mut app = state.write().await; - - if let Err(handle_err) = app - .handle_input_disconnect(RecordingInputKind::Microphone) - .await - { - warn!("Failed to handle mic disconnect event: {handle_err}"); - } - } - - tokio::time::sleep(Duration::from_millis(500)).await; - - if app_is_exiting(&app_handle) { - break; - } - - let mut app = state.write().await; - if app_is_exiting(&app_handle) { - break; - } - match app.ensure_selected_mic_ready().await { - Ok(()) => { - info!("Microphone stream recovered after error"); - if let Err(restored_err) = app - .handle_input_restored(RecordingInputKind::Microphone) - .await - { - warn!("Failed to handle mic restoration: {restored_err}"); - } - } - Err(restart_err) => { - warn!("Failed to restart microphone input after error: {restart_err}"); - } - } - } - }); -} - -fn spawn_device_watchers(app_handle: AppHandle) { - spawn_microphone_watcher(app_handle.clone()); - spawn_camera_watcher(app_handle); -} - -#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] -pub struct DevicesUpdated { - cameras: Vec, - microphones: Vec, - permissions: permissions::OSPermissionsCheck, -} - -#[tauri::command] -#[specta::specta] -async fn get_devices_snapshot() -> DevicesUpdated { - let permissions = permissions::do_permissions_check(false); - let cameras = if permissions.camera.permitted() { - cap_camera::list_cameras().collect() - } else { - Vec::new() - }; - let microphones = if permissions.microphone.permitted() { - MicrophoneFeed::list_names() - } else { - Vec::new() - }; - DevicesUpdated { - cameras, - microphones, - permissions, - } -} - -fn any_webview_window_visible(app: &AppHandle) -> bool { - app.webview_windows() - .values() - .any(|window| window.is_visible().unwrap_or(false)) -} - -fn spawn_devices_snapshot_emitter(app_handle: AppHandle) { - tokio::spawn(async move { - let mut last_perm_tuple: (u8, u8, u8, u8) = (255, 255, 255, 255); - let mut last_camera_ids: Vec = Vec::new(); - let mut last_mics: Vec = Vec::new(); - let mut fast_loops = 0u32; - loop { - if app_is_exiting(&app_handle) { - break; - } - - if power_observer::is_system_asleep() { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - continue; - } - - if !any_webview_window_visible(&app_handle) { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - continue; - } - - let permissions = permissions::do_permissions_check(false); - let Some((cameras, microphones)) = collect_device_inventory( - || app_is_exiting(&app_handle), - permissions.camera.permitted(), - permissions.microphone.permitted(), - || cap_camera::list_cameras().collect::>(), - MicrophoneFeed::list_names, - ) else { - break; - }; - let perm_tuple = ( - match permissions.screen_recording { - permissions::OSPermissionStatus::NotNeeded => 0, - permissions::OSPermissionStatus::Empty => 1, - permissions::OSPermissionStatus::Granted => 2, - permissions::OSPermissionStatus::Denied => 3, - }, - match permissions.microphone { - permissions::OSPermissionStatus::NotNeeded => 0, - permissions::OSPermissionStatus::Empty => 1, - permissions::OSPermissionStatus::Granted => 2, - permissions::OSPermissionStatus::Denied => 3, - }, - match permissions.camera { - permissions::OSPermissionStatus::NotNeeded => 0, - permissions::OSPermissionStatus::Empty => 1, - permissions::OSPermissionStatus::Granted => 2, - permissions::OSPermissionStatus::Denied => 3, - }, - match permissions.accessibility { - permissions::OSPermissionStatus::NotNeeded => 0, - permissions::OSPermissionStatus::Empty => 1, - permissions::OSPermissionStatus::Granted => 2, - permissions::OSPermissionStatus::Denied => 3, - }, - ); - let camera_ids: Vec = - cameras.iter().map(|c| c.device_id().to_string()).collect(); - let mut changed = perm_tuple != last_perm_tuple; - if !changed { - changed = camera_ids != last_camera_ids || microphones != last_mics; - } - if changed && !app_is_exiting(&app_handle) { - DevicesUpdated { - cameras: cameras.clone(), - microphones: microphones.clone(), - permissions: permissions.clone(), - } - .emit(&app_handle) - .ok(); - last_perm_tuple = perm_tuple; - last_camera_ids = camera_ids; - last_mics = microphones; - } - let dur = if fast_loops < 10 { - std::time::Duration::from_millis(500) - } else { - std::time::Duration::from_secs(5) - }; - fast_loops = fast_loops.saturating_add(1); - tokio::time::sleep(dur).await; - - if app_is_exiting(&app_handle) { - break; - } - } - }); -} - -fn spawn_system_resume_detector(app_handle: AppHandle) { - const TICK: Duration = Duration::from_secs(5); - const WAKE_THRESHOLD: Duration = Duration::from_secs(20); - - tokio::spawn(async move { - let mut last_instant = std::time::Instant::now(); - let mut last_system_time = std::time::SystemTime::now(); - - loop { - if app_is_exiting(&app_handle) { - break; - } - - tokio::time::sleep(TICK).await; - - if app_is_exiting(&app_handle) { - break; - } - - let now_instant = std::time::Instant::now(); - let now_system = std::time::SystemTime::now(); - - let monotonic_delta = now_instant.saturating_duration_since(last_instant); - let wall_delta = now_system - .duration_since(last_system_time) - .unwrap_or_default(); - - last_instant = now_instant; - last_system_time = now_system; - - let drift = wall_delta.saturating_sub(monotonic_delta); - let slept = drift >= WAKE_THRESHOLD || monotonic_delta >= TICK + WAKE_THRESHOLD; - - if slept { - tracing::warn!( - monotonic_ms = monotonic_delta.as_millis(), - wall_ms = wall_delta.as_millis(), - "System resume drift detected; scheduling resume recovery" - ); - - schedule_resume_recovery(app_handle.clone()); - } - } - }); -} - -pub(crate) fn schedule_resume_recovery(app_handle: AppHandle) { - tauri::async_runtime::spawn(async move { - if app_is_exiting(&app_handle) { - return; - } - - tokio::time::sleep(Duration::from_millis(500)).await; - - if app_is_exiting(&app_handle) { - return; - } - - #[cfg(target_os = "macos")] - { - if let Some(prewarmer) = - app_handle.try_state::() - { - prewarmer.request(true).await; - } else { - warn!("ScreenCapturePrewarmer state unavailable during resume recovery"); - } - } - - if !app_is_exiting(&app_handle) { - emit_app_event_safely(&app_handle, RequestScreenCapturePrewarm { force: true }); - } - - if !app_is_exiting(&app_handle) { - let snapshot = get_devices_snapshot().await; - emit_app_event_safely(&app_handle, snapshot); - } - }); -} - -async fn cleanup_camera_window(app: AppHandle, session_id: u64) { - if app_is_exiting(&app) { - return; - } - - let state = app.state::>(); - - let camera_feed = { - let mut app_state = state.write().await; - - if app_is_exiting(&app) { - return; - } - - let current_session_id = app_state - .camera_preview - .session_id_handle() - .load(Ordering::Acquire); - - if current_session_id != session_id { - tracing::info!( - "Camera cleanup aborted: session mismatch (cleanup session {} vs current {})", - session_id, - current_session_id - ); - return; - } - - if app_state.camera_cleanup_done { - return; - } - - app_state.camera_cleanup_done = true; - app_state.camera_preview.pause(); - - if app_state.is_recording_active_or_pending() { - return; - } - - let has_visible_target_overlay = app.webview_windows().iter().any(|(label, window)| { - label.starts_with("target-select-overlay-") && window.is_visible().unwrap_or(false) - }); - - let main_window_visible = CapWindowId::Main - .get(&app) - .and_then(|w| w.is_visible().ok()) - .unwrap_or(false); - - let is_camera_only_mode = recording_settings::RecordingSettingsStore::get(&app) - .ok() - .flatten() - .and_then(|s| s.target) - .is_some_and(|t| matches!(t, ScreenCaptureTarget::CameraOnly)); - - if is_camera_only_mode && main_window_visible { - tracing::info!("Camera cleanup: preserving camera feed for camera-only mode"); - return; - } - - if has_visible_target_overlay { - return; - } - - app_state.camera_feed.clone() - }; - - let _ = tokio::time::timeout( - APP_EXIT_STEP_TIMEOUT, - camera_feed.ask(feeds::camera::RemoveInput), - ) - .await; - - let mut app_state = state.write().await; - app_state.camera_in_use = false; -} - -async fn cleanup_camera_after_overlay_close(app: AppHandle, captured_session_id: u64) { - if app_is_exiting(&app) { - return; - } - - let state = app.state::>(); - - let camera_feed = { - let mut app_state = state.write().await; - - if app_is_exiting(&app) { - return; - } - - let current_session_id = app_state - .camera_preview - .session_id_handle() - .load(Ordering::Acquire); - if current_session_id != captured_session_id { - tracing::info!( - "Camera cleanup after overlay aborted: session mismatch ({} vs {})", - captured_session_id, - current_session_id - ); - return; - } - - if app_state.camera_cleanup_done { - return; - } - - if app_state.is_recording_active_or_pending() { - return; - } - - let has_camera_window = CapWindowId::Camera.get(&app).is_some(); - if has_camera_window { - return; - } - - let has_visible_target_overlay = app.webview_windows().iter().any(|(label, window)| { - label.starts_with("target-select-overlay-") && window.is_visible().unwrap_or(false) - }); - if has_visible_target_overlay { - return; - } - - app_state.camera_cleanup_done = true; - - if !app_state.camera_in_use { - return; - } - - app_state.camera_feed.clone() - }; - - let _ = camera_feed.ask(feeds::camera::RemoveInput).await; - - let mut app_state = state.write().await; - app_state.camera_in_use = false; -} - -async fn cleanup_app_resources_for_exit(app: &AppHandle) { - let started = Instant::now(); - log_process_memory_snapshot("exit_cleanup_begin"); - - // Reverse the macOS Liquid Glass private SPI (remove the NSGlassEffectView, restore - // window/WKWebView occlusion detection) BEFORE anything else, so a slow camera/ML - // shutdown can never starve it. Leaving an occlusion-suppressed private glass view - // attached when the process hard-exits can wedge WindowServer on macOS 26 and soft- - // restart the user's login session. Bounded so a stuck main thread can't block exit. - #[cfg(target_os = "macos")] - { - let _ = await_exit_step( - "teardown_liquid_glass", - APP_EXIT_STEP_TIMEOUT, - crate::platform::teardown_all_liquid_glass(app), - ) - .await; - } - - export::cancel_all_exports(); - upload::lifecycle::shutdown().await; - power_observer::uninstall(app); - fake_window::cancel_all_fake_window_listeners(app); - close_target_select_overlays(app); - - let preparing = app - .state::() - .inner() - .clone(); - let _ = await_exit_step( - "dispose_preparing_editor_frames", - APP_EXIT_STEP_TIMEOUT, - preparing.dispose_all(), - ) - .await; - - let app_for_pending_editors = app.clone(); - let _ = await_exit_step( - "dispose_pending_editor_instances", - APP_EXIT_STEP_TIMEOUT, - async move { - PendingEditorInstances::dispose_all(&app_for_pending_editors).await; - Ok::<(), String>(()) - }, - ) - .await; - - let app_for_editors = app.clone(); - let _ = await_exit_step( - "dispose_editor_instances", - APP_EXIT_STEP_TIMEOUT, - async move { - EditorInstances::dispose_all(&app_for_editors).await; - Ok::<(), String>(()) - }, - ) - .await; - - let app_for_pending_screenshot_editors = app.clone(); - let _ = await_exit_step( - "dispose_pending_screenshot_editor_instances", - APP_EXIT_STEP_TIMEOUT, - async move { - PendingScreenshotEditorInstances::dispose_all(&app_for_pending_screenshot_editors) - .await; - Ok::<(), String>(()) - }, - ) - .await; - - let app_for_screenshot_editors = app.clone(); - let _ = await_exit_step( - "dispose_screenshot_editor_instances", - APP_EXIT_STEP_TIMEOUT, - async move { - ScreenshotEditorInstances::dispose_all(&app_for_screenshot_editors).await; - Ok::<(), String>(()) - }, - ) - .await; - - log_process_memory_snapshot("exit_after_editor_dispose"); - - let (mic_feed, camera_feed, camera_shutdown) = { - let Some(state) = app.try_state::>() else { - warn!("App state unavailable during exit cleanup"); - return; - }; - let mut app_state = state.write().await; - let camera_shutdown = app_state.camera_preview.begin_shutdown(); - app_state.applied_mic_input.invalidate(); - app_state.camera_in_use = false; - app_state.selected_camera_id = None; - ( - app_state.mic_feed.clone(), - app_state.camera_feed.clone(), - camera_shutdown, - ) - }; - - let _ = await_exit_step( - "remove_microphone_input", - APP_EXIT_STEP_TIMEOUT, - async move { mic_feed.ask(microphone::RemoveInput).await }, - ) - .await; - let _ = await_exit_step("remove_camera_input", APP_EXIT_STEP_TIMEOUT, async move { - camera_feed.ask(feeds::camera::RemoveInput).await - }) - .await; - - if let Some(rx) = camera_shutdown { - let _ = await_exit_step( - "camera_preview_shutdown", - APP_EXIT_CAMERA_SHUTDOWN_TIMEOUT, - rx, - ) - .await; - } - - captions::release_ml_models().await; - log_process_memory_snapshot("exit_cleanup_end"); - info!( - elapsed_ms = started.elapsed().as_millis(), - "Exit cleanup completed" - ); -} - -#[cfg(target_os = "macos")] -fn finalize_app_exit(app: &AppHandle, exit_code: i32) -> ! { - let _ = app; - sentry::Hub::with(|hub| { - if let Some(client) = hub.client() { - let _ = client.flush(Some(Duration::from_millis(250))); - } - }); - match app_exit_action(exit_code) { - AppExitAction::Process(code) => force_exit(code), - } -} - -#[cfg(not(target_os = "macos"))] -fn finalize_app_exit(app: &AppHandle, exit_code: i32) { - match app_exit_action(exit_code) { - AppExitAction::Runtime(code) => app.exit(code), - } -} - -pub(crate) fn with_idle_app( - app: &AppHandle, - include_exports: bool, - begin: impl FnOnce() -> T, -) -> Result { - with_idle_app_for_title_flush(app, include_exports, false, begin) -} - -fn with_idle_app_for_title_flush( - app: &AppHandle, - include_exports: bool, - owns_title_flush: bool, - begin: impl FnOnce() -> T, -) -> Result { - let exit_state = app - .try_state::() - .ok_or(ExitBlocked::StateUnavailable)?; - let _admission = exit_state - .admission - .try_lock() - .map_err(|_| ExitBlocked::StateUnavailable)?; - if exit_state.is_exiting() - || (!owns_title_flush && exit_state.title_flush_pending.load(Ordering::Acquire)) - { - return Err(ExitBlocked::AlreadyExiting); - } - let state = app - .try_state::>() - .ok_or(ExitBlocked::StateUnavailable)?; - let finalizing = app - .try_state::() - .ok_or(ExitBlocked::StateUnavailable)?; - let _clean_capture = app - .try_state::() - .ok_or(ExitBlocked::StateUnavailable)?; - let updates = app - .try_state::() - .ok_or(ExitBlocked::StateUnavailable)?; - if updates.is_installing() { - return Err(ExitBlocked::UpdateInstalling); - } - with_idle_recording_state( - &state, - |state| { - if state.is_recording_active_or_pending() || clean_capture::phase(app).is_some() { - return Err(ExitBlocked::RecordingActive); - } - let recordings = finalizing - .recordings - .try_lock() - .map_err(|_| ExitBlocked::StateUnavailable)?; - if has_pending_finalizations(&recordings) { - return Err(ExitBlocked::FinalizationActive); - } - if include_exports { - if export::export_session_active() { - return Err(ExitBlocked::ExportActive); - } - if upload::upload_session_active() { - return Err(ExitBlocked::UploadActive); - } - } - Ok(()) - }, - begin, - ) -} - -fn begin_app_exit_if_idle(app: &AppHandle, restarting: bool) -> Result { - let exit_state = app - .try_state::() - .ok_or(ExitBlocked::StateUnavailable)?; - with_idle_app(app, restarting, || { - if !exit_state.begin() { - return false; - } - if restarting { - exit_state.begin_restart(); - } - true - }) -} - -pub(crate) fn prepare_app_exit( - app: &AppHandle, - prepare: impl FnOnce() -> Result<(), String>, -) -> Result<(), String> { - let exit_state = app - .try_state::() - .ok_or(ExitBlocked::StateUnavailable.message())?; - with_idle_app(app, true, || { - prepare_then_begin_exit(prepare, || exit_state.begin()) - }) - .map_err(|reason| reason.message().to_string())? -} - -fn show_exit_blocked(app: &AppHandle, reason: ExitBlocked) { - warn!( - ?reason, - "Refusing app exit while recording safety is unconfirmed" - ); - app.dialog() - .message(reason.message()) - .title("Cap is still busy") - .kind(tauri_plugin_dialog::MessageDialogKind::Warning) - .show(|_| {}); -} - -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct EditorTitleSaveRequest { - request_id: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct EditorTitleSaveResponse { - request_id: String, - window_label: String, - error: Option, -} - -async fn wait_for_editor_title_saves( - request_id: &str, - mut pending: HashSet, - mut responses: tokio::sync::mpsc::UnboundedReceiver, - timeout: Duration, -) -> Result<(), String> { - tokio::time::timeout(timeout, async { - while !pending.is_empty() { - let response = responses - .recv() - .await - .ok_or("The editor title save connection closed before saving finished.")?; - if response.request_id != request_id || !pending.remove(&response.window_label) { - continue; - } - if let Some(error) = response.error { - return Err(format!("Could not save the recording title: {error}")); - } - } - Ok(()) - }) - .await - .map_err(|_| { - "Could not confirm that all recording titles were saved. Try Quit again.".to_string() - })? -} - -async fn flush_editor_titles_for_exit( - app: &AppHandle, -) -> Result, String> { - let labels: HashSet<_> = app - .webview_windows() - .into_keys() - .filter(|label| matches!(CapWindowId::from_str(label), Ok(CapWindowId::Editor { .. }))) - .collect(); - if labels.is_empty() { - return Ok(None); - } - - let request = EditorTitleSaveRequest { - request_id: uuid::Uuid::new_v4().to_string(), - }; - let (sender, responses) = tokio::sync::mpsc::unbounded_channel(); - let listener = app.listen_any("editor-title-save-finished", move |event| { - if let Ok(response) = serde_json::from_str(event.payload()) { - let _ = sender.send(response); - } - }); - let result = async { - for label in &labels { - app.emit_to(label, "editor-title-save-request", &request) - .map_err(|error| format!("Could not request an editor title save: {error}"))?; - } - wait_for_editor_title_saves( - &request.request_id, - labels.clone(), - responses, - Duration::from_secs(5), - ) - .await?; - if app.webview_windows().keys().any(|label| { - !labels.contains(label) - && matches!(CapWindowId::from_str(label), Ok(CapWindowId::Editor { .. })) - }) { - return Err("Another editor opened while preparing to quit. Try Quit again.".into()); - } - Ok(()) - } - .await; - app.unlisten(listener); - if result.is_err() { - let _ = app.emit("editor-title-save-cancelled", &request); - } - result.map(|()| Some(request)) -} - -#[tauri::command] -#[specta::specta] -fn restart_app(app: AppHandle) -> Result<(), String> { - match begin_app_exit_if_idle(&app, true) { - Ok(true) => { - app.request_restart(); - Ok(()) - } - Ok(false) => Err("Cap is already shutting down.".into()), - Err(reason) => Err(reason.message().into()), - } -} - -pub async fn request_app_exit(app: AppHandle) { - let Some(exit_state) = app.try_state::() else { - show_exit_blocked(&app, ExitBlocked::StateUnavailable); - return; - }; - let pending = match with_idle_app(&app, false, || exit_state.begin_title_flush()) { - Ok(Some(pending)) => pending, - Ok(None) => return, - Err(ExitBlocked::AlreadyExiting) => return, - Err(reason) => { - show_exit_blocked(&app, reason); - return; - } - }; - let request = match flush_editor_titles_for_exit(&app).await { - Ok(request) => request, - Err(error) => { - drop(pending); - warn!(%error, "Quit canceled because an editor title could not be saved"); - app.dialog() - .message(format!( - "{error}\n\nCap is still open. Your title is still in the editor." - )) - .title("Title not saved") - .kind(tauri_plugin_dialog::MessageDialogKind::Warning) - .show(|_| {}); - return; - } - }; - let admission = with_idle_app_for_title_flush(&app, true, true, || exit_state.begin()); - drop(pending); - match admission { - Ok(true) => {} - result => { - if let Some(request) = request { - let _ = app.emit("editor-title-save-cancelled", request); - } - if let Err(reason) = result { - show_exit_blocked(&app, reason); - } - return; - } - } - complete_admitted_app_exit(app).await; -} - -pub(crate) async fn complete_admitted_app_exit(app: AppHandle) { - #[cfg(target_os = "macos")] - cancel_macos_startup_opens(&app); - spawn_exit_watchdog(); - export::cancel_all_exports(); - - 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 app resources for exit" - ); - } else { - // Cleanup finished within budget — disarm the sentinel so this graceful exit - // is not reported as an unexpected termination on next launch. A timed-out - // (hung) shutdown deliberately leaves it armed. - crash_sentinel::mark_clean_exit(); - } - - finalize_app_exit(&app, 0); -} - -pub(crate) fn find_mic_by_label(devices: &[String], selected_label: &str) -> Option { - if devices.iter().any(|name| name == selected_label) { - return Some(selected_label.to_string()); - } - - None -} - -fn spawn_microphone_watcher(app_handle: AppHandle) { - tokio::spawn(async move { - let state = app_handle.state::>(); - let state = state.inner().clone(); - - loop { - if app_is_exiting(&app_handle) { - break; - } - - if power_observer::is_system_asleep() { - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - - let (should_check, label, is_marked) = { - let guard = state.read().await; - ( - matches!(guard.recording_state, RecordingState::Active(_)), - guard.selected_mic_label.clone(), - guard - .disconnected_inputs - .contains(&RecordingInputKind::Microphone), - ) - }; - - if should_check && let Some(selected_label) = label { - let Some(devices) = run_while_active( - || app_is_exiting(&app_handle), - microphone::MicrophoneFeed::list_names, - ) else { - break; - }; - let matched = find_mic_by_label(&devices, &selected_label); - - if matched.is_none() && !is_marked { - let mut app = state.write().await; - if let Err(err) = app - .handle_input_disconnect(RecordingInputKind::Microphone) - .await - { - warn!("Failed to handle mic disconnect: {err}"); - } - } else if matched.is_some() && is_marked { - let mut app = state.write().await; - - if let Err(err) = app - .handle_input_restored(RecordingInputKind::Microphone) - .await - { - warn!("Failed to handle mic reconnection: {err}"); - } - } - } - - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); -} - -fn spawn_camera_watcher(app_handle: AppHandle) { - tokio::spawn(async move { - let state = app_handle.state::>(); - let state = state.inner().clone(); - - loop { - if app_is_exiting(&app_handle) { - break; - } - - if power_observer::is_system_asleep() { - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - - let (should_check, camera_id, is_marked) = { - let guard = state.read().await; - ( - matches!(guard.recording_state, RecordingState::Active(_)) - && guard.camera_in_use, - guard.selected_camera_id.clone(), - guard - .disconnected_inputs - .contains(&RecordingInputKind::Camera), - ) - }; - - if should_check && let Some(ref selected_id) = camera_id { - let Some(available) = run_while_active( - || app_is_exiting(&app_handle), - || is_camera_available(selected_id), - ) else { - break; - }; - debug!( - "Camera watcher: checking availability for {:?}, available={}, is_marked={}", - selected_id, available, is_marked - ); - - if !available && !is_marked { - warn!( - "Camera watcher: camera {:?} detected as unavailable, continuing recording without camera", - selected_id - ); - let mut app = state.write().await; - if let Err(err) = app - .handle_input_disconnect(RecordingInputKind::Camera) - .await - { - warn!("Failed to handle camera disconnect: {err}"); - } - } else if available && is_marked { - let mut app = state.write().await; - if let Err(err) = app.handle_input_restored(RecordingInputKind::Camera).await { - warn!("Failed to handle camera reconnection: {err}"); - } - } - } - - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); -} - -pub(crate) fn is_camera_available(id: &DeviceOrModelID) -> bool { - let cameras: Vec<_> = cap_camera::list_cameras().collect(); - debug!( - "is_camera_available: looking for {:?} in {} cameras", - id, - cameras.len() - ); - for camera in &cameras { - debug!( - " - device_id={}, model_id={:?}, name={}", - camera.device_id(), - camera.model_id(), - camera.display_name() - ); - } - cameras.iter().any(|info| match id { - DeviceOrModelID::DeviceID(device_id) => info.device_id() == device_id, - DeviceOrModelID::ModelID(model_id) => { - info.model_id().is_some_and(|existing| existing == model_id) - } - }) -} - -#[derive(specta::Type, Serialize, tauri_specta::Event, Clone)] -pub struct RecordingOptionsChanged; - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct NewStudioRecordingAdded { - path: PathBuf, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct EditorRecordingAdded { - pub editor_path: PathBuf, - pub recording_path: PathBuf, -} - -#[derive(Deserialize, specta::Type, tauri_specta::Event, Debug, Clone, Serialize)] -pub struct RecordingDeleted { - path: PathBuf, -} - -#[derive(specta::Type, tauri_specta::Event, Serialize)] -pub struct SetCaptureAreaPending(bool); - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct NewScreenshotAdded { - path: PathBuf, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RecordingStarted; - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RecordingStopped; - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RequestStartRecording { - pub mode: RecordingMode, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RequestOpenRecordingPicker { - pub target_mode: Option, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RequestSetTargetMode { - pub target_mode: Option, - pub display_id: Option, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RequestOpenSettings { - page: String, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RequestScrollToSettingsSection { - pub section: String, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct RequestScreenCapturePrewarm { - #[serde(default)] - pub force: bool, -} - -#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] -pub struct NewNotification { - title: String, - body: String, - is_error: bool, -} - -type ArcLock = Arc>; -pub type MutableState<'a, T> = State<'a, Arc>>; - -#[cfg(target_os = "linux")] -pub enum ClipboardContext { - Platform(PlatformClipboardContext), - Wayland(AppHandle), -} - -#[cfg(target_os = "linux")] -fn uses_wayland_clipboard_fallback( - wayland_display: Option<&std::ffi::OsStr>, - x11_display: Option<&std::ffi::OsStr>, -) -> bool { - wayland_display.is_some() && x11_display.is_none() -} - -#[cfg(target_os = "linux")] -impl ClipboardContext { - fn new(app: &AppHandle) -> clipboard_rs::common::Result { - match PlatformClipboardContext::new() { - Ok(context) => Ok(Self::Platform(context)), - Err(error) - if uses_wayland_clipboard_fallback( - std::env::var_os("WAYLAND_DISPLAY").as_deref(), - std::env::var_os("DISPLAY").as_deref(), - ) => - { - info!(%error, "Using native Wayland clipboard"); - Ok(Self::Wayland(app.clone())) - } - Err(error) => Err(error), - } - } - - fn set_text(&self, text: String) -> clipboard_rs::common::Result<()> { - match self { - Self::Platform(context) => context.set_text(text), - Self::Wayland(app) => app.clipboard().write_text(text).map_err(Into::into), - } - } - - fn set_image(&self, image: clipboard_rs::RustImageData) -> clipboard_rs::common::Result<()> { - match self { - Self::Platform(context) => context.set_image(image), - Self::Wayland(app) => { - let rgba = image.to_rgba8()?; - let width = rgba.width(); - let height = rgba.height(); - let image = tauri::image::Image::new_owned(rgba.into_raw(), width, height); - app.clipboard().write_image(&image).map_err(Into::into) - } - } - } - - fn set_files(&self, files: Vec) -> clipboard_rs::common::Result<()> { - match self { - Self::Platform(context) => context.set_files(files), - Self::Wayland(app) => { - let urls = files - .into_iter() - .map(|path| { - tauri::Url::from_file_path(&path) - .map(|url| url.to_string()) - .map_err(|()| format!("Invalid clipboard file path: {path}")) - }) - .collect::, _>>()?; - - if urls.is_empty() { - return Err("No files supplied for clipboard".into()); - } - - app.clipboard() - .write_text(urls.join("\r\n")) - .map_err(Into::into) - } - } - } -} - -type SingleTuple = (T,); - -#[derive(Serialize, Type)] -struct JsonValue( - #[serde(skip)] PhantomData, - #[specta(type = SingleTuple)] serde_json::Value, -); - -impl Clone for JsonValue { - fn clone(&self) -> Self { - Self(PhantomData, self.1.clone()) - } -} - -impl JsonValue { - fn new(value: &T) -> Self { - Self(PhantomData, json!(value)) - } -} - -#[derive(Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct RecordingInfo { - capture_target: ScreenCaptureTarget, -} - -#[derive(Serialize, Type)] -#[serde(rename_all = "camelCase")] -enum CurrentRecordingTarget { - Window { - id: WindowId, - bounds: Option, - }, - Screen { - id: DisplayId, - }, - Area { - screen: DisplayId, - bounds: LogicalBounds, - }, - Camera, -} - -#[derive(Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub enum RecordingStatus { - Pending, - Recording, -} - -#[derive(Serialize, Type)] -#[serde(rename_all = "camelCase")] -struct CurrentRecording { - target: CurrentRecordingTarget, - mode: RecordingMode, - status: RecordingStatus, -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(state))] -async fn get_current_recording( - state: MutableState<'_, App>, -) -> Result>, ()> { - let state = state.read().await; - - let (mode, capture_target, status) = match &state.recording_state { - RecordingState::None => { - return Ok(JsonValue::new(&None)); - } - RecordingState::Pending { mode, target } => (*mode, target, RecordingStatus::Pending), - RecordingState::Active(inner) => ( - inner.mode(), - inner.capture_target(), - RecordingStatus::Recording, - ), - }; - - let target = match capture_target { - ScreenCaptureTarget::Display { id } => CurrentRecordingTarget::Screen { id: id.clone() }, - ScreenCaptureTarget::Window { id } => { - let bounds = - scap_targets::Window::from_id(id).and_then(|w| w.display_relative_logical_bounds()); - CurrentRecordingTarget::Window { - id: id.clone(), - bounds, - } - } - ScreenCaptureTarget::Area { screen, bounds } => CurrentRecordingTarget::Area { - screen: screen.clone(), - bounds: *bounds, - }, - ScreenCaptureTarget::CameraOnly => CurrentRecordingTarget::Camera, - }; - - Ok(JsonValue::new(&Some(CurrentRecording { - target, - mode, - status, - }))) -} - -#[derive(Serialize, Type, tauri_specta::Event, Clone)] -pub struct CurrentRecordingChanged; - -pub(crate) async fn create_screenshot( - input: PathBuf, - output: PathBuf, - size: Option<(u32, u32)>, -) -> Result<(), String> { - let result: Result<(), String> = tokio::task::spawn_blocking(move || -> Result<(), String> { - let mut ictx = ffmpeg::format::input(&input).map_err(|e| e.to_string())?; - let input_stream = ictx - .streams() - .best(ffmpeg::media::Type::Video) - .ok_or("No video stream found")?; - let video_stream_index = input_stream.index(); - - let mut decoder = - ffmpeg::codec::context::Context::from_parameters(input_stream.parameters()) - .map_err(|e| e.to_string())? - .decoder() - .video() - .map_err(|e| e.to_string())?; - - let mut scaler = ffmpeg::software::scaling::context::Context::get( - decoder.format(), - decoder.width(), - decoder.height(), - ffmpeg::format::Pixel::RGB24, - size.map_or(decoder.width(), |s| s.0), - size.map_or(decoder.height(), |s| s.1), - ffmpeg::software::scaling::flag::Flags::BILINEAR, - ) - .map_err(|e| e.to_string())?; - - let mut frame = ffmpeg::frame::Video::empty(); - for (stream, packet) in ictx.packets() { - if stream.index() == video_stream_index { - decoder.send_packet(&packet).map_err(|e| e.to_string())?; - if decoder.receive_frame(&mut frame).is_ok() { - let mut rgb_frame = ffmpeg::frame::Video::empty(); - scaler - .run(&frame, &mut rgb_frame) - .map_err(|e| e.to_string())?; - - let width = rgb_frame.width() as usize; - let height = rgb_frame.height() as usize; - let bytes_per_pixel = 3; - let src_stride = rgb_frame.stride(0); - let dst_stride = width * bytes_per_pixel; - - let mut img_buffer = vec![0u8; height * dst_stride]; - - for y in 0..height { - let src_slice = - &rgb_frame.data(0)[y * src_stride..y * src_stride + dst_stride]; - let dst_slice = &mut img_buffer[y * dst_stride..(y + 1) * dst_stride]; - dst_slice.copy_from_slice(src_slice); - } - - let img = image::RgbImage::from_raw(width as u32, height as u32, img_buffer) - .ok_or("Failed to create image from frame data")?; - - img.save_with_format(&output, image::ImageFormat::Jpeg) - .map_err(|e| e.to_string())?; - - return Ok(()); - } - } - } - - Err("Failed to create screenshot".to_string()) - }) - .await - .map_err(|e| format!("Task join error: {e}"))?; - - result -} - -pub(crate) async fn create_screenshot_source_from_segments( - segments_dir: &std::path::Path, -) -> Result { - let init_path = segments_dir.join("init.mp4"); - if !init_path.exists() { - return Err(format!("init.mp4 not found in {}", segments_dir.display())); - } - - let first_segment = find_first_segment(segments_dir) - .ok_or_else(|| format!("No .m4s segments found in {}", segments_dir.display()))?; - - let temp_path = segments_dir.join(".screenshot_source.mp4"); - let mut out = tokio::fs::File::create(&temp_path) - .await - .map_err(|e| format!("Failed to create screenshot source: {e}"))?; - - let mut init_file = tokio::fs::File::open(&init_path) - .await - .map_err(|e| format!("Failed to open init.mp4: {e}"))?; - tokio::io::copy(&mut init_file, &mut out) - .await - .map_err(|e| format!("Failed to copy init.mp4: {e}"))?; - - let mut seg_file = tokio::fs::File::open(&first_segment) - .await - .map_err(|e| format!("Failed to open {}: {e}", first_segment.display()))?; - tokio::io::copy(&mut seg_file, &mut out) - .await - .map_err(|e| format!("Failed to copy segment: {e}"))?; - - Ok(temp_path) -} - -fn find_first_segment(dir: &std::path::Path) -> Option { - let mut segments: Vec = std::fs::read_dir(dir) - .ok()? - .filter_map(|e| { - let path = e.ok()?.path(); - if path.extension().is_some_and(|ext| ext == "m4s") { - Some(path) - } else { - None - } - }) - .collect(); - segments.sort(); - segments.into_iter().next() -} - -// async fn create_thumbnail(input: PathBuf, output: PathBuf, size: (u32, u32)) -> Result<(), String> { -// println!("Creating thumbnail: input={input:?}, output={output:?}, size={size:?}"); - -// tokio::task::spawn_blocking(move || -> Result<(), String> { -// let img = image::open(&input).map_err(|e| { -// eprintln!("Failed to open image: {e}"); -// e.to_string() -// })?; - -// let width = img.width() as usize; -// let height = img.height() as usize; -// let bytes_per_pixel = 3; -// let src_stride = width * bytes_per_pixel; - -// let rgb_img = img.to_rgb8(); -// let img_buffer = rgb_img.as_raw(); - -// let mut corrected_buffer = vec![0u8; height * src_stride]; - -// for y in 0..height { -// let src_slice = &img_buffer[y * src_stride..(y + 1) * src_stride]; -// let dst_slice = &mut corrected_buffer[y * src_stride..(y + 1) * src_stride]; -// dst_slice.copy_from_slice(src_slice); -// } - -// let corrected_img = -// image::RgbImage::from_raw(width as u32, height as u32, corrected_buffer) -// .ok_or("Failed to create corrected image")?; - -// let thumbnail = image::imageops::resize( -// &corrected_img, -// size.0, -// size.1, -// image::imageops::FilterType::Lanczos3, -// ); - -// thumbnail -// .save_with_format(&output, image::ImageFormat::Png) -// .map_err(|e| { -// eprintln!("Failed to save thumbnail: {e}"); -// e.to_string() -// })?; - -// println!("Thumbnail created successfully"); -// Ok(()) -// }) -// .await -// .map_err(|e| format!("Task join error: {e}"))? -// } - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(), String> { - run_command_safely("copy_file_to_path", copy_file_to_path_inner(app, src, dst)).await -} - -async fn copy_file_to_path_inner(app: AppHandle, src: String, dst: String) -> Result<(), String> { - info!(%src, %dst, "Attempting to copy file"); - - let is_screenshot = src.contains("screenshots/"); - let is_gif = src.ends_with(".gif") || dst.ends_with(".gif"); - - let src_path = std::path::Path::new(&src); - if !src_path.exists() { - return Err(format!("Source file {src} does not exist")); - } - - if !is_screenshot && !is_gif && !is_valid_video(src_path) { - let mut attempts = 0; - while attempts < 10 { - // Replaced `std::thread::sleep` with `tokio::time::sleep` — the previous version - // blocked the tokio worker for up to 10 seconds, which on release builds with - // smaller worker pools could starve the runtime and cause unrelated tasks to time - // out (including window/IPC events). - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - if is_valid_video(src_path) { - break; - } - attempts += 1; - } - if attempts == 10 { - return Err("Source video file is not a valid MP4".to_string()); - } - } - - if let Some(parent) = std::path::Path::new(&dst).parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|e| format!("Failed to create target directory: {e}"))?; - } - - let mut attempts = 0; - const MAX_ATTEMPTS: u32 = 3; - let mut last_error = None; - - while attempts < MAX_ATTEMPTS { - match tokio::fs::copy(&src, &dst).await { - Ok(bytes) => { - let src_size = match tokio::fs::metadata(&src).await { - Ok(metadata) => metadata.len(), - Err(e) => { - last_error = Some(format!("Failed to get source file metadata: {e}")); - attempts += 1; - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - continue; - } - }; - - if bytes != src_size { - last_error = Some(format!( - "File copy verification failed: copied {bytes} bytes but source is {src_size} bytes" - )); - let _ = tokio::fs::remove_file(&dst).await; - attempts += 1; - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - continue; - } - - if !is_screenshot && !is_gif && !is_valid_video(std::path::Path::new(&dst)) { - last_error = Some("Destination file is not a valid".to_string()); - let _ = tokio::fs::remove_file(&dst).await; - attempts += 1; - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - continue; - } - - info!(bytes, %src, %dst, "Successfully copied file"); - - notifications::send_notification( - &app, - if is_screenshot { - notifications::NotificationType::ScreenshotSaved - } else { - notifications::NotificationType::VideoSaved - }, - ); - return Ok(()); - } - Err(e) => { - last_error = Some(e.to_string()); - attempts += 1; - if attempts < MAX_ATTEMPTS { - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - continue; - } - } - } - } - - let last_error_message = last_error - .as_deref() - .unwrap_or("Maximum retry attempts exceeded"); - error!( - %src, - %dst, - attempts = MAX_ATTEMPTS, - error = %last_error_message, - "Failed to copy file after retries" - ); - - notifications::send_notification( - &app, - if is_screenshot { - notifications::NotificationType::ScreenshotSaveFailed - } else { - notifications::NotificationType::VideoSaveFailed - }, - ); - - Err(last_error.unwrap_or_else(|| "Maximum retry attempts exceeded".to_string())) -} - -pub fn is_valid_video(path: &std::path::Path) -> bool { - match ffmpeg::format::input(path) { - Ok(input_context) => { - // Check if we have at least one video stream - input_context - .streams() - .any(|stream| stream.parameters().medium() == ffmpeg::media::Type::Video) - } - Err(_) => false, - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(clipboard))] -async fn copy_screenshot_to_clipboard( - clipboard: MutableState<'_, ClipboardContext>, - path: String, -) -> Result<(), String> { - println!("Copying screenshot to clipboard: {path:?}"); - - let img_data = clipboard_rs::RustImageData::from_path(&path) - .map_err(|e| format!("Failed to copy screenshot to clipboard: {e}"))?; - clipboard - .write() - .await - .set_image(img_data) - .map_err(|err| format!("Failed to copy screenshot to clipboard: {err}"))?; - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(clipboard, data))] -async fn copy_image_to_clipboard( - clipboard: MutableState<'_, ClipboardContext>, - data: Vec, -) -> Result<(), String> { - println!("Copying image to clipboard ({} bytes)", data.len()); - - let img_data = clipboard_rs::RustImageData::from_bytes(&data) - .map_err(|e| format!("Failed to create image data from bytes: {e}"))?; - clipboard - .write() - .await - .set_image(img_data) - .map_err(|err| format!("Failed to copy image to clipboard: {err}"))?; - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(clipboard, instance))] -async fn copy_rendered_screenshot_to_clipboard( - clipboard: MutableState<'_, ClipboardContext>, - instance: WindowScreenshotEditorInstance, -) -> Result<(), String> { - let data = render_screenshot_png(&instance).await?; - - let img_data = clipboard_rs::RustImageData::from_bytes(&data) - .map_err(|e| format!("Failed to create image data from bytes: {e}"))?; - clipboard - .write() - .await - .set_image(img_data) - .map_err(|err| format!("Failed to copy image to clipboard: {err}"))?; - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(_app))] -async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { - let path_str = path.to_str().ok_or("Invalid path")?; - let is_dir = path.is_dir(); - - #[cfg(target_os = "windows")] - { - if is_dir { - Command::new("explorer") - .arg(path_str) - .spawn() - .map_err(|e| format!("Failed to open folder: {e}"))?; - } else { - Command::new("explorer") - .args(["/select,", path_str]) - .spawn() - .map_err(|e| format!("Failed to open folder: {e}"))?; - } - } - - #[cfg(target_os = "macos")] - { - if is_dir { - Command::new("open") - .arg(path_str) - .spawn() - .map_err(|e| format!("Failed to open folder: {e}"))?; - } else { - Command::new("open") - .arg("-R") - .arg(path_str) - .spawn() - .map_err(|e| format!("Failed to open folder: {e}"))?; - } - } - - #[cfg(target_os = "linux")] - { - if is_dir { - Command::new("xdg-open") - .arg(path_str) - .spawn() - .map_err(|e| format!("Failed to open folder: {e}"))?; - } else { - Command::new("xdg-open") - .arg( - path.parent() - .ok_or("Invalid path")? - .to_str() - .ok_or("Invalid path")?, - ) - .spawn() - .map_err(|e| format!("Failed to open folder: {e}"))?; - } - } - - Ok(()) -} - -/// The opener's Linux backend makes blocking zbus calls, which panic inside an async task. -pub(crate) async fn reveal_in_dir(app: AppHandle, path: PathBuf) -> Result<(), String> { - cap_utils::run_on_dedicated_thread("reveal-item-in-dir", move || { - app.opener().reveal_item_in_dir(path) - }) - .await - .map_err(|e| e.to_string())? - .map_err(|e| e.to_string()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn reveal_item_in_dir(app: AppHandle, path: PathBuf) -> Result<(), String> { - reveal_in_dir(app, path) - .await - .map_err(|e| format!("Failed to reveal item: {e}")) -} - -#[derive(Deserialize, specta::Type, tauri_specta::Event, Debug, Clone)] -struct RenderFrameEvent { - frame_number: u32, - fps: u32, - resolution_base: XY, -} - -#[derive(Serialize, specta::Type, tauri_specta::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)] -pub(crate) struct FrameLayoutEvent { - display: [f32; 4], - camera: Option<[f32; 4]>, - output_width: u32, - output_height: u32, -} - -impl From for FrameLayoutEvent { - fn from(layout: cap_editor::FrameLayout) -> Self { - Self { - display: layout.display, - camera: layout.camera, - output_width: layout.output_size[0], - output_height: layout.output_size[1], - } - } -} - -impl EditorStateChanged { - fn new(s: &EditorState) -> Self { - Self { - playhead_position: s.playhead_position, - } - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn start_playback( - editor_instance: WindowEditorInstance, - fps: u32, - resolution_base: XY, -) -> Result<(), String> { - editor_instance.start_playback(fps, resolution_base).await; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn stop_playback(editor_instance: WindowEditorInstance) -> Result<(), String> { - let mut state = editor_instance.state.lock().await; - - if let Some(handle) = state.playback_task.take() { - handle.stop(); - } - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -async fn commit_editor_preparing_frame( - editor_instance: WindowEditorInstance, - instance_id: String, - frame_number: u32, - fps: u32, -) -> Result { - editor_instance.commit_preparing_frame(&instance_id, frame_number, fps) -} - -#[tauri::command] -#[specta::specta] -async fn start_editor_handoff_playback( - editor_instance: WindowEditorInstance, - instance_id: String, - frame_number: u32, - fps: u32, - resolution_base: XY, -) -> Result { - editor_instance - .start_handoff_playback(&instance_id, frame_number, fps, resolution_base) - .await -} - -#[tauri::command] -#[specta::specta] -async fn stop_editor_handoff_playback( - editor_instance: WindowEditorInstance, - instance_id: String, - playback_id: String, -) -> Result<(), String> { - editor_instance - .stop_handoff_playback(&instance_id, &playback_id) - .await -} - -#[derive(Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -struct SerializedEditorInstance { - instance_id: String, - preparing_playback: bool, - frames_socket_url: String, - preparing_snapshot: Option, - recording_duration: f64, - saved_project_config: ProjectConfiguration, - recordings: Arc, - path: PathBuf, - /// Notch geometry the overlay uses when the project sets no manual - /// placement: this recording's own measurements where the recorder took - /// them, else a stock MacBook notch for the editor to start from. - notch_base: cap_project::DisplayNotch, -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window))] -async fn create_editor_instance(window: Window) -> Result { - let CapWindowId::Editor { id } = - CapWindowId::from_str(window.label()).map_err(|e| e.to_string())? - else { - return Err("Invalid window".to_string()); - }; - - let path = { - let window_ids = EditorWindowIds::get(window.app_handle()); - let window_ids = window_ids.ids.lock().unwrap(); - - let Some((path, _)) = window_ids.iter().find(|(_, _id)| *_id == id) else { - return Err("Editor instance not found".to_string()); - }; - path.clone() - }; - - let editor_instance = EditorInstances::get_or_create(&window, path).await?; - - Ok(SerializedEditorInstance { - instance_id: editor_instance.instance_id.to_string(), - preparing_playback: editor_instance.preparing_adoption().is_some(), - preparing_snapshot: window - .state::() - .snapshot_for_window(id), - frames_socket_url: format!("ws://localhost:{}", editor_instance.ws_port), - recording_duration: editor_instance.recordings.duration(), - saved_project_config: { - let project_config = editor_instance.project_config.1.borrow(); - project_config.clone() - }, - recordings: editor_instance.recordings.clone(), - path: editor_instance.project_path.clone(), - notch_base: editor_instance - .render_constants - .meta - .display_notch() - .unwrap_or(cap_project::DEFAULT_MACBOOK_NOTCH), - }) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window))] -async fn get_editor_project_path(window: Window) -> Result { - let CapWindowId::Editor { id } = - CapWindowId::from_str(window.label()).map_err(|e| e.to_string())? - else { - return Err("Invalid window".to_string()); - }; - - let window_ids = EditorWindowIds::get(window.app_handle()); - let window_ids = window_ids.ids.lock().unwrap(); - - let Some((path, _)) = window_ids.iter().find(|(_, _id)| *_id == id) else { - return Err("Editor instance not found".to_string()); - }; - - Ok(path.clone()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor))] -async fn get_editor_meta(editor: WindowEditorInstance) -> Result { - let path = editor.project_path.clone(); - RecordingMeta::load_for_project(&path).map_err(|e| e.to_string()) -} - -#[tauri::command] -#[specta::specta] -async fn get_recording_meta_by_path(project_path: PathBuf) -> Result { - RecordingMeta::load_for_project(&project_path).map_err(|e| e.to_string()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor))] -async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> Result<(), String> { - let mut meta = editor.meta().clone(); - meta.pretty_name = pretty_name; - meta.save_for_project().map_err(|e| e.to_string()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, clipboard))] -async fn copy_video_to_clipboard( - app: AppHandle, - clipboard: MutableState<'_, ClipboardContext>, - path: String, -) -> Result<(), String> { - println!("copying"); - let _ = clipboard.write().await.set_files(vec![path]); - - notifications::send_notification( - &app, - notifications::NotificationType::VideoCopiedToClipboard, - ); - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument] -async fn get_video_metadata(path: PathBuf) -> Result { - let recording_meta = RecordingMeta::load_for_project(&path).map_err(|v| v.to_string())?; - - fn get_duration_for_path(path: PathBuf) -> Result { - let input = - ffmpeg::format::input(&path).map_err(|e| format!("Failed to open video file: {e}"))?; - - let raw_duration = input.duration(); - if raw_duration <= 0 { - return Err(format!( - "Unknown or invalid duration for video file: {path:?}" - )); - } - - let duration = raw_duration as f64 / AV_TIME_BASE as f64; - Ok(duration) - } - - let display_paths = match &recording_meta.inner { - RecordingMetaInner::Instant(_) => { - vec![path.join("content/output.mp4")] - } - RecordingMetaInner::Studio(meta) => { - let status = meta.status(); - if let StudioRecordingStatus::Failed { .. } = status { - return Err("Unable to get metadata on failed recording".to_string()); - } else if let StudioRecordingStatus::InProgress = status { - return Err("Unable to get metadata on in-progress recording".to_string()); - } - - match &**meta { - StudioRecordingMeta::SingleSegment { segment } => { - vec![recording_meta.path(&segment.display.path)] - } - StudioRecordingMeta::MultipleSegments { inner } => inner - .segments - .iter() - .map(|s| recording_meta.path(&s.display.path)) - .collect(), - } - } - }; - - let duration = display_paths - .into_iter() - .map(get_duration_for_path) - .try_fold(0f64, |acc, item| -> Result { - let d = item?; - Ok(acc + d) - })?; - - let (width, height) = (1920, 1080); - let fps = 30; - - let base_bitrate = if width <= 1280 && height <= 720 { - 4_000_000.0 - } else if width <= 1920 && height <= 1080 { - 8_000_000.0 - } else if width <= 2560 && height <= 1440 { - 14_000_000.0 - } else { - 20_000_000.0 - }; - - let fps_factor = (fps as f64) / 30.0; - let video_bitrate = base_bitrate * fps_factor; - let audio_bitrate = 192_000.0; - let total_bitrate = video_bitrate + audio_bitrate; - let estimated_size_mb = (total_bitrate * duration) / (8.0 * 1024.0 * 1024.0); - - Ok(VideoRecordingMetadata { - size: estimated_size_mb, - duration, - }) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -fn close_recordings_overlay_window(app: AppHandle) { - #[cfg(target_os = "macos")] - { - let app_for_close = app.clone(); - app.run_on_main_thread(move || { - use tauri_nspanel::ManagerExt; - if let Ok(panel) = - app_for_close.get_webview_panel(&CapWindowId::RecordingsOverlay.label()) - { - panel.released_when_closed(false); - panel.close(); - } - }) - .ok(); - } - - if !cfg!(target_os = "macos") - && let Some(window) = CapWindowId::RecordingsOverlay.get(&app) - { - let _ = window.close(); - } -} - -#[tauri::command(async)] -#[specta::specta] -#[instrument(skip(_app))] -fn focus_captures_panel(_app: AppHandle) { - #[cfg(target_os = "macos")] - { - let app_for_focus = _app.clone(); - _app.run_on_main_thread(move || { - use tauri_nspanel::ManagerExt; - if let Ok(panel) = - app_for_focus.get_webview_panel(&CapWindowId::RecordingsOverlay.label()) - { - panel.make_key_window(); - } - }) - .ok(); - } -} - -#[derive(Serialize, Deserialize, specta::Type, Clone)] -#[serde(tag = "type", rename_all = "camelCase")] -pub struct FramesRendered { - rendered_count: u32, - total_frames: u32, -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn set_playhead_position( - editor_instance: WindowEditorInstance, - frame_number: u32, -) -> Result<(), String> { - editor_instance - .modify_and_emit_state(|state| { - state.playhead_position = frame_number; - }) - .await; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn set_project_config( - editor_instance: WindowEditorInstance, - config: ProjectConfiguration, -) -> Result<(), String> { - editor_instance.project_config.0.send(config.clone()).ok(); - - config - .write(&editor_instance.project_path) - .map_err(|error| format!("Failed to write project config: {error}"))?; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn update_project_config_in_memory( - editor_instance: WindowEditorInstance, - config: ProjectConfiguration, - frame_number: Option, - fps: Option, - resolution_base: Option>, -) -> Result<(), String> { - editor_instance.project_config.0.send(config).ok(); - if let (Some(frame), Some(f), Some(res)) = (frame_number, fps, resolution_base) { - editor_instance.preview_tx.send_modify(|v| { - *v = Some((frame, f, res)); - }); - } - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, editor_instance))] -async fn generate_zoom_segments_from_clicks( - app: AppHandle, - editor_instance: WindowEditorInstance, -) -> Result, String> { - let meta = editor_instance.meta(); - let recordings = &editor_instance.recordings; - - let zoom_amount = GeneralSettingsStore::get(&app) - .ok() - .flatten() - .and_then(|settings| settings.default_zoom_amount) - .unwrap_or(recording::DEFAULT_AUTO_ZOOM_AMOUNT); - - let zoom_segments = - recording::generate_zoom_segments_for_project(meta, recordings, zoom_amount); - - Ok(zoom_segments) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn generate_keyboard_segments( - editor_instance: WindowEditorInstance, - grouping_threshold_ms: f64, - linger_duration_ms: f64, - show_modifiers: bool, - show_special_keys: bool, -) -> Result, String> { - let project = editor_instance.project_config.1.borrow().clone(); - let Some(timeline) = project.timeline else { - return Ok(Vec::new()); - }; - let settings = cap_project::KeyboardSettings { - grouping_threshold_ms, - linger_duration: (linger_duration_ms / 1000.0) as f32, - show_modifiers, - show_special_keys, - ..Default::default() - }; - let meta = editor_instance.meta().clone(); - tokio::task::spawn_blocking(move || { - cap_project::generate_project_keyboard_segments(&meta, &timeline, &settings) - }) - .await - .map_err(|error| format!("Keyboard generation failed: {error}"))? -} - -#[tauri::command] -#[specta::specta] -#[instrument] -async fn list_audio_devices() -> Result, ()> { - if !permissions::do_permissions_check(false) - .microphone - .permitted() - { - return Ok(vec![]); - } - - Ok(MicrophoneFeed::list_names()) -} - -#[tauri::command] -#[specta::specta] -#[instrument] -async fn list_system_fonts() -> Vec { - tokio::task::spawn_blocking(cap_rendering::system_font_families) - .await - .unwrap_or_default() -} - -#[derive(Serialize, Type, Debug, Clone)] -pub struct UploadProgress { - progress: f64, -} - -#[derive(Debug, Deserialize, Type)] -pub enum UploadMode { - Initial { - pre_created_video: Option, - }, - Reupload, -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, channel))] -async fn upload_exported_video( - app: AppHandle, - path: PathBuf, - mode: UploadMode, - channel: Channel, - organization_id: Option, -) -> Result { - let Ok(Some(auth)) = AuthStore::get(&app) else { - AuthStore::set(&app, None).map_err(|e| e.to_string())?; - return Ok(UploadResult::NotAuthenticated); - }; - - let mut meta = RecordingMeta::load_for_project(&path).map_err(|v| v.to_string())?; - - if matches!(meta.inner, RecordingMetaInner::Instant(_)) { - match upload::lifecycle::retry_existing(app.clone(), &path).await { - Ok(Some(link)) => return Ok(UploadResult::Success(link)), - Ok(None) => {} - Err(AuthedApiError::InvalidAuthentication) => { - return Ok(UploadResult::NotAuthenticated); - } - Err(error) => return Err(error.to_string()), - } - } - - let file_path = meta.output_path(); - if !file_path.exists() { - notifications::send_notification(&app, notifications::NotificationType::UploadFailed); - return Err("Failed to upload video: Rendered video not found".to_string()); - } - - let metadata = build_video_meta(&file_path) - .map_err(|err| format!("Error getting output video meta: {err}"))?; - - if !auth.is_upgraded() && metadata.duration_in_secs > 300.0 { - return Ok(UploadResult::UpgradeRequired); - } - - channel.send(UploadProgress { progress: 0.0 }).ok(); - - let existing_video_id = upload::reusable_video_id(meta.sharing.as_ref(), meta.upload.as_ref()); - let (s3_config, created_link) = match async { - let video_id = match mode { - UploadMode::Initial { pre_created_video } => { - if let Some(video_id) = existing_video_id.clone() { - Some(video_id) - } else if let Some(pre_created) = pre_created_video { - return Ok((pre_created.config, Some(pre_created.link))); - } else { - None - } - } - UploadMode::Reupload => { - let Some(sharing) = meta.sharing.clone() else { - return Err("No sharing metadata found".into()); - }; - - Some(sharing.id) - } - }; - - let created = create_or_get_video( - &app, - false, - video_id, - Some(meta.pretty_name.clone()), - Some(metadata.clone()), - organization_id, - ) - .await?; - Ok((created.config, created.share_url)) - } - .await - { - Ok(data) => data, - Err(AuthedApiError::InvalidAuthentication) => return Ok(UploadResult::NotAuthenticated), - Err(AuthedApiError::UpgradeRequired) => return Ok(UploadResult::UpgradeRequired), - Err(err) => return Err(err.to_string()), - }; - - if existing_video_id - .as_ref() - .is_some_and(|video_id| *video_id != s3_config.id) - { - return Err("Server did not preserve the existing share link".into()); - } - - let screenshot_path = meta.project_path.join("screenshots/display.jpg"); - meta.upload = Some(UploadMeta::SinglePartUpload { - video_id: s3_config.id.clone(), - file_path: file_path.clone(), - screenshot_path: screenshot_path.clone(), - recording_dir: path.clone(), - }); - meta.save_for_project() - .map_err(|error| format!("Failed to persist upload state: {error}"))?; - - match upload_video( - &app, - s3_config.id.clone(), - file_path, - screenshot_path, - meta.sharing.is_some(), - Some(channel.clone()), - ) - .await - { - Ok(uploaded_video) => { - channel.send(UploadProgress { progress: 1.0 }).ok(); - - let link = meta - .sharing - .as_ref() - .map(|sharing| sharing.link.clone()) - .or(created_link) - .unwrap_or(uploaded_video.link); - - meta.upload = Some(UploadMeta::Complete); - meta.sharing = Some(SharingMeta { - link: link.clone(), - id: uploaded_video.id.clone(), - content_hash: None, - }); - meta.save_for_project() - .map_err(|error| format!("Failed to persist sharing state: {error}"))?; - - let _ = app - .state::>() - .write() - .await - .set_text(link.clone()); - - NotificationType::ShareableLinkCopied.send(&app); - Ok(UploadResult::Success(link)) - } - Err(AuthedApiError::UpgradeRequired) => Ok(UploadResult::UpgradeRequired), - Err(e) => { - error!("Failed to upload video: {e}"); - - NotificationType::UploadFailed.send(&app); - - meta.upload = Some(UploadMeta::Failed { - error: e.to_string(), - }); - meta.save_for_project() - .map_err(|e| error!("Failed to save recording meta: {e}")) - .ok(); - - Err(e.to_string()) - } - } -} - -fn screenshot_project_path_from_path(path: &std::path::Path) -> Option { - path.ancestors() - .find(|ancestor| ancestor.extension().and_then(|s| s.to_str()) == Some("cap")) - .map(std::path::Path::to_path_buf) -} - -fn load_screenshot_project_meta( - path: &std::path::Path, -) -> Result<(PathBuf, RecordingMeta), String> { - let project_path = screenshot_project_path_from_path(path) - .ok_or_else(|| format!("Could not find screenshot project for {}", path.display()))?; - let meta = RecordingMeta::load_for_project(&project_path) - .map_err(|err| format!("Failed to load screenshot metadata: {err}"))?; - Ok((project_path, meta)) -} - -fn save_screenshot_sharing( - project_path: &std::path::Path, - mut meta: RecordingMeta, - uploaded: &upload::UploadedItem, - content_hash: Option, -) -> Result<(), String> { - meta.sharing = Some(SharingMeta { - link: uploaded.link.clone(), - id: uploaded.id.clone(), - content_hash, - }); - meta.save_for_project() - .map_err(|err| format!("Error saving project {}: {err}", project_path.display())) -} - -#[derive(Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -struct ScreenshotSharingState { - link: String, - content_hash: Option, -} - -#[derive(Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -struct ScreenshotProjectShareState { - config: ProjectConfiguration, - sharing: Option, -} - -async fn copy_screenshot_share_link( - clipboard: &MutableState<'_, ClipboardContext>, - link: String, - app: &AppHandle, -) -> Result<(), String> { - let _ = clipboard.write().await.set_text(link); - notifications::send_notification(app, notifications::NotificationType::ShareableLinkCopied); - Ok(()) -} - -fn screenshot_share_link_for_hash( - sharing: Option<&SharingMeta>, - content_hash: &str, -) -> Option { - let sharing = sharing?; - if sharing.content_hash.as_deref() == Some(content_hash) { - return Some(sharing.link.clone()); - } - None -} - -async fn upgrade_required_result(app: &AppHandle) -> UploadResult { - if let Err(error) = open_pricing_page(app).await { - warn!(%error, "Failed to open pricing page"); - } - UploadResult::UpgradeRequired -} - -#[tauri::command] -#[specta::specta] -fn get_screenshot_project_share_state( - path: PathBuf, -) -> Result { - let (project_path, meta) = load_screenshot_project_meta(&path)?; - let config = ProjectConfiguration::load(&project_path) - .map_err(|err| format!("Failed to load screenshot config: {err}"))?; - let sharing = meta.sharing.map(|sharing| ScreenshotSharingState { - link: sharing.link, - content_hash: sharing.content_hash, - }); - - Ok(ScreenshotProjectShareState { config, sharing }) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, clipboard))] -async fn copy_current_screenshot_share_link( - app: AppHandle, - clipboard: MutableState<'_, ClipboardContext>, - project_path: PathBuf, - content_hash: String, -) -> Result, String> { - let (_, meta) = load_screenshot_project_meta(&project_path)?; - let Some(link) = screenshot_share_link_for_hash(meta.sharing.as_ref(), &content_hash) else { - return Ok(None); - }; - - copy_screenshot_share_link(&clipboard, link.clone(), &app).await?; - Ok(Some(UploadResult::Success(link))) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, clipboard))] -async fn upload_screenshot( - app: AppHandle, - clipboard: MutableState<'_, ClipboardContext>, - screenshot_path: PathBuf, -) -> Result { - let Ok(Some(auth)) = AuthStore::get(&app) else { - AuthStore::set(&app, None).map_err(|e| e.to_string())?; - return Ok(UploadResult::NotAuthenticated); - }; - - if !auth.is_upgraded() { - return Ok(upgrade_required_result(&app).await); - } - - println!("Uploading screenshot: {screenshot_path:?}"); - - let (project_path, meta) = load_screenshot_project_meta(&screenshot_path)?; - if let Some(sharing) = meta.sharing.as_ref() { - copy_screenshot_share_link(&clipboard, sharing.link.clone(), &app).await?; - return Ok(UploadResult::Success(sharing.link.clone())); - } - - let uploaded = match upload_screenshot_file(&app, screenshot_path.clone(), None, None).await { - Ok(uploaded) => uploaded, - Err(AuthedApiError::InvalidAuthentication) => return Ok(UploadResult::NotAuthenticated), - Err(AuthedApiError::UpgradeRequired) => return Ok(upgrade_required_result(&app).await), - Err(e) => return Err(e.to_string()), - }; - save_screenshot_sharing(&project_path, meta, &uploaded, None)?; - - println!("Copying to clipboard: {:?}", uploaded.link); - - copy_screenshot_share_link(&clipboard, uploaded.link.clone(), &app).await?; - - Ok(UploadResult::Success(uploaded.link)) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, clipboard, image_bytes))] -async fn upload_rendered_screenshot( - app: AppHandle, - clipboard: MutableState<'_, ClipboardContext>, - image_bytes: Vec, - content_type: String, - project_path: PathBuf, - content_hash: Option, -) -> Result { - let Ok(Some(auth)) = AuthStore::get(&app) else { - AuthStore::set(&app, None).map_err(|e| e.to_string())?; - return Ok(UploadResult::NotAuthenticated); - }; - - if !auth.is_upgraded() { - return Ok(upgrade_required_result(&app).await); - } - - let (project_path, meta) = load_screenshot_project_meta(&project_path)?; - let existing_video_id = meta.sharing.as_ref().map(|sharing| sharing.id.clone()); - let uploaded = - match upload_screenshot_bytes(&app, image_bytes, &content_type, existing_video_id, None) - .await - { - Ok(uploaded) => uploaded, - Err(AuthedApiError::InvalidAuthentication) => { - return Ok(UploadResult::NotAuthenticated); - } - Err(AuthedApiError::UpgradeRequired) => return Ok(upgrade_required_result(&app).await), - Err(e) => return Err(e.to_string()), - }; - save_screenshot_sharing(&project_path, meta, &uploaded, content_hash)?; - - copy_screenshot_share_link(&clipboard, uploaded.link.clone(), &app).await?; - - Ok(UploadResult::Success(uploaded.link)) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window))] -async fn save_file_dialog( - window: tauri::Window, - file_name: String, - file_type: String, -) -> Result, String> { - run_command_safely( - "save_file_dialog", - save_file_dialog_inner(window, file_name, file_type), - ) - .await -} - -async fn save_file_dialog_inner( - window: tauri::Window, - file_name: String, - file_type: String, -) -> Result, String> { - info!(file_name, file_type, "Save file dialog requested"); - - let file_name = file_name - .strip_suffix(".cap") - .unwrap_or(&file_name) - .to_string(); - - let (name, extension) = match file_type.as_str() { - "recording" | "mp4" => ("MP4 Video", "mp4"), - "gif" => ("GIF Image", "gif"), - "mov" => ("MOV Video", "mov"), - "screenshot" | "png" => ("PNG Image", "png"), - "srt" => ("SubRip Subtitle", "srt"), - "vtt" => ("WebVTT", "vtt"), - _ => { - warn!(file_type, "Invalid save file dialog type"); - return Err("Invalid file type".to_string()); - } - }; - - info!(file_name, name, extension, "Showing save file dialog"); - - #[cfg(target_os = "linux")] - let path = export::show_linux_save_dialog( - window.clone(), - tokio_util::sync::CancellationToken::new(), - file_name, - name, - extension, - ) - .await?; - #[cfg(target_os = "macos")] - let path = export::show_macos_save_dialog(window.app_handle(), file_name, extension).await?; - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - let path = { - use tauri_plugin_dialog::DialogExt; - - let app = window.app_handle().clone(); - // 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(std::path::PathBuf::from), - ); - }); - - match rx.await { - Ok(result) => { - info!(path = ?result, "Save file dialog completed"); - result - } - Err(e) => { - warn!(error = %e, "Save file dialog failed"); - notifications::send_notification( - &app, - notifications::NotificationType::VideoSaveFailed, - ); - return Err(e.to_string()); - } - } - }; - if let Some(path) = &path { - use tauri_plugin_fs::FsExt; - if let Some(scope) = window.try_fs_scope() { - scope.allow_file(path).map_err(|error| error.to_string())?; - } - window - .state::() - .allow_file(path) - .map_err(|error| error.to_string())?; - } - Ok(path.map(|path| path.to_string_lossy().into_owned())) -} - -#[derive(Serialize, specta::Type)] -pub struct RecordingMetaWithMetadata { - #[serde(flatten)] - pub inner: RecordingMeta, - // Easier accessors for within webview - // THESE MUST COME AFTER `inner` to override flattened fields with the same name - pub mode: RecordingMode, - pub status: StudioRecordingStatus, - // Number of recorded takes (segments) the recording is made up of. - pub clip_count: u32, - pub sort_time_millis: f64, -} - -impl RecordingMetaWithMetadata { - fn new(inner: RecordingMeta, sort_time_millis: f64) -> Self { - Self { - mode: match &inner.inner { - RecordingMetaInner::Studio(_) => RecordingMode::Studio, - RecordingMetaInner::Instant(_) => RecordingMode::Instant, - }, - clip_count: match &inner.inner { - RecordingMetaInner::Studio(meta) => match &**meta { - StudioRecordingMeta::MultipleSegments { inner } => inner.segments.len() as u32, - StudioRecordingMeta::SingleSegment { .. } => 1, - }, - RecordingMetaInner::Instant(_) => 1, - }, - status: match &inner.inner { - RecordingMetaInner::Studio(meta) => match &**meta { - StudioRecordingMeta::MultipleSegments { inner } => inner - .status - .clone() - .unwrap_or(StudioRecordingStatus::Complete), - StudioRecordingMeta::SingleSegment { .. } => StudioRecordingStatus::Complete, - }, - RecordingMetaInner::Instant(InstantRecordingMeta::InProgress { .. }) => { - StudioRecordingStatus::InProgress - } - RecordingMetaInner::Instant(InstantRecordingMeta::Failed { error }) => { - StudioRecordingStatus::Failed { - error: error.clone(), - } - } - RecordingMetaInner::Instant(InstantRecordingMeta::Complete { .. }) => { - StudioRecordingStatus::Complete - } - }, - sort_time_millis, - inner, - } - } -} - -#[derive(Serialize, specta::Type)] -pub struct ScreenshotMetaWithMetadata { - #[serde(flatten)] - pub inner: RecordingMeta, - pub sort_time_millis: f64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum FileType { - Recording, - Screenshot, -} - -#[tauri::command(async)] -#[specta::specta] -#[instrument] -fn get_recording_meta( - path: PathBuf, - _file_type: FileType, -) -> Result { - let sort_time_millis = media_sort_time_millis(&path); - RecordingMeta::load_for_project(&path) - .map(|meta| RecordingMetaWithMetadata::new(meta, sort_time_millis)) - .map_err(|e| format!("Failed to load recording meta: {e}")) -} - -fn media_sort_time(path: &Path) -> SystemTime { - let Ok(metadata) = path.metadata() else { - return UNIX_EPOCH; - }; - - metadata - .created() - .or_else(|_| metadata.modified()) - .unwrap_or(UNIX_EPOCH) -} - -fn media_sort_time_millis(path: &Path) -> f64 { - media_sort_time(path) - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs_f64() - * 1000.0 -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn list_recordings( - app: AppHandle, -) -> Result, String> { - tokio::task::spawn_blocking(move || list_recordings_inner(&app, usize::MAX)) - .await - .map_err(|error| error.to_string()) -} - -#[tauri::command] -#[specta::specta] -async fn list_recent_recordings( - app: AppHandle, -) -> Result, String> { - tokio::task::spawn_blocking(move || list_recordings_inner(&app, 9)) - .await - .map_err(|error| error.to_string()) -} - -fn newest_valid_media( - mut candidates: Vec<(T, f64)>, - limit: usize, - mut load: impl FnMut(T, f64) -> Option, -) -> Vec { - candidates.sort_by(|(_, a), (_, b)| b.total_cmp(a)); - candidates - .into_iter() - .filter_map(|(candidate, timestamp)| load(candidate, timestamp)) - .take(limit) - .collect() -} - -fn list_recordings_inner( - app: &AppHandle, - limit: usize, -) -> Vec<(PathBuf, RecordingMetaWithMetadata)> { - // Recordings can live in multiple folders (the active one, the default - // one, and any previously used custom folders) — scan them all so - // switching the storage folder never hides existing recordings. - let mut result = Vec::new(); - for recordings_dir in recordings_locations::known_recordings_dirs(app) { - let Ok(entries) = std::fs::read_dir(&recordings_dir) else { - continue; - }; - - for entry in entries.flatten() { - let path = entry.path(); - - if !path.is_dir() { - continue; - } - - let timestamp = media_sort_time_millis(&path); - result.push((path, timestamp)); - } - } - - newest_valid_media(result, limit, |path, timestamp| { - let meta = RecordingMeta::load_for_project(&path).ok()?; - Some((path, RecordingMetaWithMetadata::new(meta, timestamp))) - }) -} - -fn acquire_recording_delete_lock( - path: &std::path::Path, -) -> Result { - cap_recording::upload_resume::UploadLock::acquire(path).map_err(|error| error.to_string()) -} - -fn recording_delete_target( - recordings_dirs: &[PathBuf], - path: &Path, -) -> Result, String> { - if path - .components() - .any(|component| matches!(component, std::path::Component::ParentDir)) - { - return Err("Invalid path".to_string()); - } - - let canonical_dirs: Vec<_> = recordings_dirs - .iter() - .filter_map(|dir| dir.canonicalize().ok()) - .collect(); - if recordings_dirs - .iter() - .chain(&canonical_dirs) - .any(|dir| path == dir) - { - return Err("Path is not inside a recordings directory".to_string()); - } - - if path - .try_exists() - .map_err(|error| format!("Failed to inspect recording path: {error}"))? - { - let canonical_path = path - .canonicalize() - .map_err(|error| format!("Failed to resolve recording path: {error}"))?; - if canonical_dirs.iter().any(|dir| &canonical_path == dir) - || !canonical_dirs - .iter() - .any(|dir| canonical_path.starts_with(dir)) - { - return Err("Path is not inside a recordings directory".to_string()); - } - Ok(Some(canonical_path)) - } else if recordings_dirs - .iter() - .chain(&canonical_dirs) - .any(|dir| path.starts_with(dir)) - { - Ok(None) - } else { - Err("Path is not inside a recordings directory".to_string()) - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn delete_recording_directory(app: AppHandle, path: PathBuf) -> Result<(), String> { - // The library lists recordings from every known storage folder, so - // deletion must accept the same set — but nothing outside it. - let recordings_dirs = recordings_locations::known_recordings_dirs(&app); - - if let Some(canonical_path) = recording_delete_target(&recordings_dirs, &path)? { - upload::lifecycle::cancel(&canonical_path).await; - let ownership = acquire_recording_delete_lock(&canonical_path)?; - upload::lifecycle::mark_cancelled(&canonical_path).map_err(|error| error.to_string())?; - std::fs::remove_dir_all(&canonical_path) - .map_err(|error| format!("Failed to delete recording: {error}"))?; - drop(ownership); - } - - let _ = RecordingDeleted { path }.emit(&app); - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn list_screenshots( - app: AppHandle, -) -> Result, String> { - tokio::task::spawn_blocking(move || list_screenshots_inner(&app, usize::MAX)) - .await - .map_err(|error| error.to_string())? -} - -#[tauri::command] -#[specta::specta] -async fn list_recent_screenshots( - app: AppHandle, -) -> Result, String> { - tokio::task::spawn_blocking(move || list_screenshots_inner(&app, 9)) - .await - .map_err(|error| error.to_string())? -} - -fn list_screenshots_inner( - app: &AppHandle, - limit: usize, -) -> Result, String> { - let screenshots_dir = screenshots_path(app); - - let result = std::fs::read_dir(&screenshots_dir) - .map_err(|e| format!("Failed to read screenshots directory: {e}"))? - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - if path.is_dir() && path.extension().and_then(|s| s.to_str()) == Some("cap") { - let png_path = std::fs::read_dir(&path) - .ok()? - .filter_map(|e| e.ok()) - .find(|e| e.path().extension().and_then(|s| s.to_str()) == Some("png")) - .map(|e| e.path())?; - - let sort_time_millis = media_sort_time_millis(&png_path); - Some(((path, png_path), sort_time_millis)) - } else { - None - } - }) - .collect::>(); - - Ok(newest_valid_media( - result, - limit, - |(path, png_path), sort_time_millis| { - let inner = RecordingMeta::load_for_project(&path).ok()?; - Some(( - png_path, - ScreenshotMetaWithMetadata { - inner, - sort_time_millis, - }, - )) - }, - )) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn check_upgraded_and_update(app: AppHandle) -> Result { - println!("Checking upgraded status and updating..."); - - if let Ok(Some(settings)) = GeneralSettingsStore::get(&app) - && settings.commercial_license.is_some() - { - return Ok(true); - } - - let Ok(Some(auth)) = AuthStore::get(&app) else { - return Ok(false); - }; - - if let Some(ref plan) = auth.plan - && plan.manual - { - return Ok(true); - } - - println!("Fetching plan"); - let response = app - .authed_api_request("/api/desktop/plan", |client, url| client.get(url)) - .await - .map_err(|e| { - println!("Failed to fetch plan: {e}"); - e.to_string() - })?; - - println!("Plan fetch response status: {}", response.status()); - let plan_data = response.json::().await.map_err(|e| { - println!("Failed to parse plan response: {e}"); - format!("Failed to parse plan response: {e}") - })?; - - let is_pro = plan_data - .get("upgraded") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - println!("Pro status: {is_pro}"); - let updated_auth = AuthStore { - secret: auth.secret, - user_id: auth.user_id, - plan: Some(Plan { - upgraded: is_pro, - manual: auth.plan.map(|p| p.manual).unwrap_or(false), - last_checked: chrono::Utc::now().timestamp() as i32, - }), - organizations: auth.organizations, - organizations_updated_at: auth.organizations_updated_at, - }; - println!("Updating auth store with new pro status"); - AuthStore::set(&app, Some(updated_auth)).map_err(|e| e.to_string())?; - - Ok(is_pro) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -fn open_external_link(app: tauri::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}"))?; - Ok(()) -} - -#[tauri::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(); - - Command::new("tccutil") - .arg("reset") - .arg("Camera") - .arg(bundle_id) - .output() - .map_err(|_| "Failed to reset camera permissions".to_string())?; - } - - Ok(()) -} - -#[tauri::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(); - - Command::new("tccutil") - .arg("reset") - .arg("Microphone") - .arg(bundle_id) - .output() - .map_err(|_| "Failed to reset microphone permissions".to_string())?; - } - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn clear_presets(app: AppHandle) -> Result<(), String> { - presets::PresetsStore::clear(&app) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn is_camera_window_open(app: AppHandle) -> bool { - CapWindowId::Camera - .get(&app) - .map(|w| w.is_visible().unwrap_or(false)) - .unwrap_or(false) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Result<(), String> { - editor_instance - .modify_and_emit_state(|state| { - state.playhead_position = frame_number; - }) - .await; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn get_display_frame_for_cropping( - editor_instance: WindowEditorInstance, - fps: u32, -) -> Result, String> { - use cap_project::ClipOffsets; - use cap_rendering::{PixelFormat, cpu_yuv}; - use image::{ImageEncoder, codecs::jpeg::JpegEncoder}; - use std::io::Cursor; - use std::time::Instant; - - let total_started_at = Instant::now(); - - let frame_number = editor_instance.state.lock().await.playhead_position; - let time_secs = frame_number as f64 / fps as f64; - - let project = editor_instance.project_config.1.borrow().clone(); - let lookup_started_at = Instant::now(); - - let (segment_time, segment) = project - .get_segment_time(time_secs) - .ok_or_else(|| "No segment found for current time".to_string())?; - - let segment_medias = editor_instance - .segment_medias - .get(segment.recording_clip as usize) - .ok_or_else(|| "Segment media not found".to_string())?; - - let clip_offsets = project - .clips - .iter() - .find(|v| v.index == segment.recording_clip) - .map(|v| v.offsets) - .unwrap_or(ClipOffsets::default()); - let lookup_elapsed_ms = lookup_started_at.elapsed().as_secs_f64() * 1000.0; - - let decode_started_at = Instant::now(); - let segment_frames = segment_medias - .decoders - .get_frames(segment_time as f32, false, true, clip_offsets) - .await - .ok_or_else(|| "Failed to get frame".to_string())?; - let decode_elapsed_ms = decode_started_at.elapsed().as_secs_f64() * 1000.0; - - let screen_frame = segment_frames - .screen_frame - .ok_or_else(|| "Failed to get screen frame".to_string())?; - let width = screen_frame.width(); - let height = screen_frame.height(); - - let convert_started_at = Instant::now(); - let rgba_data = match screen_frame.format() { - PixelFormat::Rgba => screen_frame.data().to_vec(), - PixelFormat::Nv12 => { - let y_plane = screen_frame.y_plane().ok_or("Missing Y plane")?; - let uv_plane = screen_frame.uv_plane().ok_or("Missing UV plane")?; - let mut rgba = vec![0u8; (width * height * 4) as usize]; - cpu_yuv::nv12_to_rgba( - y_plane, - uv_plane, - width, - height, - screen_frame.y_stride(), - screen_frame.uv_stride(), - &mut rgba, - ); - rgba - } - PixelFormat::Yuv420p => { - let y_plane = screen_frame.y_plane().ok_or("Missing Y plane")?; - let u_plane = screen_frame.u_plane().ok_or("Missing U plane")?; - let v_plane = screen_frame.v_plane().ok_or("Missing V plane")?; - let mut rgba = vec![0u8; (width * height * 4) as usize]; - cpu_yuv::yuv420p_to_rgba( - y_plane, - u_plane, - v_plane, - width, - height, - screen_frame.y_stride(), - screen_frame.uv_stride(), - &mut rgba, - ); - rgba - } - }; - let convert_elapsed_ms = convert_started_at.elapsed().as_secs_f64() * 1000.0; - - let encode_started_at = Instant::now(); - - // The cropper maps interactions back to full display dimensions, so the - // reference image only needs to be sharp enough to position the crop. A - // downscaled JPEG keeps decode + IPC transfer near-instant even when the - // playhead is deep into a long recording. - const MAX_PREVIEW_DIM: u32 = 1440; - - let rgba_image = image::RgbaImage::from_raw(width, height, rgba_data) - .ok_or_else(|| "Failed to build image buffer from frame".to_string())?; - - let longest_side = width.max(height); - let resized = if longest_side > MAX_PREVIEW_DIM { - let scale = MAX_PREVIEW_DIM as f32 / longest_side as f32; - let target_w = ((width as f32 * scale).round() as u32).max(1); - let target_h = ((height as f32 * scale).round() as u32).max(1); - image::imageops::resize( - &rgba_image, - target_w, - target_h, - image::imageops::FilterType::Triangle, - ) - } else { - rgba_image - }; - - let rgb_image = image::DynamicImage::ImageRgba8(resized).into_rgb8(); - let out_width = rgb_image.width(); - let out_height = rgb_image.height(); - - let mut jpeg_data = Cursor::new(Vec::new()); - JpegEncoder::new_with_quality(&mut jpeg_data, 82) - .write_image( - rgb_image.as_raw(), - out_width, - out_height, - image::ExtendedColorType::Rgb8, - ) - .map_err(|e| format!("Failed to encode JPEG: {e}"))?; - let encode_elapsed_ms = encode_started_at.elapsed().as_secs_f64() * 1000.0; - let total_elapsed_ms = total_started_at.elapsed().as_secs_f64() * 1000.0; - - debug!( - target: "cap_crop_profile", - frame_number = frame_number, - time_secs = time_secs, - segment_time = segment_time, - width = width, - height = height, - out_width = out_width, - out_height = out_height, - lookup_ms = lookup_elapsed_ms, - decode_ms = decode_elapsed_ms, - convert_ms = convert_elapsed_ms, - encode_ms = encode_elapsed_ms, - total_ms = total_elapsed_ms, - "crop frame profile" - ); - - Ok(jpeg_data.into_inner()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result>, String> { - let mut out = Vec::new(); - - for segment in editor_instance.segment_medias.iter() { - // Waits for the background decode; a failed track just renders as an - // empty waveform (playback/export surface the actual error). - match segment.audio.get().await { - Ok(Some(audio)) => out.push(audio::align_waveform_to_playback( - audio::get_waveform(&audio), - segment.audio_timing_repair.mic_offset_secs, - )), - Ok(None) => out.push(Vec::new()), - Err(error) => { - warn!(%error, "Mic audio failed to load; returning empty waveform"); - out.push(Vec::new()); - } - } - } - - Ok(out) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(editor_instance))] -async fn get_system_audio_waveforms( - editor_instance: WindowEditorInstance, -) -> Result>, String> { - let mut out = Vec::new(); - - for segment in editor_instance.segment_medias.iter() { - match segment.system_audio.get().await { - Ok(Some(audio)) => out.push(audio::align_waveform_to_playback( - audio::get_waveform(&audio), - segment.audio_timing_repair.system_audio_offset_secs, - )), - Ok(None) => out.push(Vec::new()), - Err(error) => { - warn!(%error, "System audio failed to load; returning empty waveform"); - out.push(Vec::new()); - } - } - } - - Ok(out) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, editor_instance, window))] -async fn editor_delete_project( - app: tauri::AppHandle, - editor_instance: WindowEditorInstance, - window: tauri::Window, -) -> Result<(), String> { - let _ = window.close(); - - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - - let path = editor_instance.0.project_path.clone(); - drop(editor_instance); - - let _ = tokio::fs::remove_dir_all(&path).await; - - RecordingDeleted { path }.emit(&app).ok(); - - Ok(()) -} - -async fn open_pricing_page(app: &AppHandle) -> Result<(), String> { - app.shell() - .open("https://cap.so/pricing?ref=desktop", None) - .map_err(|e| e.to_string()) -} - -// keep this async otherwise opening windows may hang on windows -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { - if matches!(window, ShowCapWindow::Upgrade) { - return open_pricing_page(&app).await; - } - - if matches!(window, ShowCapWindow::Camera { .. }) { - let operation_lock = app.state::(); - let _operation_guard = operation_lock.lock().await; - window.show(&app).await.map_err(|e| e.to_string())?; - return Ok(()); - } - - window.show(&app).await.map_err(|e| e.to_string())?; - Ok(()) -} - -#[tauri::command(async)] -#[specta::specta] -#[instrument] -fn list_fails() -> Result, ()> { - Ok(cap_fail::get_state()) -} - -#[tauri::command(async)] -#[specta::specta] -#[instrument] -fn set_fail(name: String, value: bool) { - cap_fail::set_fail(&name, value) -} - -async fn check_notification_permissions(app: AppHandle) { - let Ok(Some(settings)) = GeneralSettingsStore::get(&app) else { - return; - }; - - if !settings.enable_notifications { - return; - } - - match app.notification().permission_state() { - Ok(state) if state != PermissionState::Granted => { - info!("Requesting notification permission"); - match app.notification().request_permission() { - Ok(PermissionState::Granted) => { - info!("Notification permission granted"); - } - Ok(_) | Err(_) => { - GeneralSettingsStore::update(&app, |s| { - s.enable_notifications = false; - }) - .ok(); - } - } - } - Ok(_) => { - debug!("Notification permission already granted"); - } - Err(e) => { - error!("Error checking notification permission state: {e}"); - } - } -} - -// fn configure_logging(folder: &PathBuf) -> tracing_appender::non_blocking::WorkerGuard { -// let file_appender = tracing_appender::rolling::daily(folder, "cap-logs.log"); -// let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); - -// let filter = || tracing_subscriber::filter::EnvFilter::builder().parse_lossy("cap-*=TRACE"); - -// tracing_subscriber::registry() -// .with( -// tracing_subscriber::fmt::layer() -// .with_ansi(false) -// .with_target(false) -// .with_writer(non_blocking) -// .with_filter(filter()), -// ) -// .with( -// tracing_subscriber::fmt::layer() -// .with_ansi(true) -// .with_target(false) -// .with_filter(filter()), -// ) -// .init(); - -// _guard -// } - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Result<(), ()> { - let mut app = app.write().await; - telemetry::set_server_url(&server_url); - app.server_url = server_url; - - Ok(()) -} - -#[tauri::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())?; - if let Some(ref path) = result { - general_settings::GeneralSettingsStore::update(&app, |s| { - let previous = s.recordings_path.replace(path.clone()); - recordings_locations::remember_previous_recordings_path(s, previous, Some(path)); - })?; - } - Ok(result) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn reset_recordings_folder(app: AppHandle) -> Result<(), String> { - general_settings::GeneralSettingsStore::update(&app, |s| { - let previous = s.recordings_path.take(); - recordings_locations::remember_previous_recordings_path(s, previous, None); - }) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn set_camera_preview_state( - app: MutableState<'_, App>, - state: CameraPreviewState, -) -> Result<(), String> { - let app_guard = app.read().await; - let state_for_ws = state.clone(); - app_guard - .camera_preview - .set_state(state) - .map_err(|err| format!("Error saving camera window state: {err}"))?; - - let _ = app_guard.camera_preview_state_tx.send(state_for_ws); - drop(app_guard); - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -fn set_camera_window_position(app: AppHandle, x: f64, y: f64) -> Result<(), String> { - if app - .try_state::() - .is_some_and(|guard| guard.should_ignore()) - { - return Ok(()); - } - - GeneralSettingsStore::update(&app, |settings| { - update_camera_window_position_settings(settings, x, y, None); - })?; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument] -fn ignore_camera_window_position( - guard: State<'_, CameraWindowPositionGuard>, - duration_ms: u32, -) -> Result<(), String> { - guard.ignore_for(duration_ms as u64); - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(state))] -async fn refresh_camera_feed(state: MutableState<'_, App>) -> Result<(), String> { - let app = state.read().await; - let camera_feed = app.camera_feed.clone(); - - #[allow(deprecated)] - let camera_ws_sender = app.camera_ws_sender.clone(); - - let camera_preview_sender = app.camera_preview.sender(); - let use_ws_preview = app.camera_preview.is_paused() || camera_preview_sender.is_none(); - - drop(app); - - if use_ws_preview { - if let Some(sender) = camera_preview_sender { - sender - .detach(&camera_feed) - .await - .map_err(|err| format!("error removing native preview sender: {err}"))?; - } - - #[allow(deprecated)] - camera_feed - .ask(feeds::camera::AddSender(camera_ws_sender)) - .await - .map_err(|err| format!("error re-adding camera ws sender: {err}"))?; - } else if let Some(sender) = camera_preview_sender { - #[allow(deprecated)] - camera_feed - .ask(feeds::camera::RemoveSender(camera_ws_sender)) - .await - .map_err(|err| format!("error removing camera ws sender: {err}"))?; - - sender - .attach(&camera_feed) - .await - .map_err(|err| format!("error re-adding camera preview sender: {err}"))?; - } else { - #[allow(deprecated)] - camera_feed - .ask(feeds::camera::AddSender(camera_ws_sender)) - .await - .map_err(|err| format!("error re-adding camera ws sender: {err}"))?; - } - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -async fn destroy_camera_window(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { - let shutdown_rx = { - let mut app_state = state.write().await; - app_state.camera_preview.begin_shutdown() - }; - - if let Some(rx) = shutdown_rx { - let _ = tokio::time::timeout(std::time::Duration::from_millis(500), rx).await; - } - - windows::cleanup_camera_window(&app, None, true, true).await; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result { - let app = app.read().await.camera_feed.clone(); - - let (tx, rx) = oneshot::channel(); - app.tell(feeds::camera::ListenForReady(tx)) - .await - .map_err(|err| format!("error registering ready listener: {err}"))?; - rx.await - .map_err(|err| format!("error receiving ready signal: {err}"))?; - Ok(true) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -async fn update_auth_plan(app: AppHandle) { - AuthStore::update_auth_plan(&app).await.ok(); -} - -pub async fn open_target_picker( - app: &tauri::AppHandle, - target_mode: recording_settings::RecordingTargetMode, -) { - use tauri::Manager; - - hide_main_window(app); - - let state = app.state::(); - let display_id = None; - - let session = match target_select_overlay::open_target_select_overlays_for_session( - app.clone(), - state.inner(), - None, - display_id.clone(), - Some(target_mode), - ) - .await - { - Ok(session) => session, - Err(error) => { - warn!(%error, "Failed to open target picker"); - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(app) - .await; - return; - } - }; - - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - if !state.picker_is_current(session) { - return; - } - - let _ = RequestSetTargetMode { - target_mode: Some(target_mode), - display_id, - } - .emit(app); -} - -type FilteredRegistry = tracing_subscriber::layer::Layered< - tracing_subscriber::filter::FilterFn bool>, - tracing_subscriber::Registry, ->; - -pub type DynLoggingLayer = Box + Send + Sync>; -type LoggingHandle = tracing_subscriber::reload::Handle, FilteredRegistry>; - -#[cfg(debug_assertions)] -pub fn initialize_stop_editor_benchmark( - create_log_directory: impl FnOnce(&std::path::Path, &std::path::Path) -> std::io::Result, -) -> Result, String> { - stop_editor_benchmark::initialize(create_log_directory) -} - -/// Software recovery exists to break GPU-driver crash loops: a process that died -/// while wgpu adapter/device initialisation was in flight. Any other unexpected -/// termination (force-quit, power loss, OS kill, a hung shutdown) says nothing -/// about the GPU, and forcing the WARP software rasterizer would cripple editor -/// playback (seconds-per-frame renders) on perfectly healthy hardware. A previous -/// session that was *already* in software recovery and still died shows software -/// mode isn't saving the machine, so retry hardware rather than chaining recovery -/// launches forever. -#[cfg_attr(not(target_os = "windows"), allow(dead_code))] -fn should_engage_graphics_recovery( - previous_termination: Option, -) -> bool { - previous_termination.is_some_and(|prev| prev.during_gpu_init && !prev.in_graphics_recovery) -} - -#[cfg(target_os = "windows")] -fn configure_windows_graphics_recovery( - previous_termination: Option, -) { - if should_engage_graphics_recovery(previous_termination) { - cap_rendering::set_force_software_wgpu_adapter(true); - crash_sentinel::mark_graphics_recovery(); - warn!( - "Previous Cap session terminated during GPU initialisation; using Windows software graphics recovery mode for this launch" - ); - } else if previous_termination.is_some() { - info!( - "Previous session terminated unexpectedly, but not during first-time GPU initialisation; keeping hardware graphics" - ); - } -} - -#[cfg(not(target_os = "windows"))] -fn configure_windows_graphics_recovery( - _previous_termination: Option, -) { -} - -/// Decides what `camera_blur_disabled_by_crash` should hold for this launch: a -/// death with the blur pipeline active disables blur at the current version -/// (fresh crash evidence beats version optimism), an existing disable carries -/// over within the same app version, and an app update clears it so the new -/// ort/wgpu/driver stack gets one retry. -fn next_camera_blur_disabled_version( - stored: Option<&str>, - previous_termination: Option, - current_version: &str, -) -> Option { - if previous_termination.is_some_and(|prev| prev.blur_active) { - return Some(current_version.to_string()); - } - stored.filter(|v| *v == current_version).map(String::from) -} - -/// Breaks the camera-background-blur crash loop: a native DirectML/driver crash -/// never reaches a panic handler, and the blur toggle is persisted frontend-side, -/// so without this the next camera open repeats the crash forever. Blur init is -/// lazy (first camera frame / editor render), so running this during setup is -/// early enough. Windows-only at runtime (matching graphics recovery) but -/// compiled everywhere so non-Windows builds keep it honest. -fn configure_camera_blur_recovery( - app: &AppHandle, - previous_termination: Option, -) { - if !cfg!(target_os = "windows") { - return; - } - - let current_version = env!("CARGO_PKG_VERSION"); - let stored = GeneralSettingsStore::get(app) - .ok() - .flatten() - .and_then(|settings| settings.camera_blur_disabled_by_crash); - let next = - next_camera_blur_disabled_version(stored.as_deref(), previous_termination, current_version); - - if next != stored { - let value = next.clone(); - if let Err(error) = GeneralSettingsStore::update(app, |settings| { - settings.camera_blur_disabled_by_crash = value; - }) { - warn!(%error, "Failed to persist camera blur crash-recovery state"); - } - } - - if next.is_some() { - cap_camera_effects::set_blur_disabled(true); - crash_sentinel::mark_blur_recovery(); - if stored.is_none() { - error!( - "Previous Cap session died with camera background blur active; disabling blur until the next app update" - ); - } else { - warn!("Camera background blur remains disabled by crash recovery for this launch"); - } - } else if stored.is_some() { - info!( - prev_version = stored.as_deref(), - "App version changed; re-enabling camera background blur for one retry" - ); - } -} - -#[tauri::command] -#[specta::specta] -fn animated_gradient_catalog() -> cap_project::AnimatedGradientCatalog { - cap_project::animated_gradient_catalog() -} - -#[tauri::command] -#[specta::specta] -fn random_animated_gradient() -> cap_project::AnimatedGradientConfig { - cap_project::AnimatedGradientConfig::random() -} - -#[cfg(any(debug_assertions, test))] -fn typescript_exporter() -> specta_typescript::Typescript { - specta_typescript::Typescript::default().formatter(|path| { - let source = std::fs::read_to_string(path)?; - let formatted = source - .lines() - .map(str::trim_end) - .collect::>() - .join("\n"); - std::fs::write(path, format!("{formatted}\n")) - }) -} - -fn specta_builder() -> tauri_specta::Builder { - tauri_specta::Builder::new() - .commands(tauri_specta::collect_commands![ - linux_instant_camera::submit_camera_presentation, - clean_capture::get_clean_capture_state, - clean_capture::reveal_capture_window, - animated_gradient_catalog, - random_animated_gradient, - set_mic_input, - set_camera_input, - set_native_camera_preview_enabled, - gpui_app::gpui_app_available, - gpui_app::switch_to_gpui_app, - recording_settings::set_recording_mode, - upload_logs, - get_system_diagnostics, - diagnostics::run_diagnostic, - diagnostics::upload_diagnostic_report, - diagnostics::reveal_diagnostic_report, - cli::get_cli_install_status, - cli::install_cli, - cli::uninstall_cli, - recording::start_recording, - recording::stop_recording, - recording::pause_recording, - recording::get_recording_pause_state, - 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::get_default_project_config, - 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, - windows::restore_main_window_geometry, - general_settings::get_default_excluded_windows, - list_audio_devices, - list_system_fonts, - 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::cancel_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, - reveal_item_in_dir, - get_video_metadata, - create_editor_instance, - editor_preparing::create_preparing_editor_frame, - editor_preparing::get_preparing_editor_state, - editor_preparing::seek_preparing_editor, - editor_preparing::set_preparing_editor_playing, - editor_preparing::stop_preparing_editor_frame, - 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, - commit_editor_preparing_frame, - start_editor_handoff_playback, - stop_editor_handoff_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_recent_recordings, - list_screenshots, - list_recent_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, - editor_recording::open_editor_recording_main, - editor_recording::cancel_editor_recording_flow, - editor_recording::get_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::target_select_overlay_ready, - target_select_overlay::suspend_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, - restart_app, - updates::updates_channel_changed, - ]) - .events(tauri_specta::collect_events![ - linux_instant_camera::CameraPresentationRequested, - RecordingOptionsChanged, - NewStudioRecordingAdded, - EditorRecordingAdded, - editor_recording::EditorRecordingFlowChanged, - NewScreenshotAdded, - RenderFrameEvent, - EditorStateChanged, - editor_preparing::PreparingEditorChanged, - 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, - diagnostics::DiagnosticProgress, - ]) - .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::() - .typ::() -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: Option) { - let startup = startup::Startup::default(); - // Arm the unexpected-termination sentinel before anything else can crash, and - // report any previous session that died without a clean shutdown. - let previous_termination = crash_sentinel::init(logs_dir.as_deref(), env!("CARGO_PKG_VERSION")); - configure_windows_graphics_recovery(previous_termination); - - // Keep the sentinel's blur marker in sync with live BlurProcessor instances - // (camera preview and editor render alike), so a native blur crash is - // attributable on the next launch. - cap_camera_effects::set_blur_session_observer(|active| { - if active { - crash_sentinel::enter_blur_session(); - } else { - crash_sentinel::exit_blur_session(); - } - }); - - ffmpeg::init() - .map_err(|e| { - error!("Failed to initialize ffmpeg: {e}"); - }) - .ok(); - - // Detect the camera-preview quality profile once from total RAM. On low-RAM - // machines (<= 8GB) this opts the preview into a cheaper profile (smaller - // textures, 30fps, no background blur); higher-spec machines keep the exact - // current behaviour. Only the preview is affected — recording is untouched. - { - let mut system = sysinfo::System::new(); - system.refresh_memory(); - camera::init_preview_profile(system.total_memory()); - } - - telemetry::init(); - - let tauri_context = tauri::generate_context!(); - - let specta_builder = specta_builder(); - - #[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(typescript_exporter(), bindings_path) { - warn!(error = %err, "Failed to export TypeScript bindings"); - } - } else { - debug!("Skipping TypeScript bindings export outside source checkout"); - } - } - - let (camera_preview_state_tx, camera_preview_state_rx) = - tokio::sync::watch::channel(CameraPreviewState::default()); - let camera_preview_ws = camera_legacy::create_camera_preview_ws(camera_preview_state_rx).await; - let camera_tx = camera_preview_ws.sender; - let camera_ws_port = camera_preview_ws.port; - let _shutdown = camera_preview_ws.shutdown; - #[cfg(target_os = "linux")] - let camera_processing = camera_preview_ws.processing; - let camera_ws_sender = camera_tx.clone(); - - let (mic_samples_tx, mic_samples_rx) = flume::bounded(8); - let mic_meter_sender = mic_samples_tx.clone(); - - let camera_feed = CameraFeed::spawn(CameraFeed::default()); - let _ = camera_feed.ask(feeds::camera::AddSender(camera_tx)).await; - - let (mic_error_tx, mic_error_rx) = flume::bounded(1); - - let mic_feed = { - let mic_feed = MicrophoneFeed::spawn(MicrophoneFeed::new(mic_error_tx)); - - if let Err(err) = mic_feed - .ask(feeds::microphone::AddSender(mic_samples_tx)) - .await - { - error!("Failed to attach audio meter sender: {err}"); - } - - mic_feed - }; - - tauri::async_runtime::set(tokio::runtime::Handle::current()); - - let builder = - tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { - #[cfg(target_os = "linux")] - { - let handle = app.clone(); - if let Err(error) = app.run_on_main_thread(move || { - handle_single_instance(&handle, args); - }) { - warn!(%error, "Could not queue Cap activation"); - } - } - #[cfg(not(target_os = "linux"))] - handle_single_instance(app, args); - })); - - #[cfg(target_os = "macos")] - let builder = builder - .manage(StartupOpenGate::default()) - .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()); - - let builder = builder - .manage(startup) - .on_page_load(|webview, payload| { - if webview.label() == "onboarding" - && matches!(payload.event(), tauri::webview::PageLoadEvent::Finished) - { - webview.app_handle().state::().mark_ready(); - } - }) - .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({ - let public_commands = specta_builder.invoke_handler(); - let recovery_commands: fn(tauri::ipc::Invoke) -> bool = tauri::generate_handler![recovery::get_recording_recovery_success]; - move |invoke| { - if invoke.message.command() == "get_recording_recovery_success" { - recovery_commands(invoke) - } else { - public_commands(invoke) - } - } - }) - .setup(move |app| { - let app = app.handle().clone(); - #[cfg(debug_assertions)] - stop_editor_benchmark::validate_app_identifier(&app.config().identifier)?; - #[cfg(target_os = "macos")] - let _startup_open_guard = app - .try_state::() - .map(|state| StartupOpenGuard((*state).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); - general_settings::init(&app); - // Before anything shows a window or initialises further state: when - // the native app owns the session, this one only exists to start it. - if gpui_app::redirect_at_startup_if_enabled(&app)? { - // Nothing is managed yet, so `ExitRequested` would otherwise - // spawn a cleanup with no `AppExitState` to guard it. Marking - // the exit as already begun takes the plain runtime-exit path. - let exit_state = AppExitState::default(); - exit_state.begin(); - app.manage(exit_state); - crash_sentinel::mark_clean_exit(); - - #[cfg(target_os = "macos")] - { - if app.try_state::().is_none() { - app.manage(gpui_app::StartupRedirectState::default()); - } - finish_macos_startup_opens(&app, StartupOpenDestination::Gpui); - gpui_app::retire_foreground_parent_for_handoff(&app); - let app = app.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(750)).await; - let Some(reopen_pid) = app - .try_state::() - .and_then(|state| state.exit_if_pending()) - else { - return; - }; - if let Some(pid) = reopen_pid - { - match tokio::task::spawn_blocking(move || gpui_app::request_gpui_reopen(pid)).await { - Ok(Ok(())) => info!(pid, "Queued a request to reopen Cap GPUI"), - Ok(Err(error)) => warn!(pid, %error, "Could not confirm Cap GPUI reopening; the existing instance remains unchanged"), - Err(error) => warn!(pid, %error, "Cap GPUI reopen forwarding did not finish"), - } - } - app.exit(0); - }); - } - - #[cfg(not(target_os = "macos"))] - app.exit(0); - - return Ok(()); - } - app.manage(clean_capture::State::default()); - hotkeys::init(&app); - configure_camera_blur_recovery(&app, previous_termination); - fake_window::init(&app); - app.manage(target_select_overlay::WindowFocusManager::default()); - app.manage(EditorWindowIds::default()); - app.manage(ScreenshotEditorWindowIds::default()); - app.manage(EditorRecordingTarget::default()); - app.manage(editor_recording::EditorRecordingFlowState::default()); - #[cfg(target_os = "macos")] - app.manage(crate::platform::ScreenCapturePrewarmer::default()); - #[cfg(target_os = "macos")] - app.manage(panel_manager::PanelManager::new()); - app.manage(http_client::HttpClient::default()); - app.manage(http_client::RetryableHttpClient::default()); - app.manage(PendingScreenshots::default()); - app.manage(FinalizingRecordings::default()); - app.manage(editor_preparing::PreparingConsumers::default()); - app.manage(updates::UpdatesState::default()); - updates::spawn_background_loop(app.clone()); - - #[cfg(unix)] - { - let app_for_signal = app.clone(); - tokio::spawn(async move { - use tokio::signal::unix::{SignalKind, signal}; - let Ok(mut term) = signal(SignalKind::terminate()) else { - tracing::warn!("Failed to register SIGTERM handler"); - return; - }; - let Ok(mut hup) = signal(SignalKind::hangup()) else { - tracing::warn!("Failed to register SIGHUP handler"); - return; - }; - loop { - tokio::select! { - signal = term.recv() => { - if signal.is_none() { - return; - } - tracing::info!("Received SIGTERM; requesting graceful shutdown"); - } - signal = hup.recv() => { - if signal.is_none() { - return; - } - tracing::info!("Received SIGHUP; requesting graceful shutdown"); - } - } - request_app_exit(app_for_signal.clone()).await; - if app_is_exiting(&app_for_signal) { - return; - } - } - }); - } - - tokio::spawn({ - let camera_feed = camera_feed.clone(); - let app = app.clone(); - async move { - camera_feed - .tell(feeds::camera::OnFeedDisconnect(Box::new({ - move || { - if app_is_exiting(&app) { - return; - } - - if let Some(win) = CapWindowId::Camera.get(&app) { - win.hide().ok(); - } - } - }))) - .send() - .await - .map_err(|err| error!("Error registering on camera feed disconnect: {err}")) - .ok(); - } - }); - - if let Ok(Some(auth)) = AuthStore::load(&app) { - sentry::configure_scope(|scope| { - scope.set_user(auth.user_id.map(|id| sentry::User { - id: Some(id), - ..Default::default() - })); - }); - } - - { - let (server_url, should_update) = if cfg!(debug_assertions) - && let Ok(url) = std::env::var("VITE_SERVER_URL") - { - (url, true) - } else if let Some(url) = GeneralSettingsStore::get(&app) - .ok() - .flatten() - .map(|v| v.server_url.clone()) - { - (url, false) - } else { - ( - option_env!("VITE_SERVER_URL") - .unwrap_or("https://cap.so") - .to_string(), - true, - ) - }; - - // This ensures settings reflects the correct value if it's set at startup - if should_update { - GeneralSettingsStore::update(&app, |s| { - s.server_url = server_url.clone(); - }) - .map_err(|err| warn!("Error updating server URL into settings store: {err}")) - .ok(); - } - - telemetry::set_server_url(&server_url); - - let camera_preview = CameraPreviewManager::new(&app); - let camera_session_id_handle = camera_preview.session_id_handle(); - - let requested_settings = match RecordingSettingsStore::get(&app) { - Ok(settings) => settings.unwrap_or_default(), - Err(error) => { - warn!(%error, "Failed to load recording settings during startup"); - RecordingSettingsStore::default() - } - }; - app.manage(linux_instant_camera::PresentationBroker::default()); - app.manage(RequestedInputsState::new( - requested_settings.mic_name, - requested_settings.camera_id, - )); - - app.manage(Arc::new(RwLock::new(App { - camera_ws_port, - camera_ws_sender, - #[cfg(target_os = "linux")] - camera_processing, - handle: app.clone(), - camera_preview, - camera_preview_state_tx, - recording_state: RecordingState::None, - recording_logging_handle, - mic_feed, - mic_meter_sender, - selected_mic_label: None, - applied_mic_input: AppliedMicrophoneInput::default(), - selected_camera_id: None, - camera_in_use: false, - camera_cleanup_done: false, - camera_feed, - server_url, - logs_dir: logs_dir.clone(), - disconnected_inputs: HashSet::new(), - was_camera_only_recording: false, - }))); - - app.manage(camera_session_id_handle); - app.manage(CameraWindowCloseGate::default()); - app.manage(CameraWindowPositionGuard::default()); - app.manage(CameraWindowOperationLock::default()); - app.manage(AppExitState::default()); - app.manage(MainWindowReadyState::default()); - app.manage(deeplink_actions::DeepLinkActionExecutor::new(&app)); - #[cfg(target_os = "macos")] - install_macos_native_terminate_handler(&app); - spawn_process_memory_sampler(app.clone()); - - #[cfg(target_os = "linux")] - let clipboard = ClipboardContext::new(&app) - .expect("Failed to create clipboard context"); - #[cfg(not(target_os = "linux"))] - let clipboard = ClipboardContext::new() - .expect("Failed to create clipboard context"); - app.manage(Arc::new(RwLock::new(clipboard))); - } - - app.listen_any("main-window-ready", { - let app = app.clone(); - move |_| { - app.state::().set_ready(true); - tracing::info!("Main window frontend ready"); - app.state::().mark_ready(); - #[cfg(debug_assertions)] - stop_editor_benchmark::run(app.clone()); - #[cfg(debug_assertions)] - picker_benchmark::run(app.clone()); - gpu_context::prewarm_gpu(); - tokio::task::spawn_blocking(cap_rendering::prewarm_fonts); - tokio::spawn(screenshot_editor::prewarm_screenshot_renderer()); - - #[cfg(target_os = "macos")] - { - let app = app.clone(); - tokio::spawn(async move { - if let Some(prewarmer) = - app.try_state::() - { - prewarmer.request(false).await; - } else { - warn!("ScreenCapturePrewarmer state unavailable after main window ready"); - } - }); - } - } - }); - - upload::lifecycle::init(app.clone()); - - spawn_mic_error_handler(app.clone(), mic_error_rx); - spawn_device_watchers(app.clone()); - spawn_devices_snapshot_emitter(app.clone()); - spawn_system_resume_detector(app.clone()); - power_observer::install(&app); - window_position_persistence::install(&app); - - tokio::spawn(check_notification_permissions(app.clone())); - - tokio::spawn({ - let app = app.clone(); - async move { - let startup_window = if should_show_onboarding(&app) { - ShowCapWindow::Onboarding - } else { - ShowCapWindow::Main { - init_target_mode: None, - } - }; - match startup_window.show(&app).await { - Ok(window) => { - tracing::info!(label = window.label(), "Startup window created"); - } - Err(error) => { - tracing::error!(%error, "Failed to open startup window"); - } - } - } - }); - - audio_meter::spawn_event_emitter(app.clone(), mic_samples_rx); - - if let Err(err) = tray::create_tray(&app) { - error!("Failed to create tray: {err}"); - } - - RequestStartRecording::listen_any_spawn(&app, async |event, app| { - let settings = RecordingSettingsStore::get(&app) - .ok() - .flatten() - .unwrap_or_default(); - - let _ = start_recording(app.clone(), app.state(), { - recording::StartRecordingInputs { - capture_target: settings.target.unwrap_or_else(|| { - ScreenCaptureTarget::Display { - id: Display::primary().id(), - } - }), - mode: event.mode, - capture_system_audio: settings.system_audio, - organization_id: settings.organization_id, - } - }) - .await; - }); - - RequestOpenRecordingPicker::listen_any_spawn(&app, async |event, app| { - if let Some(target_mode) = event.target_mode { - open_target_picker(&app, target_mode).await; - } else { - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(&app) - .await; - } - }); - - RequestOpenSettings::listen_any_spawn(&app, async |payload, app| { - let _ = ShowCapWindow::Settings { - page: Some(payload.page), - } - .show(&app) - .await; - }); - - #[cfg(target_os = "macos")] - RequestScreenCapturePrewarm::listen_any_spawn(&app, async |event, app| { - if let Some(prewarmer) = app.try_state::() - { - prewarmer.request(event.force).await; - } else { - warn!("ScreenCapturePrewarmer state unavailable during prewarm request"); - } - }); - - RecordingStarted::listen_any_spawn(&app, async |_event, app| { - crate::automation::run_recording_started_automations(app); - }); - - RecordingDeleted::listen_any_spawn(&app, async |event, app| { - crate::automation::run_recording_deleted_automations(app, event.path); - }); - - import::VideoImportProgress::listen_any_spawn(&app, async |event, app| { - if matches!(event.stage, import::ImportStage::Complete) { - crate::automation::run_video_imported_automations( - app, - std::path::PathBuf::from(event.project_path), - ); - } - }); - - let app_handle = app.clone(); - app.deep_link().on_open_url(move |event| { - deeplink_actions::handle(&app_handle, event.urls()); - }); - - #[cfg(target_os = "macos")] - finish_macos_startup_opens(&app, StartupOpenDestination::Desktop); - - Ok(()) - }) - .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(_) - ) && app_is_exiting(app) - { - return; - } - - match event { - WindowEvent::CloseRequested { api, .. } => { - let window_id = CapWindowId::from_str(label).ok(); - if !matches!( - window_id, - Some(CapWindowId::Editor { .. }) - | Some(CapWindowId::ScreenshotEditor { .. }) - ) && export::export_session_active() - { - api.prevent_close(); - warn!( - window = label, - "Preventing window close request during active export" - ); - return; - } - - if let Some(window_id) = window_id { - match window_id { - CapWindowId::Camera => { - if app - .try_state::() - .is_some_and(|close_gate| close_gate.allow_close()) - { - return; - } - - api.prevent_close(); - let _ = window.hide(); - tracing::warn!("Camera window CloseRequested event received!"); - let session_id = app - .try_state::>() - .map(|state| state.load(Ordering::Acquire)) - .unwrap_or(0); - let app = app.clone(); - spawn_on_runtime(async move { - cleanup_camera_window(app, session_id).await; - }); - } - CapWindowId::Main => { - api.prevent_close(); - dismiss_main_window(app); - } - _ => {} - } - } - } - WindowEvent::Destroyed => { - fake_window::cancel_fake_window_listener(app, label); - let window_id = CapWindowId::from_str(label).ok(); - if let Some(window_id) = &window_id { - retire_project_window(window, window_id); - } - if app_is_exiting(app) { - return; - } - if export::export_session_active() { - warn!( - window = label, - "Skipping window restoration during active export" - ); - return; - } - if let Some(window_id) = window_id { - if matches!(window_id, CapWindowId::Camera) { - tracing::warn!("Camera window Destroyed event received!"); - } - match window_id { - CapWindowId::Main => { - let app = app.clone(); - - close_target_select_overlays(&app); - - if let Some(camera) = CapWindowId::Camera.get(&app) { - let _ = camera.hide(); - } - - spawn_on_runtime(async move { - let Some(state) = app.try_state::>() else { - warn!("App state unavailable during main window destroyed cleanup"); - return; - }; - - let feeds = { - let mut app_state = state.write().await; - if app_state.is_recording_active_or_pending() { - None - } else { - app_state.applied_mic_input.invalidate(); - Some(( - app_state.mic_feed.clone(), - app_state.camera_feed.clone(), - )) - } - }; - - if let Some((mic_feed, camera_feed)) = feeds { - let _ = tokio::time::timeout( - APP_EXIT_STEP_TIMEOUT, - mic_feed.ask(microphone::RemoveInput), - ) - .await; - let _ = tokio::time::timeout( - APP_EXIT_STEP_TIMEOUT, - camera_feed.ask(feeds::camera::RemoveInput), - ) - .await; - - let mut app_state = state.write().await; - if !app_state.is_recording_active_or_pending() { - app_state.selected_mic_label = None; - app_state.selected_camera_id = None; - app_state.camera_in_use = false; - } - } - }); - } - CapWindowId::Editor { .. } | CapWindowId::ScreenshotEditor { .. } => { - editor_recording::abort_if_editor_gone(app); - restore_main_windows_if_no_editors(app); - } - CapWindowId::Settings => { - restore_main_and_target_select_windows(app); - - restore_camera_window(app); - - #[cfg(target_os = "windows")] - if !has_open_editor_window(app) { - reopen_main_window(app); - } - - #[cfg(target_os = "macos")] - return; - } - CapWindowId::Upgrade | CapWindowId::ModeSelect => { - restore_main_and_target_select_windows(app); - restore_camera_window(app); - #[cfg(target_os = "macos")] - return; - } - CapWindowId::TargetSelectOverlay { display_id } => { - if let Some(focus_manager) = - app.try_state::() - { - focus_manager.destroy(&display_id, app.global_shortcut()); - } - let session_id = app - .try_state::>() - .map(|state| state.load(Ordering::Acquire)) - .unwrap_or(0); - spawn_on_runtime(cleanup_camera_after_overlay_close( - app.clone(), - session_id, - )); - } - CapWindowId::Camera => { - tracing::info!( - "Camera window Destroyed event - resetting panel state" - ); - let session_id = app - .try_state::>() - .map(|state| state.load(Ordering::Acquire)) - .unwrap_or(0); - let app = app.clone(); - spawn_on_runtime(async move { - #[cfg(target_os = "macos")] - { - if let Some(panel_manager) = - app.try_state::() - { - panel_manager - .force_reset(panel_manager::PanelWindowType::Camera) - .await; - } - } - cleanup_camera_window(app, session_id).await; - }); - } - _ => {} - }; - } - - #[cfg(target_os = "macos")] - crate::permissions::sync_macos_dock_visibility(app); - } - #[cfg(target_os = "macos")] - WindowEvent::Focused(focused) => { - let window_id = CapWindowId::from_str(label); - - if matches!(window_id, Ok(CapWindowId::Upgrade)) { - for (label, window) in app.webview_windows() { - if let Ok(id) = CapWindowId::from_str(&label) - && matches!(id, CapWindowId::TargetSelectOverlay { .. }) - { - hide_overlay(&window); - } - } - } - - 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, .. }) => { - let window_id = CapWindowId::from_str(label).ok(); - for path in paths { - let result = if matches!(window_id, Some(CapWindowId::Main)) { - open_importable_from_path(path, app.clone()) - } else { - open_project_from_path(path, app.clone()) - }; - - if let Err(err) = result { - warn!(path = %path.display(), error = %err, "Failed to open dropped path"); - } - } - } - WindowEvent::Moved(position) => { - 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) - { - match window.set_position(constrained_position) { - Ok(()) => { - let scale_factor = window - .current_monitor() - .ok() - .flatten() - .map(|monitor| monitor.scale_factor()) - .unwrap_or_else(|| window.scale_factor().unwrap_or(1.0)); - let logical_pos = - constrained_position.to_logical::(scale_factor); - let display_id = display_for_position(logical_pos.x, logical_pos.y) - .map(|display| display.id()); - window_position_persistence::queue_main_position( - app, - general_settings::WindowPosition { - x: logical_pos.x, - y: logical_pos.y, - display_id, - }, - ); - return; - } - Err(error) => warn!( - %error, - "Failed to constrain main window to the visible screen area" - ), - } - } - - if let Ok(window_id) = window_id { - let scale_factor = window.scale_factor().unwrap_or(1.0); - let logical_pos = position.to_logical::(scale_factor); - - // Resolve which display the window landed on. On Windows this - // must use the physical position: mixed-DPI logical rects - // overlap, so logical containment can pick the wrong display. - #[cfg(windows)] - let moved_display = - display_for_physical_position(position.x as f64, position.y as f64); - #[cfg(not(windows))] - let moved_display = display_for_position(logical_pos.x, logical_pos.y); - - match window_id { - CapWindowId::Main => { - let display_id = - moved_display.as_ref().map(|display| display.id()); - window_position_persistence::queue_main_position( - app, - general_settings::WindowPosition { - x: logical_pos.x, - y: logical_pos.y, - display_id, - }, - ); - } - CapWindowId::Camera => { - if app - .try_state::() - .is_some_and(|guard| guard.should_ignore()) - { - return; - } - window_position_persistence::queue_camera_position( - app, - logical_pos.x, - logical_pos.y, - moved_display.map(|display| display.id()), - ); - } - _ => {} - } - } - } - WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - if let Some(webview_window) = app.get_webview_window(label) { - windows::update_window_rasterization_scale( - &webview_window, - *scale_factor, - ); - } - } - _ => {} - } - })); - - if let Err(panic) = result { - let message = panic_payload_message(&panic); - tracing::error!(panic = %message, "Suppressed panic in Tauri WindowEvent handler"); - sentry::capture_message( - &format!("Tauri WindowEvent panic suppressed: {message}"), - sentry::Level::Error, - ); - } - }); - - run_tauri_event_loop(move || { - builder - .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, - ); - } - }); - }); -} - -fn run_tauri_event_loop(run: impl FnOnce() -> R) -> R { - #[cfg(target_os = "linux")] - { - // zbus blocks on its own runtime during plugin setup and shutdown; keep GTK on this thread. - tokio::task::block_in_place(run) - } - #[cfg(not(target_os = "linux"))] - run() -} - -fn handle_single_instance(app: &AppHandle, args: Vec) { - trace!(arg_count = args.len(), "Single instance invoked"); - - #[cfg(target_os = "linux")] - if app - .try_state::() - .is_some_and(|state| state.is_exiting()) - { - return; - } - - if gpui_app::handle_update_handoff(app) { - return; - } - - #[cfg(any(target_os = "macos", windows))] - if gpui_app::forward_deep_links_to_active_gpui(app, &args) { - return; - } - - 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(); - tauri::async_runtime::spawn(async move { - if let Err(error) = (ShowCapWindow::Main { - init_target_mode: None, - }) - .show(&app) - .await - { - warn!(%error, "Could not show Cap recording controls"); - } - }); - return; - }; - - let _ = open_project_from_path(&cap_file, app.clone()); -} - -pub(crate) fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String { - if let Some(s) = payload.downcast_ref::<&'static str>() { - (*s).to_string() - } else if let Some(s) = payload.downcast_ref::() { - s.clone() - } else { - "".to_string() - } -} - -// Catches panics inside an async Tauri 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. -async fn run_command_safely(command_name: &'static str, fut: F) -> Result -where - F: std::future::Future>, -{ - match AssertUnwindSafe(fut).catch_unwind().await { - Ok(result) => result, - Err(panic) => { - let message = panic_payload_message(&*panic); - error!( - command = command_name, - panic = %message, - "Suppressed panic in Tauri command" - ); - sentry::capture_message( - &format!("Tauri command '{command_name}' panicked: {message}"), - sentry::Level::Error, - ); - Err(format!("{command_name} failed unexpectedly")) - } - } -} - -fn emit_app_event_safely(app: &AppHandle, event: E) -where - E: Event + 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 = panic_payload_message(&panic); - error!(event = event_name, panic = %message, "Suppressed panic while emitting app event"); - } - } -} - -#[cfg(any(target_os = "macos", test))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum StartupOpenDestination { - Desktop, - Gpui, -} - -#[cfg(any(target_os = "macos", test))] -struct StartupOpenDispatch { - destination: StartupOpenDestination, - urls: Vec, -} - -#[cfg(any(target_os = "macos", test))] -#[derive(Default)] -struct StartupOpenQueue { - destination: Option, - cancelled: bool, - urls: Vec, - gpui_forwarding: bool, - gpui_dispatched: Vec, -} - -#[cfg(any(target_os = "macos", test))] -impl StartupOpenQueue { - fn request(&mut self, urls: Vec) -> Result, String> { - if self.cancelled { - return Err("Cap startup stopped before the project could be opened".into()); - } - if self.destination == Some(StartupOpenDestination::Desktop) { - return Ok(Some(StartupOpenDispatch { - destination: StartupOpenDestination::Desktop, - urls, - })); - } - let mut additions = Vec::new(); - for url in urls { - if self.urls.contains(&url) - || self.gpui_dispatched.contains(&url) - || additions.contains(&url) - { - continue; - } - if self.urls.len() + self.gpui_dispatched.len() + additions.len() >= 64 { - return Err("Too many projects were requested while Cap was starting".into()); - } - additions.push(url); - } - self.urls.extend(additions); - if self.destination == Some(StartupOpenDestination::Gpui) && !self.gpui_forwarding { - return Ok(self.take_queued()); - } - Ok(None) - } - - fn finish(&mut self, destination: StartupOpenDestination) -> Option { - if self.cancelled || self.destination.is_some() { - return None; - } - self.destination = Some(destination); - self.take_queued() - } - - fn take_queued(&mut self) -> Option { - let destination = self.destination?; - if self.urls.is_empty() { - return None; - } - let urls = std::mem::take(&mut self.urls); - if destination == StartupOpenDestination::Gpui { - self.gpui_forwarding = true; - self.gpui_dispatched.extend(urls.iter().cloned()); - } - Some(StartupOpenDispatch { destination, urls }) - } - - fn next_gpui_batch(&mut self) -> Option { - if self.cancelled - || self.destination != Some(StartupOpenDestination::Gpui) - || !self.gpui_forwarding - { - return None; - } - if let Some(dispatch) = self.take_queued() { - return Some(dispatch); - } - self.cancel(); - None - } - - fn cancel(&mut self) { - self.cancelled = true; - self.urls.clear(); - self.gpui_dispatched.clear(); - self.gpui_forwarding = false; - } -} - -#[cfg(any(target_os = "macos", test))] -#[derive(Clone, Default)] -struct StartupOpenGate(Arc>); - -#[cfg(any(target_os = "macos", test))] -struct StartupOpenGuard(StartupOpenGate); - -#[cfg(any(target_os = "macos", test))] -impl Drop for StartupOpenGuard { - fn drop(&mut self) { - if let Ok(mut queue) = self.0.0.lock() - && queue.destination.is_none() - { - queue.cancel(); - } - } -} - -#[cfg(target_os = "macos")] -fn queue_macos_startup_urls(app: &AppHandle, urls: Vec) -> Result<(), String> { - let gate = app - .try_state::() - .ok_or_else(|| "Cap startup is not ready to receive projects".to_string())?; - let dispatch = gate - .0 - .lock() - .map_err(|_| "Cap startup file-open state is unavailable".to_string())? - .request(urls)?; - if let Some(dispatch) = dispatch { - dispatch_macos_startup_urls(app, dispatch); - } - Ok(()) -} - -#[cfg(target_os = "macos")] -fn finish_macos_startup_opens(app: &AppHandle, destination: StartupOpenDestination) { - let Some(gate) = app.try_state::() else { - return; - }; - let dispatch = match gate.0.lock() { - Ok(mut queue) => queue.finish(destination), - Err(error) => { - warn!(%error, "Could not release startup project requests"); - return; - } - }; - if let Some(dispatch) = dispatch { - dispatch_macos_startup_urls(app, dispatch); - } -} - -#[cfg(target_os = "macos")] -fn cancel_macos_startup_opens(app: &AppHandle) { - if let Some(gate) = app.try_state::() - && let Ok(mut queue) = gate.0.lock() - { - queue.cancel(); - } -} - -#[cfg(target_os = "macos")] -fn dispatch_macos_startup_urls(app: &AppHandle, dispatch: StartupOpenDispatch) { - let urls = dispatch.urls; - let arguments = urls - .iter() - .map(|url| url.as_str().to_string()) - .collect::>(); - - if dispatch.destination == StartupOpenDestination::Gpui { - let Some(redirect) = app.try_state::() else { - warn!("Cap GPUI startup forwarding state is unavailable"); - return; - }; - if redirect.begin_forwarding() { - let app = app.clone(); - tokio::spawn(async move { - let mut arguments = arguments; - let mut forwarded_pid = None; - loop { - let forwarded = tokio::task::spawn_blocking(move || { - gpui_app::forward_deep_links_to_gpui_when_ready(&arguments) - }) - .await - .ok() - .flatten(); - if let Some(pid) = forwarded { - forwarded_pid = Some(pid); - } else { - warn!("Could not forward the requested project batch to Cap GPUI"); - } - let next = app.try_state::().and_then(|gate| { - gate.0 - .lock() - .ok() - .and_then(|mut queue| queue.next_gpui_batch()) - }); - let Some(next) = next else { - break; - }; - arguments = next - .urls - .iter() - .map(|url| url.as_str().to_string()) - .collect(); - } - - if let Some(pid) = forwarded_pid - && let Err(error) = app.run_on_main_thread(move || { - gpui_app::activate_instance(pid); - }) - { - warn!(%error, "Could not activate Cap GPUI after forwarding a project"); - } - if app - .try_state::() - .is_some_and(|state| state.exit_after_forwarding()) - { - app.exit(0); - } - }); - } else { - cancel_macos_startup_opens(app); - warn!("Cap GPUI handoff already finished before the project could be forwarded"); - } - return; - } - - if gpui_app::forward_deep_links_to_active_gpui(app, &arguments) { - return; - } - - for url in urls { - if url.scheme() == "file" - && let Ok(path) = url.to_file_path() - && let Err(error) = open_project_from_path(&path, app.clone()) - { - warn!(path = %path.display(), %error, "Could not open the requested project"); - } - } -} - -fn handle_run_event(_handle: &AppHandle, event: tauri::RunEvent) { - match event { - #[cfg(target_os = "macos")] - tauri::RunEvent::Opened { urls } => { - if let Err(error) = queue_macos_startup_urls(_handle, urls) { - warn!(%error, "Could not receive the requested startup project"); - } - } - #[cfg(target_os = "macos")] - tauri::RunEvent::Reopen { .. } => { - if _handle - .try_state::() - .is_some() - { - return; - } - - if gpui_app::handle_update_handoff(_handle) { - return; - } - - let should_focus_onboarding = should_show_onboarding(_handle); - - if should_focus_onboarding - && let Some(onboarding) = CapWindowId::Onboarding.get(_handle) - { - onboarding.show().ok(); - onboarding.set_focus().ok(); - return; - } - - let has_window = _handle.webview_windows().iter().any(|(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") - }); - - if has_window { - if let Some(window) = _handle - .webview_windows() - .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.clone()) - { - window.set_focus().ok(); - } - } else { - let handle = _handle.clone(); - spawn_on_runtime(async move { - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(&handle) - .await; - }); - } - } - tauri::RunEvent::ExitRequested { code, api, .. } => { - info!(?code, "App exit requested"); - - match handle_exit_requested( - _handle - .try_state::() - .is_some_and(|state| state.is_exiting()), - export::export_session_active() || upload::upload_session_active(), - code.is_some(), - code == Some(tauri::RESTART_EXIT_CODE), - || api.prevent_exit(), - ) { - ExitRequestDecision::StartCleanup => { - let handle = _handle.clone(); - spawn_on_runtime(async move { - request_app_exit(handle).await; - }); - } - ExitRequestDecision::AlreadyExiting => {} - ExitRequestDecision::ExportActive => { - warn!("Preventing app exit request during an active export or upload"); - } - ExitRequestDecision::AllowRuntimeExit => {} - ExitRequestDecision::AllowRuntimeRestart => { - if let Some(state) = _handle.try_state::() { - state.begin_restart(); - } - crash_sentinel::mark_clean_exit(); - info!("Allowing Tauri to restart the app"); - } - } - } - tauri::RunEvent::Exit => { - #[cfg(target_os = "macos")] - cancel_macos_startup_opens(_handle); - #[cfg(target_os = "macos")] - { - // This arm runs on the AppKit main thread, so reverse the Liquid Glass - // SPI inline before restart or a hard _exit. This is the last-chance - // teardown for terminal paths that skip cleanup_app_resources_for_exit; - // touching the NSWindow/NSView here is safe because we are on main. - let torn_down = crate::platform::teardown_all_liquid_glass_on_main(_handle); - if _handle - .try_state::() - .is_some_and(|state| state.is_restarting()) - { - info!( - windows = torn_down, - "macOS runtime exit reached; allowing Tauri to restart" - ); - return; - } - warn!( - windows = torn_down, - "macOS runtime exit reached; tore down liquid glass, forcing process exit" - ); - force_exit(0); - } - - #[cfg(not(target_os = "macos"))] - { - let already_exiting = match _handle.try_state::() { - Some(state) => !state.begin(), - None => false, - }; - if already_exiting { - return; - } - - let handle = _handle.clone(); - spawn_on_runtime(async move { - let _ = tokio::time::timeout( - Duration::from_secs(2), - cleanup_app_resources_for_exit(&handle), - ) - .await; - }); - } - } - _ => {} - } -} - -fn spawn_on_runtime(future: F) -where - F: Future + Send + 'static, -{ - match tokio::runtime::Handle::try_current() { - Ok(handle) => { - handle.spawn(future); - } - Err(err) => { - tracing::warn!(error = %err, "No tokio runtime available; dropping background task"); - } - } -} - -fn retire_project_window(window: &Window, window_id: &CapWindowId) { - let app = window.app_handle(); - match window_id { - CapWindowId::Editor { id } => { - app.state::() - .cancel_window(*id, None); - let window_ids = EditorWindowIds::get(app); - match window_ids.ids.lock() { - Ok(mut ids) => ids.retain(|(_, current_id)| current_id != id), - Err(err) => warn!(error = %err, "Editor window ids lock poisoned"), - } - export::cancel_exports_for_window(window.label()); - let label = window.label().to_string(); - let pending = PendingEditorInstances::get(app); - spawn_on_runtime(async move { - pending.cancel_prewarm(&label).await; - }); - spawn_on_runtime(EditorInstances::remove(window.clone())); - } - CapWindowId::ScreenshotEditor { id } => { - let window_ids = ScreenshotEditorWindowIds::get(app); - match window_ids.ids.lock() { - Ok(mut ids) => ids.retain(|(_, current_id)| current_id != id), - Err(err) => warn!(error = %err, "Screenshot editor window ids lock poisoned"), - } - export::cancel_exports_for_window(window.label()); - let label = window.label().to_string(); - let pending = PendingScreenshotEditorInstances::get(app); - spawn_on_runtime(async move { - pending.cancel_prewarm(&label).await; - }); - spawn_on_runtime(ScreenshotEditorInstances::remove(window.clone())); - } - _ => {} - } -} - -#[cfg(target_os = "windows")] -fn has_open_editor_window(app: &AppHandle) -> bool { - app.webview_windows() - .keys() - .any(|label| matches!(CapWindowId::from_str(label), Ok(CapWindowId::Editor { .. }))) -} - -fn restore_main_windows_if_no_editors(app: &AppHandle) { - let has_other_editors = app.webview_windows().keys().any(|label| { - matches!( - CapWindowId::from_str(label), - Ok(CapWindowId::Editor { .. } | CapWindowId::ScreenshotEditor { .. }) - ) - }); - - if !has_other_editors { - if CapWindowId::Settings.get(app).is_none() { - if let Some(main) = CapWindowId::Main.get(app) { - let generation = clean_capture::generation(app); - let _ = clean_capture::reveal_now(&main, generation); - } - - restore_main_window_inputs(app); - restore_camera_window(app); - } - - spawn_on_runtime(captions::release_ml_models()); - } -} - -fn restore_main_and_target_select_windows(app: &AppHandle) { - let target_select_overlays_hidden_for_restore = app - .try_state::() - .map(|focus_manager| focus_manager.take_overlay_restore_labels()) - .unwrap_or_default(); - - for (label, window) in app.webview_windows() { - if let Ok(id) = CapWindowId::from_str(&label) { - match id { - CapWindowId::TargetSelectOverlay { .. } => { - if window.is_visible().unwrap_or(false) - || target_select_overlays_hidden_for_restore.contains(label.as_str()) - { - show_overlay(&window); - } - } - CapWindowId::Main => { - let generation = clean_capture::generation(app); - let _ = clean_capture::reveal_now(&window, generation); - restore_main_window_inputs(app); - } - _ => {} - } - } - } -} - -fn restore_main_window_inputs(app: &AppHandle) { - let handle = app.clone(); - spawn_on_runtime(async move { - windows::restore_main_window_inputs(&handle).await; - }); -} - -fn restore_camera_window(app: &AppHandle) { - let snapshot = app.state::().snapshot(); - if snapshot.camera.value.is_none() || snapshot.camera.pending || snapshot.camera.error.is_some() - { - return; - } - let app = app.clone(); - spawn_on_runtime(async move { - let requested = app.state::(); - let _input_operation = requested.operation.lock().await; - let operation_lock = app.state::(); - let _operation_guard = operation_lock.lock().await; - let state = app.state::>(); - let can_restore = { - let state = state.read().await; - requested.is_current(&snapshot) - && !state.is_recording_active_or_pending() - && !state.camera_cleanup_done - && state.selected_camera_id == snapshot.camera.value - }; - if can_restore { - let _ = show_requested_camera_window(&app, snapshot.camera.revision).await; - } - }); -} - -fn close_target_select_overlays(app: &AppHandle) { - target_select_overlay::close_target_select_overlay_windows(app); -} - -#[cfg(target_os = "windows")] -fn reopen_main_window(app: &AppHandle) { - #[cfg(target_os = "linux")] - if let Some(attempt) = recording::linux_instant::current(app) - && attempt.has_capture() - && !attempt.ui_ready() - { - let app = app.clone(); - drop(tauri::async_runtime::spawn(async move { - if recording::linux_instant::control(app.clone(), attempt, false) - .await - .is_ok() - { - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(&app) - .await; - } - })); - return; - } - if let Some(main) = CapWindowId::Main.get(app) { - let _ = main.show(); - let _ = main.set_focus(); - restore_main_window_inputs(app); - } else { - let handle = app.clone(); - tokio::spawn(async move { - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(&handle) - .await; - }); - } -} - -fn instant_upload_may_resume(inner: &RecordingMetaInner) -> bool { - !matches!( - inner, - RecordingMetaInner::Instant( - InstantRecordingMeta::InProgress { .. } | InstantRecordingMeta::Failed { .. } - ) - ) -} - -#[cfg(test)] -fn load_instant_resume_candidate(path: &std::path::Path) -> Result, String> { - load_upload_resume_candidate(path, true).map(|candidate| candidate.map(|(meta, _lock)| meta)) -} - -fn load_upload_resume_candidate( - path: &std::path::Path, - mark_crashed: bool, -) -> Result, String> { - load_upload_resume_candidate_at(path, mark_crashed, SystemTime::now()) -} - -fn load_upload_resume_candidate_at( - path: &std::path::Path, - mark_crashed: bool, - now: SystemTime, -) -> Result, String> { - if !mark_crashed && !upload::recovery_age::eligible(path, now) { - return Ok(None); - } - let lock = match upload::acquire_upload_lock(path) { - Ok(lock) => lock, - Err(_) => return Ok(None), - }; - let mut meta = RecordingMeta::load_for_project(path).map_err(|error| error.to_string())?; - if mark_crashed { - let changed = match &mut meta.inner { - RecordingMetaInner::Instant(InstantRecordingMeta::InProgress { .. }) => { - meta.inner = RecordingMetaInner::Instant(InstantRecordingMeta::Failed { - error: "Recording crashed".into(), - }); - true - } - RecordingMetaInner::Studio(studio) => { - if let StudioRecordingMeta::MultipleSegments { inner } = &mut **studio - && matches!(inner.status, Some(StudioRecordingStatus::InProgress)) - { - inner.status = Some(StudioRecordingStatus::Failed { - error: "Recording crashed".into(), - }); - true - } else { - false - } - } - _ => false, - }; - if changed { - meta.save_for_project().map_err(|error| error.to_string())?; - } - } - if !upload::recovery_age::eligible(path, now) { - return Ok(None); - } - upload::lifecycle::reconcile_reupload(&mut meta).map_err(|error| error.to_string())?; - Ok(instant_upload_may_resume(&meta.inner).then_some((meta, lock))) -} - -async fn resume_uploads(app: AppHandle, mark_crashed: bool) -> Result<(), String> { - upload::lifecycle::reap().await; - if app_is_exiting(&app) || !upload::lifecycle::has_capacity() { - return Ok(()); - } - let scan_app = app.clone(); - let paths = tokio::task::spawn_blocking(move || { - let mut paths = Vec::new(); - for directory in recordings_locations::known_recordings_dirs(&scan_app) { - let Ok(entries) = std::fs::read_dir(directory) else { - continue; - }; - for entry in entries.flatten() { - if app_is_exiting(&scan_app) { - return paths; - } - let path = entry.path(); - if path.extension().and_then(|value| value.to_str()) == Some("cap") - && (mark_crashed || upload::recovery_age::eligible(&path, SystemTime::now())) - { - paths.push(path); - } - } - } - paths - }) - .await - .map_err(|error| error.to_string())?; - for path in paths { - if app_is_exiting(&app) || !upload::lifecycle::has_capacity() { - break; - } - let candidate_path = path.clone(); - let candidate_app = app.clone(); - let candidate = tokio::task::spawn_blocking(move || { - let mark_crashed = mark_crashed - && candidate_app - .state::() - .predates_launch(&candidate_path); - load_upload_resume_candidate(&candidate_path, mark_crashed) - }) - .await - .map_err(|error| error.to_string())?; - let (meta, lock) = match candidate { - Ok(Some(candidate)) => candidate, - Ok(None) => continue, - Err(error) => { - warn!(%error, recording = %path.display(), "Recording upload state could not be read; files retained"); - continue; - } - }; - if matches!( - meta.upload, - None | Some(UploadMeta::Complete | UploadMeta::Failed { .. }) - ) { - continue; - } - let fallback_audio = matches!( - &meta.inner, - RecordingMetaInner::Instant(InstantRecordingMeta::Complete { - sample_rate: Some(_), - .. - }) - ); - let required_audio = - match upload::lifecycle::resume_audio(&app, &path, fallback_audio).await { - Ok(Some(required_audio)) => required_audio, - Ok(None) => continue, - Err(error) => { - warn!(%error, "Recording upload intent could not be read; files retained"); - continue; - } - }; - if let Err(error) = - upload::lifecycle::resume_existing(app.clone(), meta, lock, required_audio).await - { - warn!(%error, "Upload retry remains local"); - } - } - Ok(()) -} - -async fn create_editor_instance_impl( - app: &AppHandle, - path: PathBuf, - frame_cb: cap_editor::EditorFrameCallback, -) -> Result<(Arc, tauri::EventId), String> { - let app = app.clone(); - - wait_for_recording_ready(&app, &path).await?; - - recording::spawn_heal_oversized_desktop_background_snapshots(path.clone()); - - let shared_device = - gpu_context::get_shared_gpu() - .await - .map(|shared| cap_rendering::SharedWgpuDevice { - instance: (*shared.instance).clone(), - adapter: (*shared.adapter).clone(), - device: (*shared.device).clone(), - queue: (*shared.queue).clone(), - is_software_adapter: shared.is_software_adapter, - }); - - #[cfg(debug_assertions)] - let frame_cb: cap_editor::EditorFrameCallback = - if stop_editor_benchmark::frame_capture_requested() { - let capture_path = path.clone(); - let mut frame_cb = frame_cb; - Box::new(move |output, layout| { - stop_editor_benchmark::capture_output( - stop_editor_benchmark::CaptureKind::Ordinary, - &capture_path, - &output, - ); - frame_cb(output, layout); - }) - } else { - frame_cb - }; - - let (audio_output, startup, live_handoff) = app - .state::() - .take_startup(&path) - .await?; - let instance = { - let app = app.clone(); - EditorInstance::new_with_startup_inputs( - path, - move |state| { - let _ = EditorStateChanged::new(state).emit(&app); - }, - frame_cb, - shared_device, - cap_editor::EditorFrameFormat::Rgba, - audio_output, - startup, - ) - .await? - }; - - if let Some(handoff) = &live_handoff { - instance.install_preparing_handoff(handoff).await?; - } - - let event_id = RenderFrameEvent::listen_checked(&app, { - let preview_tx = instance.preview_tx.clone(); - move |e| { - preview_tx.send_modify(|v| { - *v = Some((e.frame_number, e.fps, e.resolution_base)); - }); - } - }); - - let started = match instance - .start_preparing_handoff(EDITOR_PREVIEW_FPS, default_editor_preview_resolution()) - .await - { - Ok(started) => started, - Err(error) => { - app.unlisten(event_id); - let cleanup_failed = if let Some(handoff) = &live_handoff { - handoff.stop_and_wait().await.cleanup_failed - } else { - false - }; - instance.dispose().await; - return Err(if cleanup_failed { - format!( - "Preparing handoff start failed: {error:?}; preparing playback cleanup failed" - ) - } else { - format!("Preparing handoff start failed: {error:?}") - }); - } - }; - if !started { - let frame = instance - .preparing_adoption() - .and_then(|adoption| adoption.frame_number(EDITOR_PREVIEW_FPS)) - .unwrap_or(0); - instance.preview_tx.send_modify(|v| { - *v = Some(( - frame, - EDITOR_PREVIEW_FPS, - default_editor_preview_resolution(), - )) - }); - } - - Ok((instance, event_id)) -} - -pub(crate) async fn wait_for_recording_ready(app: &AppHandle, path: &Path) -> Result<(), String> { - let result = wait_for_recording_ready_inner(app, path).await; - if result.is_err() { - let cleanup = editor_preparing::join_before_ordinary(app, path).await; - return result.and(cleanup); - } - result -} - -async fn await_finalization_before_ordinary( - app: &AppHandle, - path: &Path, - result: watch::Receiver, -) -> Result<(), String> { - let finalization = await_finalization_result(result).await; - let cleanup = editor_preparing::join_before_ordinary(app, path).await; - finalization.and(cleanup) -} - -async fn wait_for_recording_ready_inner(app: &AppHandle, path: &Path) -> Result<(), String> { - let display_path = path; - let project = FinalizationProject::observe(path.to_path_buf()).await?; - let path = project.work_path(); - let finalizing_state = app.state::(); - - if let Some(result) = finalizing_state.is_finalizing(&project) { - info!("Recording is being finalized, waiting for completion..."); - await_finalization_before_ordinary(app, display_path, result).await?; - project.validate_async().await?; - info!("Recording finalization completed"); - let meta = RecordingMeta::load_for_project(path) - .map_err(|e| format!("Failed to reload recording meta: {e}"))?; - if let Some(studio_meta) = meta.studio_meta() { - studio_meta.ensure_ordinary_media_access(path)?; - } - return Ok(()); - } - - editor_preparing::join_before_ordinary(app, display_path).await?; - - let meta = match RecordingMeta::load_for_project(path) { - Ok(meta) => meta, - Err(e) => { - return Err(format!("Failed to load recording meta: {e}")); - } - }; - - if let Some(studio_meta) = meta.studio_meta() - && matches!(studio_meta.status(), StudioRecordingStatus::InProgress) - { - info!("Recording/import is in progress, waiting for completion..."); - const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250); - const MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(600); - let start = std::time::Instant::now(); - - loop { - if start.elapsed() > MAX_WAIT { - return Err("Timed out waiting for import to complete".to_string()); - } - - tokio::time::sleep(POLL_INTERVAL).await; - - project.validate_async().await?; - if let Some(result) = finalizing_state.is_finalizing(&project) { - await_finalization_before_ordinary(app, display_path, result).await?; - break; - } - - let current_meta = match RecordingMeta::load_for_project(path) { - Ok(m) => m, - Err(_) => continue, - }; - - if let Some(current_studio) = current_meta.studio_meta() { - match current_studio.status() { - StudioRecordingStatus::Complete => { - info!("Recording/import completed"); - break; - } - StudioRecordingStatus::Failed { error } => { - return Err(format!("Import failed: {error}")); - } - StudioRecordingStatus::InProgress => continue, - StudioRecordingStatus::NeedsRemux => break, - } - } - } - } - - project.validate_async().await?; - if let Some(result) = finalizing_state.is_finalizing(&project) { - await_finalization_before_ordinary(app, display_path, result).await?; - project.validate_async().await?; - } - - let meta = RecordingMeta::load_for_project(path) - .map_err(|e| format!("Failed to reload recording meta: {e}"))?; - if let Some(studio_meta) = meta.studio_meta() { - studio_meta.ensure_ordinary_media_access(path)?; - } - - if let Some(studio_meta) = meta.studio_meta() - && recording::needs_fragment_remux(path, studio_meta) - { - info!("Recording needs remux (crash recovery), starting remux..."); - let work_project = FinalizationProject::admit(project.display_path().to_path_buf()).await?; - if work_project.identity != project.identity { - return Err(FinalizationProject::identity_error( - project.display_path(), - "Recording directory changed before recovery", - )); - } - match finalizing_state.request(work_project, false, FinalizationOrigin::Recording, false)? { - FinalizationRequest::Started(token) => { - run_finalization_worker(token, recording::remux_fragmented_recording).await?; - } - FinalizationRequest::Existing(result) => { - await_finalization_before_ordinary(app, display_path, result).await? - } - } - project.validate_async().await?; - info!("Crash recovery remux completed"); - } - - if meta.studio_meta().is_some() { - // Repair video tracks that were stamped at the wrong rate by older - // recorders (slow-motion display on high-refresh Windows monitors, - // 2x-length camera). Non-fatal: the editor still opens with the - // unhealed files if this fails. - let path = path.to_path_buf(); - match tokio::task::spawn_blocking(move || { - cap_recording::track_heal::heal_stretched_tracks(&path) - }) - .await - { - Ok(Ok(true)) => info!("Healed stretched video track(s)"), - Ok(Ok(false)) => {} - Ok(Err(e)) => warn!("Track heal check failed: {e:#}"), - Err(e) => warn!("Track heal task panicked: {e}"), - } - } - - project.validate_async().await?; - Ok(()) -} - -fn recordings_path(app: &AppHandle) -> PathBuf { - general_settings::GeneralSettingsStore::recordings_dir(app) -} - -// fn recording_path(app: &AppHandle, recording_id: &str) -> PathBuf { -// recordings_path(app).join(format!("{recording_id}.cap")) -// } - -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 screenshot_path(app: &AppHandle, screenshot_id: &str) -> PathBuf { -// screenshots_path(app).join(format!("{screenshot_id}.cap")) -// } - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app))] -fn global_message_dialog(app: AppHandle, message: String) { - app.dialog().message(message).show(|_| {}); -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(clipboard))] -async fn write_clipboard_string( - clipboard: MutableState<'_, ClipboardContext>, - text: String, -) -> Result<(), String> { - let writer = clipboard - .try_write() - .map_err(|e| format!("Failed to acquire lock on clipboard state: {e}"))?; - writer - .set_text(text) - .map_err(|e| format!("Failed to write text to clipboard: {e}")) -} - -#[tauri::command(async)] -#[specta::specta] -fn format_project_name( - template: Option, - target_name: String, - target_kind: String, - recording_mode: RecordingMode, - datetime: Option>, -) -> String { - recording::format_project_name( - template.as_deref(), - target_name.as_str(), - target_kind.as_str(), - recording_mode, - datetime, - ) -} - -trait EventExt: tauri_specta::Event { - fn listen_any_spawn( - app: &AppHandle, - handler: impl Fn(Self, AppHandle) -> Fut + Send + 'static + Clone, - ) -> tauri::EventId - where - Fut: Future + Send, - Self: serde::de::DeserializeOwned + Send + 'static, - { - let app = app.clone(); - Self::listen_any(&app.clone(), move |e| { - let app = app.clone(); - let handler = handler.clone(); - tokio::spawn(async move { - (handler)(e.payload, app).await; - }); - }) - } -} - -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(|_| {}); -} - -/// Everything a close request on the main window does -- hide it, drop the -/// preview inputs when nothing is recording, and hand the foreground back to -/// the editor that started a "Record a new clip" flow. -pub(crate) fn dismiss_main_window(app: &AppHandle) { - clean_capture::cancel_closed_preflight(app); - hide_main_window(app); - - #[cfg(target_os = "macos")] - crate::permissions::schedule_macos_dock_visibility_sync(app); - - let Some(state) = app.try_state::>() else { - warn!("App state unavailable during main window close request"); - return; - }; - let is_recording = state - .try_read() - .map(|s| s.is_recording_active_or_pending()) - .unwrap_or(true); - - if !is_recording { - if let Some(camera_window) = CapWindowId::Camera.get(app) { - let _ = camera_window.hide(); - } - - close_target_select_overlays(app); - - let app_for_cleanup = app.clone(); - spawn_on_runtime(async move { - let app = app_for_cleanup; - let Some(state) = app.try_state::>() else { - warn!("App state unavailable during main window close cleanup"); - return; - }; - - let (mic_feed, camera_feed) = { - let mut app_state = state.write().await; - app_state.camera_preview.pause(); - app_state.applied_mic_input.invalidate(); - (app_state.mic_feed.clone(), app_state.camera_feed.clone()) - }; - - let _ = - tokio::time::timeout(APP_EXIT_STEP_TIMEOUT, mic_feed.ask(microphone::RemoveInput)) - .await; - let _ = tokio::time::timeout( - APP_EXIT_STEP_TIMEOUT, - camera_feed.ask(feeds::camera::RemoveInput), - ) - .await; - - let mut app_state = state.write().await; - app_state.selected_mic_label = None; - app_state.camera_in_use = false; - }); - - if let Some(editor_path) = editor_recording::abort(app) { - editor_recording::reveal_editor(app, &editor_path); - } - } -} - -pub(crate) fn hide_main_window(app: &AppHandle) { - if let Some(main_window) = CapWindowId::Main.get(app) - && main_window.hide().is_ok() - { - let _ = main_window.emit_to(CapWindowId::Main.label(), "main-window-hidden", ()); - } -} - -fn open_importable_from_path(path: &Path, app: AppHandle) -> Result<(), String> { - if import::is_supported_video_import_path(path) { - let source_path = path.to_path_buf(); - tokio::spawn(async move { - match import::start_video_import(app.clone(), source_path).await { - Ok(project_path) => { - if let Err(err) = (ShowCapWindow::Editor { project_path }).show(&app).await { - error!("Failed to show imported video editor: {err}"); - show_import_error_dialog( - &app, - format!("Failed to open imported video: {err}"), - ); - return; - } - - hide_main_window(&app); - } - Err(err) => { - error!("Failed to import dropped video: {err}"); - show_import_error_dialog(&app, format!("Failed to import video: {err}")); - } - } - }); - return Ok(()); - } - - if import::is_supported_image_import_path(path) { - let source_path = path.to_path_buf(); - tokio::spawn(async move { - match import::start_image_import(app.clone(), source_path).await { - Ok(path) => { - if let Err(err) = (ShowCapWindow::ScreenshotEditor { path }).show(&app).await { - error!("Failed to show imported image editor: {err}"); - show_import_error_dialog( - &app, - format!("Failed to open imported image: {err}"), - ); - return; - } - - hide_main_window(&app); - } - Err(err) => { - error!("Failed to import dropped image: {err}"); - show_import_error_dialog(&app, format!("Failed to import image: {err}")); - } - } - }); - return Ok(()); - } - - open_project_from_path(path, app) -} - -fn open_project_from_path(path: &Path, app: AppHandle) -> Result<(), String> { - #[cfg(target_os = "macos")] - { - let gate = app - .try_state::() - .ok_or_else(|| "Cap startup is not ready to receive projects".to_string())?; - let ready = { - let queue = gate - .0 - .lock() - .map_err(|_| "Cap startup file-open state is unavailable".to_string())?; - !queue.cancelled && queue.destination == Some(StartupOpenDestination::Desktop) - }; - if !ready { - let path = if path.is_absolute() { - path.to_path_buf() - } else { - std::env::current_dir() - .map_err(|error| error.to_string())? - .join(path) - }; - let url = tauri::Url::from_file_path(path) - .map_err(|_| "The requested project path is invalid".to_string())?; - return queue_macos_startup_urls(&app, vec![url]); - } - } - - let meta = RecordingMeta::load_for_project(path).map_err(|v| v.to_string())?; - - match &meta.inner { - RecordingMetaInner::Studio(meta) => { - let status = meta.status(); - if let StudioRecordingStatus::Failed { .. } = status { - return Err("Unable to open failed recording".to_string()); - } else if let StudioRecordingStatus::InProgress = status { - return Err("Recording in progress".to_string()); - } - - let project_path = path.to_path_buf(); - tokio::spawn(async move { - if let Err(error) = (ShowCapWindow::Editor { project_path }).show(&app).await { - warn!(%error, "Could not show the requested project editor"); - } - }); - } - RecordingMetaInner::Instant(_) => { - let mp4_path = path.join("content/output.mp4"); - - if mp4_path.exists() && mp4_path.is_file() { - let _ = app - .opener() - .open_path(mp4_path.to_str().unwrap_or_default(), None::); - hide_main_window(&app); - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod startup_project_open_tests { - use super::{StartupOpenDestination, StartupOpenGate, StartupOpenGuard, StartupOpenQueue}; - - fn project(name: &str) -> tauri::Url { - tauri::Url::parse(&format!("file:///recordings/{name}.cap")).unwrap() - } - - #[test] - fn early_file_opens_wait_for_full_desktop_startup() { - let mut queue = StartupOpenQueue::default(); - let urls = vec![project("first"), project("second")]; - assert!(queue.request(urls.clone()).unwrap().is_none()); - let dispatch = queue.finish(StartupOpenDestination::Desktop).unwrap(); - assert_eq!(dispatch.destination, StartupOpenDestination::Desktop); - assert_eq!(dispatch.urls, urls); - assert!(queue.urls.is_empty()); - assert!(queue.finish(StartupOpenDestination::Desktop).is_none()); - } - - #[test] - fn queued_files_follow_gpui_handoff_without_opening_classic_editors() { - let mut queue = StartupOpenQueue::default(); - let url = project("handoff"); - assert!(queue.request(vec![url.clone()]).unwrap().is_none()); - let dispatch = queue.finish(StartupOpenDestination::Gpui).unwrap(); - assert_eq!(dispatch.destination, StartupOpenDestination::Gpui); - assert_eq!(dispatch.urls, [url]); - assert!(queue.request(vec![project("later")]).unwrap().is_none()); - let dispatch = queue.next_gpui_batch().unwrap(); - assert_eq!(dispatch.destination, StartupOpenDestination::Gpui); - assert_eq!(dispatch.urls, [project("later")]); - } - - #[test] - fn normal_post_ready_file_opens_dispatch_immediately() { - let mut queue = StartupOpenQueue::default(); - assert!(queue.finish(StartupOpenDestination::Desktop).is_none()); - let url = project("ready"); - let dispatch = queue.request(vec![url.clone()]).unwrap().unwrap(); - assert_eq!(dispatch.destination, StartupOpenDestination::Desktop); - assert_eq!(dispatch.urls, [url]); - } - - #[test] - fn failed_or_cancelled_setup_discards_pending_opens() { - let gate = StartupOpenGate::default(); - let guard = StartupOpenGuard(gate.clone()); - assert!( - gate.0 - .lock() - .unwrap() - .request(vec![project("pending")]) - .unwrap() - .is_none() - ); - drop(guard); - let mut queue = gate.0.lock().unwrap(); - assert!(queue.urls.is_empty()); - assert!(queue.request(vec![project("later")]).is_err()); - assert!(queue.finish(StartupOpenDestination::Desktop).is_none()); - assert!(queue.finish(StartupOpenDestination::Gpui).is_none()); - } - - #[test] - fn completed_setup_guard_preserves_selected_destination() { - for destination in [ - StartupOpenDestination::Desktop, - StartupOpenDestination::Gpui, - ] { - let gate = StartupOpenGate::default(); - let guard = StartupOpenGuard(gate.clone()); - assert!(gate.0.lock().unwrap().finish(destination).is_none()); - drop(guard); - let dispatch = gate - .0 - .lock() - .unwrap() - .request(vec![project("ready")]) - .unwrap() - .unwrap(); - assert_eq!(dispatch.destination, destination); - } - } - - #[test] - fn pending_file_open_queue_is_bounded_and_deduplicates() { - let mut queue = StartupOpenQueue::default(); - for _ in 0..100 { - assert!(queue.request(vec![project("same")]).unwrap().is_none()); - } - assert_eq!(queue.urls.len(), 1); - let urls = (0..100).map(|index| project(&index.to_string())).collect(); - assert!(queue.request(urls).is_err()); - assert_eq!(queue.urls, [project("same")]); - let urls = (0..63).map(|index| project(&index.to_string())).collect(); - assert!(queue.request(urls).unwrap().is_none()); - assert_eq!(queue.urls.len(), 64); - assert!(queue.request(vec![project("overflow")]).is_err()); - } - - #[test] - fn gpui_handoff_drains_later_batches_without_duplicate_dispatches() { - let mut queue = StartupOpenQueue::default(); - assert!(queue.finish(StartupOpenDestination::Gpui).is_none()); - let first = queue.request(vec![project("first")]).unwrap().unwrap(); - assert_eq!(first.urls, [project("first")]); - assert!( - queue - .request(vec![project("first"), project("second")]) - .unwrap() - .is_none() - ); - assert!(queue.request(vec![project("second")]).unwrap().is_none()); - let second = queue.next_gpui_batch().unwrap(); - assert_eq!(second.urls, [project("second")]); - assert_eq!(second.destination, StartupOpenDestination::Gpui); - assert!(queue.request(vec![project("third")]).unwrap().is_none()); - assert_eq!(queue.next_gpui_batch().unwrap().urls, [project("third")]); - assert!(queue.next_gpui_batch().is_none()); - assert!(queue.request(vec![project("after-exit")]).is_err()); - } - - #[test] - fn cancellation_stops_desktop_dispatch_and_gpui_pending_batches() { - for destination in [ - StartupOpenDestination::Desktop, - StartupOpenDestination::Gpui, - ] { - let mut queue = StartupOpenQueue::default(); - assert!(queue.finish(destination).is_none()); - assert!(queue.request(vec![project("first")]).unwrap().is_some()); - let _ = queue.request(vec![project("pending")]).unwrap(); - queue.cancel(); - assert!(queue.request(vec![project("after-exit")]).is_err()); - assert!(queue.next_gpui_batch().is_none()); - assert!(queue.urls.is_empty()); - assert!(queue.gpui_dispatched.is_empty()); - } - } -} - -#[cfg(test)] -mod editor_title_save_tests { - use super::{AppExitState, EditorTitleSaveResponse, wait_for_editor_title_saves}; - use std::{collections::HashSet, time::Duration}; - use tokio::sync::mpsc; - - fn response(request: &str, window: &str, error: Option<&str>) -> EditorTitleSaveResponse { - EditorTitleSaveResponse { - request_id: request.into(), - window_label: window.into(), - error: error.map(str::to_string), - } - } - - #[test] - fn failed_title_flush_releases_admission_without_stopping_device_watchers() { - let state = AppExitState::default(); - let pending = state.begin_title_flush().unwrap(); - assert!(state.exit_pending()); - assert!(!state.is_exiting()); - assert!(state.begin_title_flush().is_none()); - drop(pending); - assert!(!state.exit_pending()); - assert!(!state.is_exiting()); - let pending = state.begin_title_flush().unwrap(); - assert!(state.begin()); - drop(pending); - assert!(state.is_exiting()); - assert!(state.exit_pending()); - } - - #[tokio::test] - async fn quit_waits_for_every_editor_and_ignores_stale_or_duplicate_responses() { - let (sender, responses) = mpsc::unbounded_channel(); - let pending = HashSet::from(["editor-1".into(), "editor-2".into()]); - sender.send(response("previous", "editor-1", None)).unwrap(); - sender.send(response("current", "unknown", None)).unwrap(); - sender.send(response("current", "editor-1", None)).unwrap(); - sender.send(response("current", "editor-1", None)).unwrap(); - let waiting = - wait_for_editor_title_saves("current", pending, responses, Duration::from_secs(1)); - tokio::pin!(waiting); - assert!(futures::poll!(&mut waiting).is_pending()); - sender.send(response("current", "editor-2", None)).unwrap(); - assert_eq!(waiting.await, Ok(())); - } - - #[tokio::test] - async fn one_editor_save_failure_prevents_quit() { - let (sender, responses) = mpsc::unbounded_channel(); - sender - .send(response("current", "editor-1", Some("disk full"))) - .unwrap(); - let result = wait_for_editor_title_saves( - "current", - HashSet::from(["editor-1".into(), "editor-2".into()]), - responses, - Duration::from_secs(1), - ) - .await; - assert!(result.unwrap_err().contains("disk full")); - } - - #[tokio::test] - async fn missing_editor_acknowledgement_does_not_authorize_quit() { - let (_sender, responses) = mpsc::unbounded_channel(); - let result = wait_for_editor_title_saves( - "current", - HashSet::from(["editor-1".into()]), - responses, - Duration::from_millis(1), - ) - .await; - assert!(result.unwrap_err().contains("Could not confirm")); - } - - #[tokio::test] - async fn closed_response_channel_does_not_authorize_quit() { - let (sender, responses) = mpsc::unbounded_channel(); - drop(sender); - let result = wait_for_editor_title_saves( - "current", - HashSet::from(["editor-1".into()]), - responses, - Duration::from_secs(1), - ) - .await; - assert!(result.unwrap_err().contains("connection closed")); - } - - #[tokio::test] - async fn no_editor_requires_no_acknowledgement() { - let (_sender, responses) = mpsc::unbounded_channel(); - assert_eq!( - wait_for_editor_title_saves( - "current", - HashSet::new(), - responses, - Duration::from_millis(1), - ) - .await, - Ok(()) - ); - } -} - -#[cfg(test)] -mod studio_microphone_ownership_tests { - use super::{ - MicrophoneRemovalError, RequestedInputsState, acknowledge_studio_microphone, - replace_owned_microphone, wait_for_microphone_removal, - }; - use std::{ - path::{Path, PathBuf}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, AtomicUsize, Ordering}, - }, - time::Duration, - }; - use tokio::sync::oneshot; - - struct Lock { - drops: Arc, - unlock: Option>, - } - - impl Drop for Lock { - fn drop(&mut self) { - self.drops.fetch_add(1, Ordering::SeqCst); - if let Some(unlock) = self.unlock.take() { - let _ = unlock.send(()); - } - } - } - - #[tokio::test] - async fn detach_drops_app_lock_after_ack_and_waits_for_unlock_and_removal() { - let drops = Arc::new(AtomicUsize::new(0)); - let (unlock_tx, unlock_rx) = oneshot::channel(); - let lock = Arc::new(Lock { - drops: drops.clone(), - unlock: Some(unlock_tx), - }); - let app_slot = Mutex::new(Some(lock.clone())); - let actor_slot = Mutex::new(Some(lock)); - let (ack_tx, ack_rx) = oneshot::channel(); - let unlocked = AtomicBool::new(false); - let removed = AtomicBool::new(false); - let attempts = AtomicUsize::new(0); - let transition = async { - acknowledge_studio_microphone( - async { - ack_rx.await.unwrap(); - drop(actor_slot.lock().unwrap().take()); - Ok(()) - }, - async { - replace_owned_microphone( - Path::new("owner"), - Some((Path::new("owner"), &mut *app_slot.lock().unwrap())), - None, - ) - }, - "detaching", - ) - .await - .unwrap(); - assert_eq!(drops.load(Ordering::SeqCst), 1); - wait_for_microphone_removal(|| async { - attempts.fetch_add(1, Ordering::SeqCst); - if unlocked.load(Ordering::SeqCst) { - removed.store(true, Ordering::SeqCst); - Ok(()) - } else { - Err(MicrophoneRemovalError::Locked) - } - }) - .await - .unwrap(); - }; - tokio::join!( - biased; - transition, - async { - assert!(app_slot.lock().unwrap().is_some()); - assert_eq!(drops.load(Ordering::SeqCst), 0); - ack_tx.send(()).unwrap(); - unlock_rx.await.unwrap(); - assert!(!removed.load(Ordering::SeqCst)); - tokio::time::sleep(Duration::from_millis(10)).await; - unlocked.store(true, Ordering::SeqCst); - } - ); - assert!(removed.load(Ordering::SeqCst)); - assert!(attempts.load(Ordering::SeqCst) >= 2); - } - - #[tokio::test] - async fn failed_detach_keeps_app_ownership() { - let drops = Arc::new(AtomicUsize::new(0)); - let mut slot = Some(Arc::new(Lock { - drops: drops.clone(), - unlock: None, - })); - let result = acknowledge_studio_microphone( - async { Err("Pause before changing input".into()) }, - async { - replace_owned_microphone( - Path::new("owner"), - Some((Path::new("owner"), &mut slot)), - None, - ) - }, - "detaching", - ) - .await; - assert!(result.is_err()); - assert!(slot.is_some()); - assert_eq!(drops.load(Ordering::SeqCst), 0); - } - - #[tokio::test] - async fn timed_out_detach_keeps_app_ownership() { - let mut slot = Some(Arc::new(1)); - let result = acknowledge_studio_microphone( - std::future::pending(), - async { - replace_owned_microphone( - Path::new("owner"), - Some((Path::new("owner"), &mut slot)), - None, - ) - }, - "detaching", - ) - .await; - assert!(result.unwrap_err().contains("Timed out")); - assert!(slot.is_some()); - } - - #[tokio::test] - async fn stale_successful_attachment_retains_actual_arc_for_next_request() { - let requests = RequestedInputsState::new(Some("A".into()), None); - let revision = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - let lock = Arc::new(42); - let actor_slot = Mutex::new(None); - let app_slot = Mutex::new(None); - let (tx, rx) = oneshot::channel(); - let (result, ()) = tokio::join!( - biased; - acknowledge_studio_microphone( - async { rx.await.unwrap(); *actor_slot.lock().unwrap() = Some(lock.clone()); Ok(()) }, - async { replace_owned_microphone(Path::new("owner"), Some((Path::new("owner"), &mut *app_slot.lock().unwrap())), Some(lock.clone())) }, - "attaching", - ), - async { - assert!(app_slot.lock().unwrap().is_none()); - let _revision = requests.inner.lock().unwrap().microphone.begin(None); - tx.send(()).unwrap(); - } - ); - result.unwrap(); - assert!(!requests.mic_is_current(revision)); - assert!(Arc::ptr_eq( - app_slot.lock().unwrap().as_ref().unwrap(), - actor_slot.lock().unwrap().as_ref().unwrap() - )); - } - - #[tokio::test] - async fn failed_attachment_does_not_retain_new_lock() { - let mut slot: Option> = None; - let result = acknowledge_studio_microphone( - async { Err("Studio stopped".into()) }, - async { - replace_owned_microphone( - Path::new("owner"), - Some((Path::new("owner"), &mut slot)), - Some(Arc::new(42)), - ) - }, - "attaching", - ) - .await; - assert!(result.is_err()); - assert!(slot.is_none()); - } - - #[tokio::test] - async fn stop_or_replacement_during_ack_does_not_overwrite_another_owner() { - for next_owner in [None, Some(PathBuf::from("new-owner"))] { - let requests = RequestedInputsState::new(None, None); - let _operation = requests.operation.lock().await; - let owner = Mutex::new(Some(PathBuf::from("old-owner"))); - let new_lock = Arc::new(9); - let slot = Mutex::new(Some(new_lock.clone())); - let (tx, rx) = oneshot::channel(); - let (result, ()) = tokio::join!( - biased; - acknowledge_studio_microphone( - async { rx.await.unwrap(); Ok(()) }, - async { - let owner = owner.lock().unwrap(); - let mut slot = slot.lock().unwrap(); - replace_owned_microphone(Path::new("old-owner"), owner.as_deref().map(|owner| (owner, &mut *slot)), Some(Arc::new(42))) - }, - "attaching", - ), - async { *owner.lock().unwrap() = next_owner; tx.send(()).unwrap(); } - ); - assert!(result.is_err()); - assert!(Arc::ptr_eq( - slot.lock().unwrap().as_ref().unwrap(), - &new_lock - )); - } - } - - #[tokio::test] - async fn locked_input_is_not_reported_removed_at_the_deadline() { - let result = tokio::time::timeout( - Duration::from_secs(2), - wait_for_microphone_removal(|| async { Err(MicrophoneRemovalError::Locked) }), - ) - .await - .unwrap(); - assert!(result.is_err()); - } - - #[tokio::test] - async fn unresponsive_remove_ask_is_bounded() { - let result = tokio::time::timeout( - Duration::from_secs(2), - wait_for_microphone_removal(std::future::pending), - ) - .await - .unwrap(); - assert!(result.unwrap_err().contains("Timed out")); - } - - #[tokio::test] - async fn pending_microphone_change_blocks_resume() { - let requests = RequestedInputsState::new(Some("A".into()), None); - let _revision = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - let error = requests.try_resume_guard().unwrap_err(); - assert!(error.contains("still being configured")); - } - - #[tokio::test] - async fn failed_microphone_change_blocks_resume_until_reselected() { - let requests = RequestedInputsState::new(Some("A".into()), None); - let revision = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - requests - .inner - .lock() - .unwrap() - .microphone - .finish(revision, &Err("B unavailable".into())); - assert!( - requests - .try_resume_guard() - .unwrap_err() - .contains("B unavailable") - ); - let none = requests.inner.lock().unwrap().microphone.begin(None); - requests - .inner - .lock() - .unwrap() - .microphone - .finish(none, &Ok(())); - assert!(requests.try_resume_guard().is_ok()); - } - - #[tokio::test] - async fn camera_setup_also_blocks_resume() { - let requests = RequestedInputsState::new(None, None); - let _revision = requests.inner.lock().unwrap().camera.begin(None); - assert!(requests.try_resume_guard().unwrap_err().contains("camera")); - } - - #[tokio::test] - async fn resume_rejects_busy_operation_without_waiting_for_it() { - let requests = RequestedInputsState::new(None, None); - let _operation = requests.operation.lock().await; - assert!( - requests - .try_resume_guard() - .unwrap_err() - .contains("Input change in progress") - ); - } - - #[tokio::test] - async fn healthy_resume_holds_operation_until_actor_acknowledgement() { - let requests = RequestedInputsState::new(Some("A".into()), None); - let (tx, rx) = oneshot::channel(); - tokio::join!( - biased; - async { - let _guard = requests.try_resume_guard().unwrap(); - rx.await.unwrap(); - }, - async { - assert!(requests.operation.try_lock().is_err()); - tx.send(()).unwrap(); - } - ); - assert!(requests.operation.try_lock().is_ok()); - } - - #[tokio::test] - async fn non_lock_failure_is_not_retried() { - let attempts = AtomicUsize::new(0); - let result = wait_for_microphone_removal(|| async { - attempts.fetch_add(1, Ordering::SeqCst); - Err(MicrophoneRemovalError::Failed("Actor unavailable".into())) - }) - .await; - assert_eq!(result.unwrap_err(), "Actor unavailable"); - assert_eq!(attempts.load(Ordering::SeqCst), 1); - } -} - -#[cfg(test)] -mod applied_microphone_tests { - use super::{ - AppliedMicrophoneInput, RequestedInputsState, finish_microphone_input_change, - wait_for_microphone_setup, - }; - use std::sync::Mutex; - use tokio::sync::oneshot; - - #[tokio::test] - async fn microphone_configuration_accepts_native_startup_longer_than_legacy_deadline() { - let result = wait_for_microphone_setup(async { - tokio::time::sleep(std::time::Duration::from_millis(1600)).await; - Ok(()) - }) - .await; - assert_eq!(result, Ok(())); - } - - #[tokio::test] - async fn microphone_configuration_preserves_backend_failure() { - let result = wait_for_microphone_setup(async { Err("device disconnected".into()) }).await; - assert_eq!(result, Err("device disconnected".into())); - } - - #[derive(Default)] - struct Feed { - applied: AppliedMicrophoneInput, - selected: Option, - actual: Option, - attached: Option, - active: bool, - set_count: usize, - remove_count: usize, - detach_count: usize, - attach_count: usize, - } - - impl Feed { - fn healthy_active() -> Self { - Self { - applied: AppliedMicrophoneInput { - valid: true, - generation: 0, - settings: None, - }, - selected: Some("A".into()), - actual: Some("A".into()), - attached: Some("A".into()), - active: true, - ..Default::default() - } - } - } - - async fn apply( - feed: &Mutex, - requests: &RequestedInputsState, - label: Option<&str>, - revision: u64, - ready: oneshot::Receiver<()>, - fail: bool, - ) -> Result<(), String> { - let generation = { - let mut feed = feed.lock().unwrap(); - let active = feed.active; - let selected = feed.selected.clone(); - if !feed - .applied - .prepare_change(active, label, selected.as_deref()) - { - return Ok(()); - } - feed.applied.generation - }; - let result = finish_microphone_input_change( - async { - { - let mut feed = feed.lock().unwrap(); - if feed.active { - feed.detach_count += 1; - feed.attached = None; - } - } - ready.await.unwrap(); - let mut feed = feed.lock().unwrap(); - if label.is_some() { - feed.set_count += 1; - } else { - feed.remove_count += 1; - } - feed.actual = if fail { - None - } else { - label.map(str::to_string) - }; - if fail { - Err("Setup failed".into()) - } else { - Ok(()) - } - }, - || requests.mic_is_current(revision), - async { - let mut feed = feed.lock().unwrap(); - if feed.active && label.is_some() { - feed.attach_count += 1; - feed.attached = feed.actual.clone(); - } - Ok(()) - }, - ) - .await; - if result.is_ok() { - let mut confirmed = false; - requests.publish_mic_if_current(revision, || { - let mut feed = feed.lock().unwrap(); - if feed.applied.confirm_if_current(generation) { - feed.selected = label.map(str::to_string); - confirmed = true; - } - }); - if !confirmed { - return Err("Input changed during setup".into()); - } - } - result - } - - fn ready() -> oneshot::Receiver<()> { - let (tx, rx) = oneshot::channel(); - tx.send(()).unwrap(); - rx - } - - #[tokio::test] - async fn stale_a_then_none_removes_actual_input_even_when_metadata_is_none() { - let feed = Mutex::new(Feed::default()); - let requests = RequestedInputsState::new(None, None); - let a = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("A".into())); - let (tx, rx) = oneshot::channel(); - let (old, none) = tokio::join!(apply(&feed, &requests, Some("A"), a, rx, false), async { - let none = requests.inner.lock().unwrap().microphone.begin(None); - tx.send(()).unwrap(); - none - }); - assert!(old.is_err()); - { - let feed = feed.lock().unwrap(); - assert_eq!(feed.actual.as_deref(), Some("A")); - assert!(feed.selected.is_none()); - assert!(!feed.applied.valid); - } - apply(&feed, &requests, None, none, ready(), false) - .await - .unwrap(); - let feed = feed.lock().unwrap(); - assert!(feed.actual.is_none()); - assert_eq!(feed.remove_count, 1); - } - - #[tokio::test] - async fn stale_b_then_a_restores_actual_feed_and_studio_attachment() { - let feed = Mutex::new(Feed::healthy_active()); - let requests = RequestedInputsState::new(Some("A".into()), None); - let b = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - let (tx, rx) = oneshot::channel(); - let (old, a) = tokio::join!(apply(&feed, &requests, Some("B"), b, rx, false), async { - let a = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("A".into())); - tx.send(()).unwrap(); - a - }); - assert!(old.is_err()); - assert!(feed.lock().unwrap().attached.is_none()); - apply(&feed, &requests, Some("A"), a, ready(), false) - .await - .unwrap(); - let feed = feed.lock().unwrap(); - assert_eq!(feed.actual.as_deref(), Some("A")); - assert_eq!(feed.attached.as_deref(), Some("A")); - assert_eq!(feed.set_count, 2); - assert_eq!(feed.detach_count, 2); - assert_eq!(feed.attach_count, 1); - } - - #[tokio::test] - async fn failed_b_leaves_dirty_state_and_a_cannot_reuse_metadata() { - let feed = Mutex::new(Feed::healthy_active()); - let requests = RequestedInputsState::new(Some("A".into()), None); - let b = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - assert!( - apply(&feed, &requests, Some("B"), b, ready(), true) - .await - .is_err() - ); - { - let feed = feed.lock().unwrap(); - assert_eq!(feed.selected.as_deref(), Some("A")); - assert!(feed.actual.is_none()); - assert!(feed.attached.is_none()); - assert!(!feed.applied.valid); - } - let a = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("A".into())); - apply(&feed, &requests, Some("A"), a, ready(), false) - .await - .unwrap(); - let feed = feed.lock().unwrap(); - assert_eq!(feed.actual.as_deref(), Some("A")); - assert_eq!(feed.attached.as_deref(), Some("A")); - assert_eq!(feed.set_count, 2); - assert_eq!(feed.attach_count, 1); - } - - #[tokio::test] - async fn disconnect_during_setup_cannot_restore_validity() { - let feed = Mutex::new(Feed::healthy_active()); - let requests = RequestedInputsState::new(Some("A".into()), None); - let b = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - let (tx, rx) = oneshot::channel(); - let (result, ()) = tokio::join!( - biased; - apply(&feed, &requests, Some("B"), b, rx, false), - async { - feed.lock().unwrap().applied.invalidate(); - tx.send(()).unwrap(); - } - ); - assert!(result.is_err()); - let feed = feed.lock().unwrap(); - assert!(!feed.applied.valid); - assert_eq!(feed.selected.as_deref(), Some("A")); - } - - #[tokio::test] - async fn healthy_active_same_input_has_zero_setup_or_detach() { - let feed = Mutex::new(Feed::healthy_active()); - let requests = RequestedInputsState::new(Some("A".into()), None); - let a = requests - .inner - .lock() - .unwrap() - .microphone - .begin(Some("A".into())); - apply(&feed, &requests, Some("A"), a, ready(), false) - .await - .unwrap(); - let feed = feed.lock().unwrap(); - assert_eq!(feed.set_count, 0); - assert_eq!(feed.detach_count, 0); - assert_eq!(feed.attach_count, 0); - assert_eq!(feed.attached.as_deref(), Some("A")); - } -} - -#[cfg(test)] -mod microphone_permission_tests { - use super::{RequestedInput, check_requested_microphone_permission}; - - #[test] - fn disabled_microphone_bypasses_permission() { - check_requested_microphone_permission(None, || { - panic!("turning the microphone off must not require permission") - }) - .unwrap(); - } - - #[test] - fn saved_microphone_permission_failure_retains_intent_and_blocks_start() { - let mut input = RequestedInput::new(Some("Saved microphone".to_string())); - let revision = input.revision; - let result = check_requested_microphone_permission(input.value.as_deref(), || { - Err("Allow microphone access in System Settings".into()) - }); - assert!(input.prepare_restore(revision)); - input.finish(revision, &result); - assert_eq!(input.value.as_deref(), Some("Saved microphone")); - assert!(!input.pending); - assert!( - input - .validate("microphone") - .unwrap_err() - .contains("System Settings") - ); - } - - #[test] - fn revoked_access_blocks_current_selection_until_reselected_after_grant() { - let mut input = RequestedInput::new(Some("Saved microphone".to_string())); - let denied = input.begin(Some("Saved microphone".into())); - let result = check_requested_microphone_permission(input.value.as_deref(), || { - Err("Microphone access is blocked".into()) - }); - input.finish(denied, &result); - assert!(input.validate("microphone").is_err()); - let granted = input.begin(Some("Saved microphone".into())); - let result = check_requested_microphone_permission(input.value.as_deref(), || Ok(())); - input.finish(granted, &result); - assert!(input.validate("microphone").is_ok()); - assert_eq!(input.value.as_deref(), Some("Saved microphone")); - } - - #[test] - fn stale_permission_failure_cannot_replace_a_newer_selection() { - let mut input = RequestedInput::::new(None); - let old = input.begin(Some("Old microphone".into())); - let current = input.begin(Some("New microphone".into())); - let result = check_requested_microphone_permission(Some("Old microphone"), || { - Err("Microphone access is blocked".into()) - }); - input.finish(old, &result); - assert!(input.pending); - assert!(input.error.is_none()); - input.finish(current, &Ok(())); - assert_eq!(input.value.as_deref(), Some("New microphone")); - assert!(input.validate("microphone").is_ok()); - } - - #[test] - fn explicit_off_clears_permission_failure_and_ignores_old_completion() { - let mut input = RequestedInput::new(Some("Saved microphone".to_string())); - let old = input.begin(Some("Saved microphone".into())); - input.finish(old, &Err("Microphone access is blocked".into())); - let off = input.begin(None); - let result = check_requested_microphone_permission(input.value.as_deref(), || { - panic!("turning the microphone off must not require permission") - }); - input.finish(off, &result); - input.finish(old, &Err("Old permission failure".into())); - assert!(input.value.is_none()); - assert!(input.validate("microphone").is_ok()); - } -} - -#[cfg(test)] -mod requested_inputs_tests { - use super::{RequestedInput, RequestedInputsState, wait_for_existing_input}; - - #[test] - fn matching_pending_requests_share_setup_but_changed_configuration_supersedes_it() { - let mut input = RequestedInput::new(None::); - let settings = serde_json::json!({ "sampleRate": 48_000 }); - let (first, joined) = input.begin_or_join(Some("mic".into()), settings.clone(), false); - assert!(!joined); - assert_eq!( - input.begin_or_join(Some("mic".into()), settings.clone(), false), - (first, true) - ); - let (second, joined) = input.begin_or_join( - Some("mic".into()), - serde_json::json!({ "sampleRate": 44_100 }), - false, - ); - assert!(!joined); - assert_ne!(first, second); - input.finish(first, &Ok(())); - assert!(input.pending); - input.finish(second, &Err("disconnected".into())); - let (retry, joined) = input.begin_or_join(Some("mic".into()), settings, false); - assert!(!joined); - assert_ne!(retry, second); - } - - #[tokio::test] - async fn restoring_identical_inputs_during_startup_keeps_recording_publishable() { - let state = RequestedInputsState::new(None, None); - let mic = Some("MacBook Pro Microphone".to_string()); - let camera = Some(super::DeviceOrModelID::DeviceID( - "MacBook Pro Camera".into(), - )); - let mic_settings = serde_json::json!(null); - let camera_settings = serde_json::json!((None::, false)); - let (mic_revision, camera_revision) = { - let mut inputs = state.inner.lock().unwrap(); - let (mic_revision, _) = - inputs - .microphone - .begin_or_join(mic.clone(), mic_settings.clone(), false); - inputs.microphone.finish(mic_revision, &Ok(())); - let (camera_revision, _) = - inputs - .camera - .begin_or_join(camera.clone(), camera_settings.clone(), false); - inputs.camera.finish(camera_revision, &Ok(())); - (mic_revision, camera_revision) - }; - let snapshot = state.ready_snapshot().unwrap(); - let _startup = state.operation.lock().await; - { - let mut inputs = state.inner.lock().unwrap(); - assert_eq!( - inputs.microphone.begin_or_join(mic, mic_settings, true), - (mic_revision, true) - ); - assert_eq!( - inputs.camera.begin_or_join(camera, camera_settings, true), - (camera_revision, true) - ); - } - for (revision, kind) in [(mic_revision, "Microphone"), (camera_revision, "Camera")] { - let result = tokio::time::timeout(std::time::Duration::from_millis(100), async { - if kind == "Microphone" { - wait_for_existing_input(revision, kind, || state.snapshot().microphone).await - } else { - wait_for_existing_input(revision, kind, || state.snapshot().camera).await - } - }) - .await - .unwrap(); - assert_eq!(result, Ok(())); - } - assert!(state.ready_snapshot().is_ok()); - assert!(state.is_current(&snapshot)); - let mut published = false; - assert!(state.publish_if_current(&snapshot, || published = true)); - assert!(published); - } - - #[test] - fn startup_still_rejects_changed_devices_settings_and_disabled_inputs() { - for (value, settings) in [ - (Some("other".to_string()), serde_json::json!(48_000)), - (Some("mic".to_string()), serde_json::json!(44_100)), - (None, serde_json::json!(48_000)), - ] { - let state = RequestedInputsState::new(None, None); - { - let mut inputs = state.inner.lock().unwrap(); - let (revision, _) = inputs.microphone.begin_or_join( - Some("mic".into()), - serde_json::json!(48_000), - false, - ); - inputs.microphone.finish(revision, &Ok(())); - } - let snapshot = state.ready_snapshot().unwrap(); - let (_, joined) = state - .inner - .lock() - .unwrap() - .microphone - .begin_or_join(value, settings, true); - assert!(!joined); - assert!(!state.is_current(&snapshot)); - assert!(!state.publish_if_current(&snapshot, || panic!("changed input published"))); - } - } - - #[test] - fn completed_requests_are_retried_when_idle_or_previously_failed() { - for (recording_starting, result) in - [(false, Ok(())), (true, Err("disconnected".to_string()))] - { - let mut input = RequestedInput::new(None::); - let (first, _) = - input.begin_or_join(Some("mic".into()), serde_json::json!(null), false); - input.finish(first, &result); - let (retry, joined) = input.begin_or_join( - Some("mic".into()), - serde_json::json!(null), - recording_starting, - ); - assert!(!joined); - assert_ne!(retry, first); - assert!(input.pending); - assert!(input.error.is_none()); - } - } - - #[tokio::test] - async fn joined_input_requests_report_completion_failure_and_supersession() { - for result in [Ok(()), Err("device unavailable".to_string())] { - let input = std::sync::Mutex::new(RequestedInput::new(None::)); - let revision = input.lock().unwrap().begin(Some("mic".into())); - let waiter = - wait_for_existing_input(revision, "Microphone", || input.lock().unwrap().clone()); - let finish = async { - tokio::task::yield_now().await; - input.lock().unwrap().finish(revision, &result); - }; - let (observed, ()) = tokio::join!(waiter, finish); - assert_eq!(observed, result); - input.lock().unwrap().begin(Some("different".into())); - assert!( - wait_for_existing_input(revision, "Microphone", || input.lock().unwrap().clone()) - .await - .unwrap_err() - .contains("superseded") - ); - } - } - - #[test] - fn persisted_intent_is_available_before_preview_setup() { - let state = RequestedInputsState::new(Some("Missing microphone".into()), None); - let snapshot = state.ready_snapshot().unwrap(); - assert_eq!( - snapshot.microphone.value.as_deref(), - Some("Missing microphone") - ); - assert!(snapshot.camera.value.is_none()); - } - - #[test] - fn failed_change_retains_requested_device_and_blocks_start() { - let mut input = RequestedInput::new(Some("A".to_string())); - let revision = input.begin(Some("B".to_string())); - input.finish(revision, &Err("B unavailable".into())); - assert_eq!(input.value.as_deref(), Some("B")); - assert!( - input - .validate("microphone") - .unwrap_err() - .contains("B unavailable") - ); - } - - #[test] - fn stale_completion_cannot_finish_newer_request() { - let mut input = RequestedInput::new(None); - let old = input.begin(Some("A".to_string())); - let current = input.begin(Some("B".to_string())); - input.finish(old, &Err("A failed".into())); - assert!(input.pending); - assert!(input.error.is_none()); - assert_eq!(input.value.as_deref(), Some("B")); - input.finish(current, &Ok(())); - assert!(input.validate("microphone").is_ok()); - } - - #[test] - fn rapid_requests_preserve_explicit_none() { - let mut input = RequestedInput::new(None); - let first = input.begin(Some("A".to_string())); - let second = input.begin(Some("B".to_string())); - let none = input.begin(None); - input.finish(none, &Ok(())); - input.finish(first, &Ok(())); - input.finish(second, &Err("B unavailable".into())); - assert!(input.value.is_none()); - assert!(input.validate("microphone").is_ok()); - } - - #[test] - fn restore_cannot_replace_newer_or_pending_request() { - let mut input = RequestedInput::new(Some("A".to_string())); - let old = input.revision; - let current = input.begin(None); - assert!(!input.prepare_restore(old)); - assert!(!input.prepare_restore(current)); - input.finish(current, &Ok(())); - assert!(input.prepare_restore(current)); - assert!(input.value.is_none()); - } - - #[test] - fn start_is_blocked_while_setup_is_pending_or_failed() { - let state = RequestedInputsState::new(None, None); - let revision = state - .inner - .lock() - .unwrap() - .microphone - .begin(Some("A".into())); - assert!(state.ready_snapshot().is_err()); - state - .inner - .lock() - .unwrap() - .microphone - .finish(revision, &Err("Unavailable".into())); - assert!(state.ready_snapshot().is_err()); - let none = state.inner.lock().unwrap().microphone.begin(None); - state.inner.lock().unwrap().microphone.finish(none, &Ok(())); - assert!(state.ready_snapshot().is_ok()); - } - - #[test] - fn changed_request_prevents_recording_publication() { - let state = RequestedInputsState::new(Some("A".into()), None); - let snapshot = state.ready_snapshot().unwrap(); - let _revision = state - .inner - .lock() - .unwrap() - .microphone - .begin(Some("B".into())); - let mut published = false; - assert!(!state.publish_if_current(&snapshot, || published = true)); - assert!(!published); - } - - #[test] - fn stale_camera_restore_cannot_publish_runtime_state() { - let state = RequestedInputsState::new(None, None); - let revision = state.snapshot().camera.revision; - let _new_revision = state.inner.lock().unwrap().camera.begin(None); - let mut published = false; - assert!(!state.publish_camera_if_current(revision, || published = true)); - assert!(!published); - } - - #[test] - fn stale_microphone_completion_cannot_publish_runtime_state() { - let state = RequestedInputsState::new(None, None); - let revision = state.snapshot().microphone.revision; - let _new_revision = state.inner.lock().unwrap().microphone.begin(None); - let mut published = false; - assert!(!state.publish_mic_if_current(revision, || published = true)); - assert!(!published); - } - - #[test] - fn unchanged_request_can_publish_recording() { - let state = RequestedInputsState::new(None, None); - let snapshot = state.ready_snapshot().unwrap(); - let mut published = false; - assert!(state.publish_if_current(&snapshot, || published = true)); - assert!(published); - } -} - -#[cfg(test)] -mod screenshot_share_cache_tests { - use super::*; - - fn sharing(content_hash: Option<&str>) -> SharingMeta { - SharingMeta { - id: String::from("video-id"), - link: String::from("https://cap.so/s/video-id"), - content_hash: content_hash.map(str::to_string), - } - } - - #[test] - fn reuses_link_when_content_hash_matches() { - let link = - screenshot_share_link_for_hash(Some(&sharing(Some("hash-a"))), "hash-a").unwrap(); - - assert_eq!(link, "https://cap.so/s/video-id"); - } - - #[test] - fn does_not_reuse_link_when_content_hash_changed() { - let link = screenshot_share_link_for_hash(Some(&sharing(Some("hash-a"))), "hash-b"); - - assert!(link.is_none()); - } - - #[test] - fn does_not_reuse_legacy_link_without_content_hash() { - let link = screenshot_share_link_for_hash(Some(&sharing(None)), "hash-a"); - - assert!(link.is_none()); - } -} - -#[cfg(test)] -mod recording_delete_path_tests { - use super::recording_delete_target; - - #[test] - fn delete_target_rejects_an_allowed_root_nested_in_another_allowed_root() { - let temp = tempfile::tempdir().unwrap(); - let recordings = temp.path().join("recordings"); - let nested = recordings.join("nested"); - let bundle = nested.join("keep-me.cap"); - std::fs::create_dir_all(&bundle).unwrap(); - let dirs = [recordings, nested.clone()]; - - for path in [&nested, &nested.canonicalize().unwrap()] { - assert_eq!( - recording_delete_target(&dirs, path), - Err("Path is not inside a recordings directory".to_string()) - ); - assert!(bundle.is_dir()); - } - assert_eq!( - recording_delete_target(&dirs, &bundle), - Ok(Some(bundle.canonicalize().unwrap())) - ); - } - - #[test] - fn delete_target_accepts_only_strict_children_of_a_recordings_root() { - let temp = tempfile::tempdir().unwrap(); - let recordings = temp.path().join("recordings"); - let bundle = recordings.join("delete-me.cap"); - let outside = temp.path().join("outside.cap"); - std::fs::create_dir_all(&bundle).unwrap(); - std::fs::create_dir_all(&outside).unwrap(); - - assert_eq!( - recording_delete_target(std::slice::from_ref(&recordings), &bundle), - Ok(Some(bundle.canonicalize().unwrap())) - ); - assert_eq!( - recording_delete_target(std::slice::from_ref(&recordings), &recordings), - Err("Path is not inside a recordings directory".to_string()) - ); - assert_eq!( - recording_delete_target(std::slice::from_ref(&recordings), &outside), - Err("Path is not inside a recordings directory".to_string()) - ); - assert_eq!( - recording_delete_target( - std::slice::from_ref(&recordings), - &recordings.join("..").join("outside.cap"), - ), - Err("Invalid path".to_string()) - ); - assert_eq!( - recording_delete_target( - std::slice::from_ref(&recordings), - &recordings.join("already-gone.cap"), - ), - Ok(None) - ); - } - - #[cfg(unix)] - #[test] - fn delete_target_rejects_a_symlink_escape() { - let temp = tempfile::tempdir().unwrap(); - let recordings = temp.path().join("recordings"); - let victim = temp.path().join("victim"); - let escape = recordings.join("escape.cap"); - std::fs::create_dir_all(&recordings).unwrap(); - std::fs::create_dir_all(&victim).unwrap(); - std::fs::write(victim.join("keep.txt"), b"keep").unwrap(); - std::os::unix::fs::symlink(&victim, &escape).unwrap(); - - assert_eq!( - recording_delete_target(&[recordings], &escape), - Err("Path is not inside a recordings directory".to_string()) - ); - assert!(victim.join("keep.txt").is_file()); - } - - #[cfg(windows)] - #[test] - fn delete_target_accepts_a_canonical_windows_project_under_a_raw_root() { - let temp = tempfile::tempdir().unwrap(); - let recordings = temp.path().join("recordings"); - let bundle = recordings.join("delete-me.cap"); - std::fs::create_dir_all(&bundle).unwrap(); - let canonical_bundle = bundle.canonicalize().unwrap(); - - assert_eq!( - recording_delete_target(std::slice::from_ref(&recordings), &canonical_bundle), - Ok(Some(canonical_bundle.clone())) - ); - std::fs::remove_dir_all(&canonical_bundle).unwrap(); - assert_eq!( - recording_delete_target(&[recordings], &canonical_bundle), - Ok(None) - ); - } -} - -#[cfg(test)] -mod typescript_bindings_tests { - #[test] - fn export_typescript_bindings() { - let bindings_path = std::path::Path::new("../src/utils/tauri.ts"); - if bindings_path.parent().is_some_and(|parent| parent.exists()) { - super::specta_builder() - .export(super::typescript_exporter(), bindings_path) - .expect("failed to export TypeScript bindings"); - } - } -} - -#[cfg(test)] -mod instant_resume_safety_tests { - use super::*; - - #[test] - fn old_recordings_are_reconciled_once_but_skipped_before_periodic_metadata_reads() { - let path = project("old", InstantRecordingMeta::InProgress { recording: true }); - let later = path.metadata().unwrap().created().unwrap() + Duration::from_secs(25 * 60 * 60); - assert!( - load_upload_resume_candidate_at(&path, true, later) - .unwrap() - .is_none() - ); - assert!(matches!( - RecordingMeta::load_for_project(&path).unwrap().inner, - RecordingMetaInner::Instant(InstantRecordingMeta::Failed { .. }) - )); - std::fs::write(path.join("recording-meta.json"), b"invalid").unwrap(); - assert!( - load_upload_resume_candidate_at(&path, false, later) - .unwrap() - .is_none() - ); - assert_eq!( - std::fs::read(path.join("recording-meta.json")).unwrap(), - b"invalid" - ); - std::fs::remove_dir_all(path).unwrap(); - } - fn project(tag: &str, inner: InstantRecordingMeta) -> PathBuf { - static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); - let path = std::env::temp_dir().join(format!( - "cap-instant-recovery-{tag}-{}-{}", - std::process::id(), - NEXT.fetch_add(1, std::sync::atomic::Ordering::AcqRel) - )); - std::fs::create_dir_all(&path).unwrap(); - let meta = RecordingMeta { - platform: None, - project_path: path.clone(), - pretty_name: "Synthetic recovery".into(), - sharing: None, - inner: RecordingMetaInner::Instant(inner), - upload: Some(UploadMeta::SegmentUpload { - video_id: "synthetic".into(), - recording_dir: path.clone(), - pre_created_video: VideoUploadInfo { - id: "synthetic".into(), - link: "https://example.invalid/s/synthetic".into(), - config: cap_project::S3UploadMeta { - id: "synthetic".into(), - }, - }, - }), - }; - meta.save_for_project().unwrap(); - path - } - #[test] - fn crashed_and_failed_projects_never_become_automatic_upload_candidates() { - for inner in [ - InstantRecordingMeta::InProgress { recording: true }, - InstantRecordingMeta::Failed { - error: "required audio failed".into(), - }, - ] { - let path = project("failed", inner); - assert!(load_instant_resume_candidate(&path).unwrap().is_none()); - let meta = RecordingMeta::load_for_project(&path).unwrap(); - assert!(matches!( - meta.inner, - RecordingMetaInner::Instant(InstantRecordingMeta::Failed { .. }) - )); - assert!(matches!( - meta.upload, - Some(UploadMeta::SegmentUpload { .. }) - )); - assert!(path.is_dir()); - std::fs::remove_dir_all(path).unwrap(); - } - } - #[test] - fn positively_complete_project_remains_resumable_without_media_deletion() { - let path = project( - "complete", - InstantRecordingMeta::Complete { - fps: 30, - sample_rate: Some(48000), - }, - ); - assert!(load_instant_resume_candidate(&path).unwrap().is_some()); - assert!(path.is_dir()); - std::fs::remove_dir_all(path).unwrap(); - } - #[test] - fn invalid_recovery_metadata_cannot_authorize_upload() { - let path = project( - "invalid", - InstantRecordingMeta::Complete { - fps: 30, - sample_rate: None, - }, - ); - std::fs::write(path.join("recording-meta.json"), b"invalid").unwrap(); - assert!(load_instant_resume_candidate(&path).is_err()); - assert!(path.is_dir()); - std::fs::remove_dir_all(path).unwrap(); - } - #[test] - fn periodic_reconciliation_does_not_mark_an_active_recording_failed() { - let path = project( - "periodic", - InstantRecordingMeta::InProgress { recording: true }, - ); - assert!( - load_upload_resume_candidate(&path, false) - .unwrap() - .is_none() - ); - assert!(matches!( - RecordingMeta::load_for_project(&path).unwrap().inner, - RecordingMetaInner::Instant(InstantRecordingMeta::InProgress { recording: true }) - )); - std::fs::remove_dir_all(path).unwrap(); - } - - #[test] - fn another_upload_owner_prevents_startup_metadata_mutation() { - let path = project( - "owned", - InstantRecordingMeta::InProgress { recording: true }, - ); - let lock = cap_recording::upload_resume::UploadLock::acquire(&path).unwrap(); - let before = std::fs::read(path.join("recording-meta.json")).unwrap(); - assert!(load_upload_resume_candidate(&path, true).unwrap().is_none()); - assert_eq!( - std::fs::read(path.join("recording-meta.json")).unwrap(), - before - ); - drop(lock); - std::fs::remove_dir_all(path).unwrap(); - } - #[test] - fn explicit_delete_accepts_idle_gpui_recordings_while_resume_defers_them() { - let path = project( - "gpui-delete", - InstantRecordingMeta::Complete { - fps: 30, - sample_rate: None, - }, - ); - std::fs::write(path.join("instant-upload.json"), b"{}").unwrap(); - assert!(upload::acquire_upload_lock(&path).is_err()); - let ownership = acquire_recording_delete_lock(&path).unwrap(); - assert!(acquire_recording_delete_lock(&path).is_err()); - std::fs::remove_dir_all(&path).unwrap(); - drop(ownership); - assert!(!path.exists()); - } - - #[test] - fn explicit_delete_cannot_bypass_another_clients_active_upload_lock() { - let path = project( - "active-delete", - InstantRecordingMeta::Complete { - fps: 30, - sample_rate: None, - }, - ); - let ownership = cap_recording::upload_resume::UploadLock::acquire(&path).unwrap(); - let original = std::fs::read(path.join("recording-meta.json")).unwrap(); - assert!(acquire_recording_delete_lock(&path).is_err()); - assert_eq!( - std::fs::read(path.join("recording-meta.json")).unwrap(), - original - ); - drop(ownership); - std::fs::remove_dir_all(path).unwrap(); - } -} - -#[cfg(test)] -mod recent_media_tests { - use super::*; - - #[test] - fn limited_history_matches_the_full_list_with_invalid_metadata_and_timestamp_ties() { - let candidates = vec![ - ("old", 1.0), - ("first tie", 4.0), - ("broken", 5.0), - ("second tie", 4.0), - ("middle", 2.0), - ]; - let load = |path, timestamp| (path != "broken").then_some((path, timestamp)); - let full = newest_valid_media(candidates.clone(), usize::MAX, load); - let recent = newest_valid_media(candidates, 2, load); - assert_eq!(recent, full[..2]); - assert_eq!(recent, vec![("first tie", 4.0), ("second tie", 4.0)]); - } - - #[test] - fn a_large_library_only_loads_metadata_for_the_requested_recent_items() { - let mut loads = 0; - let recent = newest_valid_media( - (0..10_000).map(|id| (id, f64::from(id))).collect(), - 9, - |id, _| { - loads += 1; - Some(id) - }, - ); - assert_eq!(loads, 9); - assert_eq!(recent, (9991..10_000).rev().collect::>()); - } -} - -#[cfg(test)] -mod camera_permission_tests { - use super::{ - DeviceOrModelID, RequestedInput, RequestedInputsState, await_current_camera_request, - check_requested_camera_permission, - }; - - fn camera(name: &str) -> DeviceOrModelID { - DeviceOrModelID::DeviceID(name.into()) - } - - #[test] - fn disabled_camera_bypasses_permission() { - check_requested_camera_permission(None, || { - panic!("turning the camera off must not require permission") - }) - .unwrap(); - } - - #[test] - fn saved_camera_permission_failure_preserves_intent_and_retry() { - let mut input = RequestedInput::new(Some(camera("Saved camera"))); - let revision = input.revision; - assert!(input.prepare_restore(revision)); - let result = check_requested_camera_permission(input.value.as_ref(), || { - Err("Allow camera access in System Settings".into()) - }); - input.finish(revision, &result); - assert_eq!(input.value, Some(camera("Saved camera"))); - assert!(!input.pending); - assert!( - input - .validate("camera") - .unwrap_err() - .contains("System Settings") - ); - assert!(input.prepare_restore(revision)); - let result = check_requested_camera_permission(input.value.as_ref(), || Ok(())); - input.finish(revision, &result); - assert!(input.validate("camera").is_ok()); - } - - #[test] - fn revoked_camera_access_requires_successful_reselection() { - let mut input = RequestedInput::new(Some(camera("Saved camera"))); - let denied = input.begin(Some(camera("Saved camera"))); - let result = check_requested_camera_permission(input.value.as_ref(), || { - Err("Camera access is blocked".into()) - }); - input.finish(denied, &result); - assert!(input.validate("camera").is_err()); - let granted = input.begin(Some(camera("Saved camera"))); - let result = check_requested_camera_permission(input.value.as_ref(), || Ok(())); - input.finish(granted, &result); - input.finish(denied, &Err("Stale denial".into())); - assert!(input.validate("camera").is_ok()); - assert_eq!(input.value, Some(camera("Saved camera"))); - } - - #[test] - fn camera_off_clears_permission_failure_without_requesting_access() { - let mut input = RequestedInput::new(Some(camera("Saved camera"))); - let old = input.begin(Some(camera("Saved camera"))); - input.finish(old, &Err("Camera access is blocked".into())); - let off = input.begin(None); - let result = check_requested_camera_permission(input.value.as_ref(), || { - panic!("camera off must not request permission") - }); - input.finish(off, &result); - input.finish(old, &Err("Old failure".into())); - assert!(input.value.is_none()); - assert!(input.validate("camera").is_ok()); - } - - #[tokio::test] - async fn current_camera_setup_keeps_success_and_device_failure() { - assert_eq!( - await_current_camera_request(async { Ok::<_, String>(()) }, || true).await, - Ok(Ok(())) - ); - assert_eq!( - await_current_camera_request( - async { Err::<(), _>("CameraTimeout".to_string()) }, - || true - ) - .await, - Ok(Err("CameraTimeout".into())) - ); - } - - #[tokio::test] - async fn stale_camera_ready_failure_does_not_publish_or_retry() { - let requested = RequestedInputsState::new(None, Some(camera("Saved camera"))); - let revision = requested - .inner - .lock() - .unwrap() - .camera - .begin(Some(camera("Saved camera"))); - let result = await_current_camera_request( - async { - let mut current = requested.inner.lock().unwrap(); - let latest = current.camera.begin(Some(camera("New camera"))); - current.camera.finish(latest, &Ok(())); - Err::<(), _>("CameraTimeout".to_string()) - }, - || requested.camera_is_current(revision), - ) - .await; - assert!(result.unwrap_err().contains("superseded")); - let mut published = false; - assert!(!requested.publish_camera_if_current(revision, || published = true)); - assert!(!published); - assert!(requested.snapshot().camera.validate("camera").is_ok()); - assert_eq!( - requested.snapshot().camera.value, - Some(camera("New camera")) - ); - } - - #[tokio::test] - async fn stale_camera_ready_success_cannot_clear_newer_failure() { - let requested = RequestedInputsState::new(None, Some(camera("Saved camera"))); - let revision = requested - .inner - .lock() - .unwrap() - .camera - .begin(Some(camera("Saved camera"))); - let result = await_current_camera_request( - async { - let mut current = requested.inner.lock().unwrap(); - let latest = current.camera.begin(Some(camera("Saved camera"))); - current - .camera - .finish(latest, &Err("New selection failed".into())); - Ok::<(), String>(()) - }, - || requested.camera_is_current(revision), - ) - .await; - assert!(result.unwrap_err().contains("superseded")); - assert_eq!( - requested.snapshot().camera.error.as_deref(), - Some("New selection failed") - ); - } - - #[tokio::test] - async fn superseded_camera_retry_wait_cannot_start_another_attempt() { - let requested = RequestedInputsState::new(None, Some(camera("Saved camera"))); - let revision = requested - .inner - .lock() - .unwrap() - .camera - .begin(Some(camera("Saved camera"))); - let mut attempts = 1; - let retry = await_current_camera_request( - async { - requested.inner.lock().unwrap().camera.begin(None); - }, - || requested.camera_is_current(revision), - ) - .await; - if retry.is_ok() { - attempts += 1; - } - assert!(retry.unwrap_err().contains("superseded")); - assert_eq!(attempts, 1); - assert!(requested.snapshot().camera.value.is_none()); - } - - #[test] - fn superseded_camera_cleanup_cannot_mark_a_newer_camera_disconnected() { - let requested = RequestedInputsState::new(None, Some(camera("Old camera"))); - let old = requested - .inner - .lock() - .unwrap() - .camera - .begin(Some(camera("Old camera"))); - let latest = requested - .inner - .lock() - .unwrap() - .camera - .begin(Some(camera("New camera"))); - requested - .inner - .lock() - .unwrap() - .camera - .finish(latest, &Ok(())); - let mut connected = true; - let mut notification = None; - assert!(!requested.publish_camera_if_current(old, || { - connected = false; - notification = Some("Camera unavailable"); - })); - assert!(connected); - assert!(notification.is_none()); - } -} diff --git a/apps/desktop/src-tauri/src/linux_instant_camera.rs b/apps/desktop/src-tauri/src/linux_instant_camera.rs deleted file mode 100644 index ff9fa79d111..00000000000 --- a/apps/desktop/src-tauri/src/linux_instant_camera.rs +++ /dev/null @@ -1,1144 +0,0 @@ -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; - -use serde::{Deserialize, Serialize}; -use specta::Type; -#[cfg(target_os = "linux")] -use std::time::Instant; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_specta::Event; - -use crate::{camera::CameraPreviewState, windows::CapWindowId}; - -#[cfg(target_os = "linux")] -use crate::camera::CameraPreviewShape; - -#[derive(Clone)] -struct SelectionGuard { - inputs: Arc>, - revision: u64, - selected: cap_recording::feeds::camera::DeviceOrModelID, -} - -impl SelectionGuard { - fn current(&self) -> bool { - let inputs = self.inputs.lock().unwrap(); - inputs.camera.revision == self.revision - && inputs.camera.value.as_ref() == Some(&self.selected) - && !inputs.camera.pending - && inputs.camera.error.is_none() - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct CameraPresentationInput { - pub viewport_width: f64, - pub viewport_height: f64, - pub left: f64, - pub top: f64, - pub width: f64, - pub height: f64, - pub radius: f64, - pub layout_revision: u32, - pub state: CameraPreviewState, -} - -#[derive(Clone, Serialize, Type, Event)] -#[serde(rename_all = "camelCase")] -pub struct CameraPresentationRequested { - pub nonce: String, - pub generation: u32, - pub camera_revision: String, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PhysicalRect { - pub x: i32, - pub y: i32, - pub width: u32, - pub height: u32, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct NativeSnapshot { - id: u64, - rect: PhysicalRect, -} - -impl NativeSnapshot { - #[cfg(target_os = "linux")] - fn read(window: &WebviewWindow) -> Result { - use wgpu::rwh::{HasWindowHandle, RawWindowHandle}; - let id = match window - .window_handle() - .map_err(|error| error.to_string())? - .as_raw() - { - RawWindowHandle::Xlib(handle) => handle.window, - RawWindowHandle::Xcb(handle) => u64::from(handle.window.get()), - _ => return Err("Camera presentation requires a native X11 window".into()), - }; - let position = window.inner_position().map_err(|error| error.to_string())?; - let size = window.inner_size().map_err(|error| error.to_string())?; - Ok(Self { - id, - rect: PhysicalRect { - x: position.x, - y: position.y, - width: size.width, - height: size.height, - }, - }) - } - - #[cfg(not(target_os = "linux"))] - fn read(_window: &WebviewWindow) -> Result { - Err("Processed camera presentation is only available on Linux X11".into()) - } -} - -async fn native_snapshot(window: WebviewWindow) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = window.clone(); - window - .run_on_main_thread(move || { - let _ = tx.send(NativeSnapshot::read(&handle)); - }) - .map_err(|error| error.to_string())?; - tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Camera window geometry timed out".to_string())? - .map_err(|_| "Camera window geometry was cancelled".to_string())? -} - -#[derive(Clone)] -struct PresentationOwner { - nonce: String, - generation: u32, - native: NativeSnapshot, - selection: SelectionGuard, - state: tokio::sync::watch::Receiver, -} - -impl PresentationOwner { - fn validate( - &self, - app: &AppHandle, - native: NativeSnapshot, - input: &CameraPresentationInput, - ) -> Result<(), String> { - if !crate::clean_capture::is_current(app, self.generation) - || !self.selection.current() - || self.native != native - || *self.state.borrow() != input.state - { - return Err( - "Camera selection, appearance, or window changed. Start again to confirm it." - .into(), - ); - } - Ok(()) - } -} - -struct PendingPresentation { - owner: PresentationOwner, - reply: tokio::sync::oneshot::Sender>, -} - -#[derive(Default)] -pub struct PresentationBroker { - pending: Arc>>, -} - -#[cfg(target_os = "linux")] -struct PendingGuard { - pending: Arc>>, - nonce: String, -} - -#[cfg(target_os = "linux")] -impl Drop for PendingGuard { - fn drop(&mut self) { - let mut pending = self.pending.lock().unwrap(); - if pending - .as_ref() - .is_some_and(|pending| pending.owner.nonce == self.nonce) - { - *pending = None; - } - } -} - -#[tauri::command] -#[specta::specta] -pub async fn submit_camera_presentation( - window: WebviewWindow, - nonce: String, - input: Option, - error: Option, -) -> Result<(), String> { - if !cfg!(target_os = "linux") - || !matches!( - window.label().parse::(), - Ok(CapWindowId::Camera) - ) - { - return Err( - "Only the current Linux camera window may answer a presentation request".into(), - ); - } - let app = window.app_handle(); - let broker = app.state::(); - let owner = broker - .pending - .lock() - .unwrap() - .as_ref() - .filter(|pending| pending.owner.nonce == nonce) - .map(|pending| pending.owner.clone()) - .ok_or("Camera presentation request expired")?; - let native = native_snapshot(window.clone()).await?; - if native.id != owner.native.id - || !crate::clean_capture::is_current(app, owner.generation) - || !owner.selection.current() - { - return Err("Camera presentation owner changed".into()); - } - let result = match (input, error) { - (Some(input), None) => owner.validate(app, native, &input).map(|()| input), - (None, Some(error)) => Err(error), - _ => return Err("Camera presentation requires either pixels geometry or an error".into()), - }; - let mut pending = broker.pending.lock().unwrap(); - if pending - .as_ref() - .is_none_or(|pending| pending.owner.nonce != nonce) - { - return Err("Camera presentation request was superseded".into()); - } - let _ = pending.take().unwrap().reply.send(result); - Ok(()) -} - -#[cfg(target_os = "linux")] -pub struct PreparedPresentation { - owner: PresentationOwner, - actor: kameo::actor::ActorRef, - input: CameraPresentationInput, - pub presentation: cap_recording::instant_recording::LinuxCameraPresentation, - pub reference_size: (u32, u32), - pub processing: cap_recording::instant_recording::LinuxCameraProcessing, -} - -#[cfg(target_os = "linux")] -impl PreparedPresentation { - pub fn validate_before_hide(&self, window: &WebviewWindow) -> Result<(), String> { - if !matches!( - window.label().parse::(), - Ok(CapWindowId::Camera) - ) { - return Err("Camera presentation window changed".into()); - } - self.owner.validate( - window.app_handle(), - NativeSnapshot::read(window)?, - &self.input, - ) - } -} - -#[cfg(target_os = "linux")] -pub async fn request_presentation( - app: &AppHandle, - generation: u32, - capture: PhysicalRect, -) -> Result { - let requested = app.state::(); - let snapshot = requested.ready_snapshot()?; - let selected = snapshot - .camera - .value - .clone() - .ok_or("Select a camera before requesting its presentation")?; - let selection = SelectionGuard { - inputs: requested.inner.clone(), - revision: snapshot.camera.revision, - selected, - }; - let window = CapWindowId::Camera - .get(app) - .ok_or("Open the selected camera preview before recording")?; - if !window.is_visible().map_err(|error| error.to_string())? { - return Err("Open the selected camera preview before recording".into()); - } - let native = native_snapshot(window.clone()).await?; - let (actor, state) = { - let state = app.state::>(); - let state = state.read().await; - ( - state.camera_feed.clone(), - state.camera_preview_state_tx.subscribe(), - ) - }; - if !selection.current() || !crate::clean_capture::is_current(app, generation) { - return Err("Recording or camera selection changed".into()); - } - let nonce = uuid::Uuid::new_v4().to_string(); - let owner = PresentationOwner { - nonce: nonce.clone(), - generation, - native, - selection, - state, - }; - let broker = app.state::(); - let (reply, rx) = tokio::sync::oneshot::channel(); - { - let mut pending = broker.pending.lock().unwrap(); - if pending.is_some() { - return Err("Camera presentation is already being measured".into()); - } - *pending = Some(PendingPresentation { - owner: owner.clone(), - reply, - }); - } - let _guard = PendingGuard { - pending: broker.pending.clone(), - nonce: nonce.clone(), - }; - CameraPresentationRequested { - nonce, - generation, - camera_revision: snapshot.camera.revision.to_string(), - } - .emit_to(app, window.label()) - .map_err(|error| error.to_string())?; - let input = tokio::time::timeout(Duration::from_secs(2), rx) - .await - .map_err(|_| "Camera preview did not answer before the presentation deadline".to_string())? - .map_err(|_| "Camera presentation was cancelled".to_string())??; - owner.validate(app, native_snapshot(window).await?, &input)?; - let (presentation, processing) = presentation_from_input(&input, native.rect, capture)?; - Ok(PreparedPresentation { - owner, - actor, - input, - presentation, - reference_size: (capture.width, capture.height), - processing, - }) -} - -#[cfg(target_os = "linux")] -fn processing( - state: &CameraPreviewState, -) -> cap_recording::instant_recording::LinuxCameraProcessing { - use cap_recording::instant_recording::{LinuxCameraBlur, LinuxCameraProcessing}; - LinuxCameraProcessing { - mirrored: state.mirrored, - blur: match state.background_blur { - cap_project::BackgroundBlurMode::Off | cap_project::BackgroundBlurMode::Remove => { - LinuxCameraBlur::Off - } - cap_project::BackgroundBlurMode::Light => LinuxCameraBlur::Light, - cap_project::BackgroundBlurMode::Heavy => LinuxCameraBlur::Heavy, - }, - } -} - -#[cfg(target_os = "linux")] -fn presentation_from_input( - input: &CameraPresentationInput, - window: PhysicalRect, - capture: PhysicalRect, -) -> Result< - ( - cap_recording::instant_recording::LinuxCameraPresentation, - cap_recording::instant_recording::LinuxCameraProcessing, - ), - String, -> { - use cap_recording::instant_recording::{ - LinuxCameraEffect, LinuxCameraPresentation, LinuxCameraRect, LinuxCameraShape, - }; - if ![ - input.viewport_width, - input.viewport_height, - input.left, - input.top, - input.width, - input.height, - input.radius, - ] - .iter() - .all(|value| value.is_finite()) - || input.viewport_width <= 0.0 - || input.viewport_height <= 0.0 - || input.width <= 0.0 - || input.height <= 0.0 - || input.left < 0.0 - || input.top < 0.0 - || input.radius < 0.0 - || input.left + input.width > input.viewport_width + 1.0 - || input.top + input.height > input.viewport_height + 1.0 - || window.width == 0 - || window.height == 0 - || !input.state.size.is_finite() - || !(crate::camera::MIN_CAMERA_SIZE..=crate::camera::MAX_CAMERA_SIZE) - .contains(&input.state.size) - { - return Err("Camera preview has invalid content bounds".into()); - } - let sx = f64::from(window.width) / input.viewport_width; - let sy = f64::from(window.height) / input.viewport_height; - if (sx - sy).abs() > sx.min(sy) * 0.01 { - return Err("Camera preview has an unsupported physical scale".into()); - } - let left = f64::from(window.x) + (input.left * sx).round(); - let top = f64::from(window.y) + (input.top * sy).round(); - let right = f64::from(window.x) + ((input.left + input.width) * sx).round(); - let bottom = f64::from(window.y) + ((input.top + input.height) * sy).round(); - let x = left - f64::from(capture.x); - let y = top - f64::from(capture.y); - let width = right - left; - let height = bottom - top; - if x < 0.0 - || y < 0.0 - || width < 1.0 - || height < 1.0 - || x + width > f64::from(capture.width) - || y + height > f64::from(capture.height) - { - return Err( - "Move the whole camera preview inside the capture area before recording".into(), - ); - } - let width = width as u32; - let height = height as u32; - let shape = match input.state.shape { - CameraPreviewShape::Round if width == height => LinuxCameraShape::Round, - CameraPreviewShape::Round if width.abs_diff(height) <= 1 => { - LinuxCameraShape::RoundedRectangle { - radius_pixels: width.min(height) / 2, - } - } - CameraPreviewShape::Round => return Err("Round camera preview is not square".into()), - CameraPreviewShape::Square | CameraPreviewShape::Full => { - LinuxCameraShape::RoundedRectangle { - radius_pixels: ((input.radius * sx.min(sy)).round() as u32) - .min(width.min(height) / 2), - } - } - }; - let mut presentation = LinuxCameraPresentation { - rect: LinuxCameraRect { - x: x as u32, - y: y as u32, - width, - height, - }, - shape, - mirrored: input.state.mirrored, - effect: LinuxCameraEffect::None, - }; - presentation - .validate(capture.width, capture.height) - .map_err(|error| error.to_string())?; - if input.state.background_blur != cap_project::BackgroundBlurMode::Off { - presentation.effect = LinuxCameraEffect::BackgroundBlur; - } - Ok((presentation, processing(&input.state))) -} - -#[cfg(target_os = "linux")] -mod producer { - use super::*; - use cap_recording::{ - FFmpegVideoFrame, - feeds::camera::{self, CameraFeed, CameraFeedLock}, - instant_recording::{ - LINUX_CAMERA_MAX_MASK_AGE, LinuxCameraBlur, LinuxCameraMaskReceipt, LinuxCameraPixels, - LinuxCameraProcessing, LinuxCameraPublisher, LinuxProcessedCameraFrame, - LinuxProcessedCameraSource, - }, - }; - use cap_timestamp::Timestamp; - use kameo::actor::ActorRef; - use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; - - pub struct Reservation(Arc); - - impl Reservation { - pub(super) fn acquire(active: &Arc) -> Option { - active - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .ok() - .map(|_| Self(active.clone())) - } - } - - impl Drop for Reservation { - fn drop(&mut self) { - self.0.store(false, Ordering::Release); - } - } - - struct Attachment { - actor: ActorRef, - sender: flume::Sender, - runtime: tokio::runtime::Handle, - _reservation: Reservation, - } - - impl Drop for Attachment { - fn drop(&mut self) { - let actor = self.actor.clone(); - let sender = self.sender.clone(); - drop(self.runtime.spawn(async move { - let _ = actor.ask(camera::RemoveSender(sender)).await; - })); - } - } - - #[derive(Clone)] - pub struct ProcessingFactory { - commands: flume::Sender>, - reserved: Arc, - generation: Arc, - } - - pub(crate) fn channel() -> (ProcessingFactory, flume::Receiver>) { - let (commands, receiver) = flume::bounded(1); - ( - ProcessingFactory { - commands, - reserved: Arc::new(AtomicBool::new(false)), - generation: Arc::new(AtomicU64::new(0)), - }, - receiver, - ) - } - - impl ProcessingFactory { - pub async fn subscribe( - &self, - feed: Arc, - prepared: &PreparedPresentation, - ) -> anyhow::Result { - let info = feed.camera_info(); - let matches = match &prepared.owner.selection.selected { - camera::DeviceOrModelID::DeviceID(id) => info.device_id() == id, - camera::DeviceOrModelID::ModelID(id) => info.model_id() == Some(id), - }; - anyhow::ensure!( - matches && feed.id() == prepared.actor.id(), - "Processed camera does not match the requested camera lock" - ); - anyhow::ensure!( - prepared.owner.selection.current() - && *prepared.owner.state.borrow() == prepared.input.state, - "Camera request or appearance changed before recording" - ); - let reservation = tokio::time::timeout(Duration::from_millis(1500), async { - loop { - if let Some(reservation) = Reservation::acquire(&self.reserved) { - break reservation; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - .map_err(|_| { - anyhow::anyhow!("Previous camera processing has not released its lease") - })?; - let (sender, frames) = flume::bounded(2); - let attachment = Attachment { - actor: prepared.actor.clone(), - sender: sender.clone(), - runtime: tokio::runtime::Handle::current(), - _reservation: reservation, - }; - anyhow::ensure!( - prepared.owner.selection.current(), - "Camera selection changed while waiting for processing" - ); - let generation = self.generation.fetch_add(1, Ordering::AcqRel) + 1; - let not_before = Instant::now(); - let (publisher, mut source) = LinuxProcessedCameraSource::channel( - feed.clone(), - prepared.processing, - generation, - not_before, - ); - tokio::time::timeout( - Duration::from_millis(1500), - feed.ask(camera::AddSender(sender)), - ) - .await - .map_err(|_| anyhow::anyhow!("Camera processing attachment timed out"))? - .map_err(|error| anyhow::anyhow!("Camera processing attachment failed: {error}"))?; - let work = RecordingWork { - frames, - publisher, - state: prepared.input.state.clone(), - selection: prepared.owner.selection.clone(), - current_state: prepared.owner.state.clone(), - generation, - not_before, - published: AtomicBool::new(false), - _attachment: attachment, - }; - tokio::time::timeout( - Duration::from_millis(1500), - self.commands.send_async(Box::new(work)), - ) - .await - .map_err(|_| anyhow::anyhow!("Camera processing command timed out"))? - .map_err(|_| anyhow::anyhow!("Camera processing worker disconnected"))?; - source.wait_ready(Duration::from_secs(10)).await?; - anyhow::ensure!( - prepared.owner.selection.current() - && *prepared.owner.state.borrow() == prepared.input.state, - "Camera request or appearance changed while processing warmed up" - ); - Ok(source) - } - } - - #[derive(Clone, Debug)] - pub(crate) struct FrameReceipt { - pub timestamp: Timestamp, - pub generation: u64, - pub processing: LinuxCameraProcessing, - pub dimensions: (u32, u32), - pub blur: Option, - } - - pub(crate) struct RecordingWork { - pub frames: flume::Receiver, - publisher: LinuxCameraPublisher, - pub state: CameraPreviewState, - selection: SelectionGuard, - current_state: tokio::sync::watch::Receiver, - generation: u64, - not_before: Instant, - published: AtomicBool, - _attachment: Attachment, - } - - impl RecordingWork { - pub fn cancelled(&self) -> bool { - self.publisher.is_cancelled() - } - pub fn fail(&self, error: impl Into) { - self.publisher.fail(error.into()); - } - pub fn accepts(&self, timestamp: Timestamp) -> Result { - if !self.selection.current() || *self.current_state.borrow() != self.state { - return Err("Requested camera or appearance changed during recording".into()); - } - let Timestamp::Instant(captured) = timestamp else { - return Err("Camera requires an original monotonic timestamp".into()); - }; - Ok(captured >= self.not_before) - } - pub fn receipt(&self, timestamp: Timestamp, dimensions: (u32, u32)) -> FrameReceipt { - FrameReceipt { - timestamp, - generation: self.generation, - processing: processing(&self.state), - dimensions, - blur: None, - } - } - pub fn publish(&self, rgba: &[u8], receipt: FrameReceipt) { - if self.cancelled() { - return; - } - match self.accepts(receipt.timestamp) { - Ok(true) => {} - Ok(false) => return, - Err(error) => { - self.fail(error); - return; - } - } - let now = Instant::now(); - if !self.published.load(Ordering::Acquire) - && matches!(receipt.timestamp, Timestamp::Instant(captured) if now.saturating_duration_since(captured)>Duration::from_secs(1)) - { - return; - } - let result = make_processed_frame( - rgba, - receipt, - self.generation, - self.not_before, - processing(&self.state), - now, - ); - match result { - Ok(Some(frame)) => { - self.publisher.publish(frame); - self.published.store(true, Ordering::Release); - } - Ok(None) => {} - Err(error) => self.fail(error), - } - } - } - - pub(crate) fn make_processed_frame( - rgba: &[u8], - receipt: FrameReceipt, - generation: u64, - not_before: Instant, - expected: LinuxCameraProcessing, - now: Instant, - ) -> Result, String> { - if receipt.generation != generation || receipt.processing != expected { - return Err("Processed camera receipt belongs to another request".into()); - } - let Timestamp::Instant(captured) = receipt.timestamp else { - return Err("Processed camera timestamp is not monotonic".into()); - }; - if captured < not_before - || captured > now - || now.saturating_duration_since(captured) > Duration::from_secs(1) - { - return Err("Processed camera capture timestamp is stale or invalid".into()); - } - let mask = match (expected.blur, receipt.blur) { - (LinuxCameraBlur::Off, None) => None, - (LinuxCameraBlur::Light | LinuxCameraBlur::Heavy, Some(status)) => { - let mode = if expected.blur == LinuxCameraBlur::Light { - cap_camera_effects::BlurMode::Light - } else { - cap_camera_effects::BlurMode::Heavy - }; - match status.applied_at(now, LINUX_CAMERA_MAX_MASK_AGE) { - Ok(applied) - if applied.mode == mode - && applied.output_dimensions == receipt.dimensions - && applied.mask.input_submitted_at >= not_before - && applied.mask.generation > 0 => - { - Some(LinuxCameraMaskReceipt { - generation: applied.mask.generation, - submitted_at: applied.mask.input_submitted_at, - completed_at: applied.mask.inference_completed_at, - }) - } - Err(cap_camera_effects::BlurOutputUnavailable::Pending) => return Ok(None), - other => { - return Err(format!("Requested camera blur was not applied: {other:?}")); - } - } - } - _ => return Err("Requested camera blur has no matching output receipt".into()), - }; - let (width, height) = receipt.dimensions; - let row = (width as usize) - .checked_mul(4) - .ok_or("Camera row overflow")?; - let size = row - .checked_mul(height as usize) - .ok_or("Camera image overflow")?; - if width == 0 || height == 0 || rgba.len() != size { - return Err("Camera pixels do not match their receipt".into()); - } - let mut bgra = vec![0; size]; - for (source, destination) in rgba.chunks_exact(row).zip(bgra.chunks_exact_mut(row)) { - for (x, pixel) in destination.chunks_exact_mut(4).enumerate() { - let source_x = if expected.mirrored { - width as usize - 1 - x - } else { - x - }; - let source = &source[source_x * 4..source_x * 4 + 4]; - pixel.copy_from_slice(&[source[2], source[1], source[0], source[3]]); - } - } - Ok(Some(LinuxProcessedCameraFrame { - bgra: LinuxCameraPixels::new(bgra), - dimensions: receipt.dimensions, - stride: row, - timestamp: receipt.timestamp, - generation, - processing: expected, - mask, - })) - } -} - -#[cfg(target_os = "linux")] -pub use producer::ProcessingFactory; -#[cfg(target_os = "linux")] -pub(crate) use producer::{FrameReceipt, RecordingWork, channel as processing_channel}; - -#[cfg(all(test, target_os = "linux"))] -mod tests { - use super::*; - use cap_recording::instant_recording::{LinuxCameraBlur, LinuxCameraProcessing}; - use cap_timestamp::Timestamp; - - fn input() -> CameraPresentationInput { - CameraPresentationInput { - viewport_width: 230.0, - viewport_height: 286.0, - left: 0.0, - top: 56.0, - width: 230.0, - height: 230.0, - radius: 115.0, - layout_revision: 1, - state: CameraPreviewState::default(), - } - } - - #[test] - fn camera_content_excludes_toolbar_and_preserves_physical_position() { - let window = PhysicalRect { - x: -1200, - y: 100, - width: 460, - height: 572, - }; - let capture = PhysicalRect { - x: -1920, - y: 0, - width: 1920, - height: 1080, - }; - let (presentation, _) = presentation_from_input(&input(), window, capture).unwrap(); - assert_eq!(presentation.rect.x, 720); - assert_eq!(presentation.rect.y, 212); - assert_eq!( - (presentation.rect.width, presentation.rect.height), - (460, 460) - ); - } - - #[test] - fn presentation_rejects_outside_capture_ambiguous_scale_and_invalid_geometry() { - let capture = PhysicalRect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }; - assert!( - presentation_from_input( - &input(), - PhysicalRect { - x: 1800, - y: 0, - width: 230, - height: 286 - }, - capture - ) - .is_err() - ); - assert!( - presentation_from_input( - &input(), - PhysicalRect { - x: 0, - y: 0, - width: 460, - height: 286 - }, - capture - ) - .is_err() - ); - let mut invalid = input(); - invalid.width = f64::NAN; - assert!( - presentation_from_input( - &invalid, - PhysicalRect { - x: 0, - y: 0, - width: 230, - height: 286 - }, - capture - ) - .is_err() - ); - } - - #[test] - fn full_shape_and_exact_effects_are_not_replaced_by_defaults() { - let mut input = input(); - input.width = 460.0; - input.viewport_width = 460.0; - input.radius = 24.0; - input.state.shape = CameraPreviewShape::Full; - input.state.mirrored = true; - input.state.background_blur = cap_project::BackgroundBlurMode::Heavy; - let (presentation, processing) = presentation_from_input( - &input, - PhysicalRect { - x: 20, - y: 30, - width: 460, - height: 286, - }, - PhysicalRect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - ) - .unwrap(); - assert_eq!( - presentation.shape, - cap_recording::instant_recording::LinuxCameraShape::RoundedRectangle { - radius_pixels: 24 - } - ); - assert_eq!(presentation.rect.width, 460); - assert_eq!( - processing, - LinuxCameraProcessing { - mirrored: true, - blur: LinuxCameraBlur::Heavy - } - ); - } - - fn receipt(now: Instant) -> FrameReceipt { - FrameReceipt { - timestamp: Timestamp::Instant(now), - generation: 3, - processing: LinuxCameraProcessing { - mirrored: true, - blur: LinuxCameraBlur::Off, - }, - dimensions: (2, 1), - blur: None, - } - } - - #[test] - fn recording_packs_rgba_to_bgra_and_mirrors_already_processed_pixels_once() { - let now = Instant::now(); - let receipt = receipt(now); - let expected = receipt.processing; - let frame = producer::make_processed_frame( - &[1, 2, 3, 255, 4, 5, 6, 128], - receipt, - 3, - now, - expected, - now, - ) - .unwrap() - .unwrap(); - assert_eq!(&*frame.bgra, &[6, 5, 4, 128, 3, 2, 1, 255]); - assert_eq!(frame.generation, 3); - assert_eq!(frame.dimensions, (2, 1)); - } - - #[test] - fn recording_rejects_old_generation_old_capture_and_missing_requested_blur() { - let now = Instant::now(); - let original = receipt(now); - assert!( - producer::make_processed_frame( - &[0; 8], - original.clone(), - 4, - now, - original.processing, - now - ) - .is_err() - ); - assert!( - producer::make_processed_frame( - &[0; 8], - original.clone(), - 3, - now + Duration::from_millis(1), - original.processing, - now - ) - .is_err() - ); - assert!( - producer::make_processed_frame( - &[0; 8], - original.clone(), - 3, - now, - original.processing, - now + Duration::from_secs(2) - ) - .is_err() - ); - let mut blurred = original; - blurred.processing.blur = LinuxCameraBlur::Light; - let expected = blurred.processing; - assert!(producer::make_processed_frame(&[0; 8], blurred, 3, now, expected, now).is_err()); - } - - #[test] - fn pending_failed_wrong_mode_and_stale_masks_never_become_recorded_raw_frames() { - use cap_camera_effects::{ - BlurFailure, BlurMaskReceipt, BlurMaskStatus, BlurMode, BlurOutputStatus, - }; - let now = Instant::now(); - let mut receipt = receipt(now); - receipt.processing.blur = LinuxCameraBlur::Heavy; - let expected = receipt.processing; - let mut status = BlurOutputStatus { - mode: BlurMode::Heavy, - output_sequence: 1, - output_dimensions: (2, 1), - mask: BlurMaskStatus::Pending, - }; - receipt.blur = Some(status.clone()); - assert!( - producer::make_processed_frame(&[0; 8], receipt.clone(), 3, now, expected, now) - .unwrap() - .is_none() - ); - status.mask = BlurMaskStatus::Failed(BlurFailure::Inference("failed".into())); - receipt.blur = Some(status.clone()); - assert!( - producer::make_processed_frame(&[0; 8], receipt.clone(), 3, now, expected, now) - .is_err() - ); - status.mask = BlurMaskStatus::Ready(BlurMaskReceipt { - generation: 1, - input_submitted_at: now, - inference_completed_at: now, - input_dimensions: (2, 1), - }); - status.mode = BlurMode::Light; - receipt.blur = Some(status.clone()); - assert!( - producer::make_processed_frame(&[0; 8], receipt.clone(), 3, now, expected, now) - .is_err() - ); - status.mode = BlurMode::Heavy; - receipt.blur = Some(status); - assert!( - producer::make_processed_frame(&[0; 8], receipt.clone(), 3, now, expected, now) - .unwrap() - .is_some() - ); - assert!( - producer::make_processed_frame( - &[0; 8], - receipt, - 3, - now, - expected, - now + Duration::from_millis(751) - ) - .is_err() - ); - } - - #[tokio::test] - async fn presentation_request_cancellation_only_releases_owned_nonce() { - let pending = Arc::new(Mutex::new(None)); - let inputs = Arc::new(Mutex::new(crate::RequestedInputs { - microphone: crate::RequestedInput::new(None), - camera: crate::RequestedInput::new(Some( - cap_recording::feeds::camera::DeviceOrModelID::DeviceID("camera-a".into()), - )), - })); - let (_, state) = tokio::sync::watch::channel(CameraPreviewState::default()); - let owner = PresentationOwner { - nonce: "new".into(), - generation: 2, - native: NativeSnapshot { - id: 1, - rect: PhysicalRect { - x: 0, - y: 0, - width: 230, - height: 286, - }, - }, - selection: SelectionGuard { - inputs, - revision: 0, - selected: cap_recording::feeds::camera::DeviceOrModelID::DeviceID( - "camera-a".into(), - ), - }, - state, - }; - let (reply, received) = tokio::sync::oneshot::channel(); - *pending.lock().unwrap() = Some(PendingPresentation { owner, reply }); - drop(PendingGuard { - pending: pending.clone(), - nonce: "old".into(), - }); - assert!(pending.lock().unwrap().is_some()); - drop(PendingGuard { - pending: pending.clone(), - nonce: "new".into(), - }); - assert!(received.await.is_err()); - assert!(pending.lock().unwrap().is_none()); - } - - #[test] - fn camera_request_revision_guard_does_not_rewrite_input_intent() { - let state = crate::RequestedInputsState::new( - None, - Some(cap_recording::feeds::camera::DeviceOrModelID::DeviceID( - "camera-a".into(), - )), - ); - let guard = SelectionGuard { - inputs: state.inner.clone(), - revision: 0, - selected: cap_recording::feeds::camera::DeviceOrModelID::DeviceID("camera-a".into()), - }; - assert!(guard.current()); - let revision = state.inner.lock().unwrap().camera.begin(None); - assert!(!guard.current()); - state.inner.lock().unwrap().camera.finish(revision, &Ok(())); - assert!(!guard.current()); - assert!(state.snapshot().camera.value.is_none()); - } - - #[tokio::test] - async fn recording_reservation_excludes_another_attempt_until_owned_drop() { - use std::sync::atomic::{AtomicBool, Ordering}; - let active = Arc::new(AtomicBool::new(false)); - let first = producer::Reservation::acquire(&active).unwrap(); - let next_active = active.clone(); - let (blocked, seen) = tokio::sync::oneshot::channel(); - let (release, ready) = tokio::sync::oneshot::channel(); - let next = tokio::spawn(async move { - assert!(producer::Reservation::acquire(&next_active).is_none()); - blocked.send(()).unwrap(); - ready.await.unwrap(); - producer::Reservation::acquire(&next_active).unwrap() - }); - seen.await.unwrap(); - assert!(active.load(Ordering::Acquire)); - drop(first); - release.send(()).unwrap(); - let next = next.await.unwrap(); - assert!(active.load(Ordering::Acquire)); - drop(next); - assert!(!active.load(Ordering::Acquire)); - } -} diff --git a/apps/desktop/src-tauri/src/logging.rs b/apps/desktop/src-tauri/src/logging.rs deleted file mode 100644 index a3650410a27..00000000000 --- a/apps/desktop/src-tauri/src/logging.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::{ - ArcLock, feeds::microphone::MicrophoneFeed, general_settings::GeneralSettingsStore, - permissions, web_api::ManagerExt, -}; -use cap_recording::diagnostics::{ - CameraDiagnostics, CameraFormatInfo, DisplayDiagnostics, HardwareInfo, MicrophoneDiagnostics, - StorageInfo, -}; -use serde::Serialize; - -use tauri::{AppHandle, Manager}; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct LogUploadDiagnostics { - hardware: HardwareInfo, - system: cap_recording::diagnostics::SystemDiagnostics, - displays: Vec, - cameras: Vec, - microphones: Vec, - storage: Option, - permissions: PermissionsInfo, - app_state: AppStateInfo, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct PermissionsInfo { - screen_recording: String, - camera: String, - microphone: String, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct AppStateInfo { - is_recording: bool, - recordings_dir: String, - app_data_dir: String, -} - -fn collect_cameras(has_permission: bool) -> Vec { - if !has_permission { - return vec![]; - } - - cap_camera::list_cameras() - .map(|camera| { - let formats = camera - .formats() - .unwrap_or_default() - .into_iter() - .take(10) - .map(|f| CameraFormatInfo { - width: f.width(), - height: f.height(), - frame_rate: f.frame_rate(), - pixel_format: f.pixel_format_name(), - }) - .collect(); - - CameraDiagnostics { - device_id: camera.device_id().to_string(), - display_name: camera.display_name().to_string(), - model_id: camera.model_id().map(|m| m.to_string()), - formats, - } - }) - .collect() -} - -fn collect_microphones(has_permission: bool) -> Vec { - if !has_permission { - return vec![]; - } - - MicrophoneFeed::list() - .into_iter() - .map(|(name, (_device, config))| MicrophoneDiagnostics { - name, - sample_rate: config.sample_rate().0, - channels: config.channels(), - sample_format: format!("{:?}", config.sample_format()), - // The richer capability/heuristic fields belong to the diagnostic - // report; the log upload keeps its existing shape. - is_default: None, - is_bluetooth: None, - is_usb: None, - is_builtin: None, - supported_configs: None, - }) - .collect() -} - -fn collect_storage_info(recordings_path: &std::path::Path) -> Option { - use sysinfo::Disks; - let disks = Disks::new_with_refreshed_list(); - - let mut best_match: Option<(&sysinfo::Disk, usize)> = None; - - for disk in disks.iter() { - if recordings_path.starts_with(disk.mount_point()) { - let mount_point_len = disk.mount_point().as_os_str().len(); - if best_match.is_none_or(|(_, len)| mount_point_len > len) { - best_match = Some((disk, mount_point_len)); - } - } - } - - best_match.map(|(disk, _)| StorageInfo { - // The diagnostic report redacts these same paths, and both fields ride - // in one upload -- leaving this one raw defeats the redaction. - recordings_path: cap_recording::diagnostics::redact_home_paths( - &recordings_path.display().to_string(), - ), - available_space_mb: disk.available_space() / (1024 * 1024), - total_space_mb: disk.total_space() / (1024 * 1024), - }) -} - -pub(crate) fn permission_status_str(status: &permissions::OSPermissionStatus) -> &'static str { - match status { - permissions::OSPermissionStatus::NotNeeded => "not_needed", - permissions::OSPermissionStatus::Empty => "not_requested", - permissions::OSPermissionStatus::Granted => "granted", - permissions::OSPermissionStatus::Denied => "denied", - } -} - -fn collect_diagnostics_for_upload( - recordings_dir: &std::path::Path, - app_data_dir: &std::path::Path, - is_recording: bool, -) -> LogUploadDiagnostics { - let hardware = cap_recording::diagnostics::collect_hardware_info(); - let system = cap_recording::diagnostics::collect_diagnostics(); - let displays = cap_recording::diagnostics::collect_displays(); - let permissions = permissions::do_permissions_check(false); - - let cameras = collect_cameras(permissions.camera.permitted()); - let microphones = collect_microphones(permissions.microphone.permitted()); - let storage = collect_storage_info(recordings_dir); - - LogUploadDiagnostics { - hardware, - system, - displays, - cameras, - microphones, - storage, - permissions: PermissionsInfo { - screen_recording: permission_status_str(&permissions.screen_recording).to_string(), - camera: permission_status_str(&permissions.camera).to_string(), - microphone: permission_status_str(&permissions.microphone).to_string(), - }, - app_state: AppStateInfo { - is_recording, - recordings_dir: cap_recording::diagnostics::redact_home_paths( - &recordings_dir.display().to_string(), - ), - app_data_dir: cap_recording::diagnostics::redact_home_paths( - &app_data_dir.display().to_string(), - ), - }, - } -} - -pub async fn upload_log_file(app: &AppHandle) -> Result<(), String> { - upload_log_file_inner(app, None).await -} - -pub(crate) async fn upload_log_file_inner( - app: &AppHandle, - report: Option, -) -> Result<(), String> { - let logs_dir = app - .state::>() - .read() - .await - .logs_dir - .clone(); - let log_bundle = tokio::task::spawn_blocking(move || { - logs_dir - .as_deref() - .map_or_else(cap_utils::log_upload::LogBundle::unavailable, |directory| { - cap_utils::log_upload::collect(directory, "cap-desktop.log") - }) - }) - .await - .map_err(|_| "Log collection could not finish".to_string())?; - - let app_data_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Failed to get app data dir: {e}"))?; - let recordings_dir = GeneralSettingsStore::recordings_dir(app); - - let is_recording = { - let app_lock = app.state::>(); - let state = app_lock.read().await; - matches!( - state.recording_state, - crate::RecordingState::Active(_) | crate::RecordingState::Pending { .. } - ) - }; - - let diagnostics = tokio::task::spawn_blocking(move || { - collect_diagnostics_for_upload(&recordings_dir, &app_data_dir, is_recording) - }) - .await - .ok(); - let diagnostics_json = serde_json::to_string(&diagnostics).unwrap_or_else(|_| "{}".to_string()); - let context = serde_json::json!({ - "schemaVersion": 1, - "app": { - "flavor": "tauri", - "version": env!("CARGO_PKG_VERSION"), - "os": std::env::consts::OS, - "binaryArchitecture": std::env::consts::ARCH, - "debugBuild": cfg!(debug_assertions), - "sourceRevision": option_env!("CAP_BUILD_REVISION"), - "sourceDirty": option_env!("CAP_BUILD_DIRTY").and_then(|value| value.parse::().ok()), - }, - "operations": cap_utils::operation_diagnostics::snapshot(), - "logCoverage": &log_bundle, - "environmentCollectedAt": "upload_time", - "mediaIncluded": false, - }); - - // Everything leaving the machine goes through the redactor. Logs record - // whole URLs on failure (reqwest's Display and Debug both append the URL), - // and an upload failure logs a presigned S3 PUT, whose query string is a - // live write credential for up to an hour. - use cap_recording::log_redaction::scrub_log_text; - - let upload = cap_utils::log_upload::prepare_upload( - log_bundle, - context, - Some(&diagnostics_json), - report.as_deref(), - scrub_log_text, - ); - let mut form = reqwest::multipart::Form::new() - .text("log", upload.log) - .text("os", std::env::consts::OS) - .text("version", env!("CARGO_PKG_VERSION")) - .text("context", upload.context); - if let Some(diagnostics) = upload.diagnostics { - form = form.text("diagnostics", diagnostics); - } - if let Some(report) = upload.report { - form = form.text("report", report); - } - - let response = app - .api_request("/api/desktop/logs", |client, url| { - client.post(url).multipart(form) - }) - .await - .map_err(|e| format!("Failed to upload logs: {e}"))?; - - if !response.status().is_success() { - return Err(format!("Upload failed with status: {}", response.status())); - } - - Ok(()) -} diff --git a/apps/desktop/src-tauri/src/macos_save_panel.rs b/apps/desktop/src-tauri/src/macos_save_panel.rs deleted file mode 100644 index c975b5d4c18..00000000000 --- a/apps/desktop/src-tauri/src/macos_save_panel.rs +++ /dev/null @@ -1,108 +0,0 @@ -use block2::RcBlock; -use objc2::rc::Retained; -use objc2::{ClassType, MainThreadMarker, msg_send}; -use objc2_app_kit::{NSApplication, NSModalResponseCancel, NSModalResponseOK, NSSavePanel}; -use objc2_foundation::{NSArray, NSString}; -use std::cell::RefCell; -use std::path::PathBuf; - -fn response_result(response: isize, path: Option) -> Result, String> { - match response { - value if value == NSModalResponseCancel => Ok(None), - value if value == NSModalResponseOK => path - .map(Some) - .ok_or_else(|| "The save dialog did not return a file path".to_string()), - _ => Err("The save dialog could not be displayed".to_string()), - } -} - -pub fn show( - file_name: &str, - extensions: &[&str], - completion: impl FnOnce(Result, String>) + 'static, -) { - let Some(mtm) = MainThreadMarker::new() else { - completion(Err( - "The save dialog must run on the main thread".to_string() - )); - return; - }; - - // AppKit can return nil here even when the bundle signature is valid (Cap #2163). - let panel: Option> = - unsafe { msg_send![NSSavePanel::class(), savePanel] }; - let Some(panel) = panel else { - completion(Err("The save dialog is unavailable".to_string())); - return; - }; - - unsafe { - panel.setCanCreateDirectories(true); - panel.setTitle(Some(&NSString::from_str("Save File"))); - panel.setNameFieldStringValue(&NSString::from_str(file_name)); - if !extensions.is_empty() { - let extensions: Vec<_> = extensions - .iter() - .map(|extension| NSString::from_str(extension)) - .collect(); - let extensions = NSArray::from_retained_slice(&extensions); - let _: () = msg_send![&*panel, setAllowedFileTypes: &*extensions]; - } - } - - let completion = RefCell::new(Some(completion)); - let callback_panel = panel.clone(); - let callback = RcBlock::new(move |response: isize| { - let Some(completion) = completion.borrow_mut().take() else { - return; - }; - let path = if response == NSModalResponseOK { - unsafe { callback_panel.URL() } - .and_then(|url| unsafe { url.path() }) - .map(|path| PathBuf::from(path.to_string())) - } else { - None - }; - callback_panel.orderOut(None); - completion(response_result(response, path)); - }); - let app = NSApplication::sharedApplication(mtm); - let parent = app.keyWindow().or_else(|| unsafe { app.mainWindow() }); - unsafe { - if let Some(parent) = parent { - panel.beginSheetModalForWindow_completionHandler(&parent, &callback); - } else { - panel.beginWithCompletionHandler(&callback); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use objc2_app_kit::NSModalResponseAbort; - - #[test] - fn explicit_cancel_does_not_become_a_dialog_failure() { - assert_eq!(response_result(NSModalResponseCancel, None), Ok(None)); - } - - #[test] - fn native_failure_is_distinct_from_explicit_cancel() { - assert!(response_result(NSModalResponseAbort, None).is_err()); - } - - #[test] - fn accepted_dialog_requires_a_destination() { - assert!(response_result(NSModalResponseOK, None).is_err()); - } - - #[test] - fn accepted_dialog_preserves_the_selected_destination() { - let path = PathBuf::from("/tmp/My Export.mov"); - assert_eq!( - response_result(NSModalResponseOK, Some(path.clone())), - Ok(Some(path)) - ); - } -} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs deleted file mode 100644 index aa57efeaa70..00000000000 --- a/apps/desktop/src-tauri/src/main.rs +++ /dev/null @@ -1,597 +0,0 @@ -#![recursion_limit = "256"] -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use std::sync::Arc; - -use cap_desktop_lib::DynLoggingLayer; -use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt}; - -const TOKIO_WORKER_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024; - -fn main() { - #[cfg(target_os = "linux")] - if let Some(threads) = cap_utils::linux_runtime::llvmpipe_thread_count() { - // Mesa counts host CPUs inside containers; configure it before spawning threads or bundled children. - unsafe { - std::env::set_var("LP_NUM_THREADS", threads.to_string()); - } - } - - #[cfg(target_os = "linux")] - if let Some(config) = cap_utils::linux_package::appimage_alsa_config_path() { - // Configure ALSA before starting threads or handing off to a bundled child process. - unsafe { - std::env::set_var("ALSA_CONFIG_PATH", config); - } - } - - #[cfg(target_os = "linux")] - if let Err(error) = cap_cli_install::appimage::dispatch_cli() { - eprintln!("{error}"); - std::process::exit(1); - } - - #[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() - { - unsafe { - std::env::set_var("ORT_DYLIB_PATH", path); - } - } - - #[cfg(debug_assertions)] - unsafe { - std::env::set_var("RUST_LOG", "trace"); - } - - #[cfg(target_os = "linux")] - unsafe { - std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); - } - - // We have to hold onto the ClientInitGuard until the very end - let _sentry_guard = std::option_env!("CAP_DESKTOP_SENTRY_URL").map(|url| { - // Crashpad minidump initialization is intentionally disabled. Its process-wide SEH - // handler terminates through TerminateProcess, bypassing panic hooks, Tauri exit - // events, and Windows Error Reporting. Re-enable it by binding this guard and - // passing it to tauri_plugin_sentry::minidump::init once the WER trace is captured. - sentry::init(( - url, - sentry::ClientOptions { - release: sentry::release_name!(), - debug: cfg!(debug_assertions), - before_send: Some(Arc::new(|mut event| { - // this is irrelevant to us + users probably don't want us knowing their computer names - event.server_name = None; - - #[cfg(debug_assertions)] - { - let msg = event.message.clone().unwrap_or("No message".into()); - println!("Sentry captured {}: {}", &event.level, &msg); - println!("-- user: {:?}", &event.user); - println!("-- event tags: {:?}", &event.tags); - println!("-- event contexts: {:?}", &event.contexts); - None - } - - #[cfg(not(debug_assertions))] - { - Some(event) - } - })), - ..Default::default() - }, - )) - }); - - let (reload_layer, handle) = tracing_subscriber::reload::Layer::new(None::); - - let logs_dir = { - #[cfg(target_os = "macos")] - let base_directory = dirs::home_dir(); - - #[cfg(not(target_os = "macos"))] - let base_directory = dirs::data_local_dir(); - - let path = resolve_log_directory(base_directory); - - #[cfg(debug_assertions)] - let path = - match cap_desktop_lib::initialize_stop_editor_benchmark(create_benchmark_log_directory) - { - Ok(directory) => directory.or(path), - Err(error) => { - eprintln!("Invalid Stop benchmark invocation: {error}"); - std::process::exit(2); - } - }; - - path - }; - - if logs_dir.is_none() { - eprintln!( - "Log directory is unavailable; file logging and persistent crash detection are disabled; console logging remains enabled" - ); - } - - let (info_file_writer, _info_logger_guard) = match ( - logs_dir.as_deref(), - create_log_appender(logs_dir.as_deref(), "cap-desktop.log"), - ) { - (Some(logs_dir), Some(appender)) => { - let (writer, guard) = tracing_appender::non_blocking( - cap_utils::diagnostic_writer::DiagnosticWriter::new( - appender, - logs_dir, - "cap-desktop.log", - ), - ); - let queue_errors = writer.error_counter(); - cap_utils::operation_diagnostics::install_queue_loss_counter(move || { - queue_errors.dropped_lines() - }); - let diagnostic_writer = writer.clone(); - cap_utils::operation_diagnostics::install_sink( - cap_utils::operation_diagnostics::AppInfo { - flavor: "tauri", - version: env!("CARGO_PKG_VERSION"), - source_revision: option_env!("CAP_BUILD_REVISION"), - debug_build: cfg!(debug_assertions), - source_dirty: option_env!("CAP_BUILD_DIRTY").map(|value| value == "true"), - }, - move |bytes| { - use std::io::Write; - let _ = diagnostic_writer.clone().write_all(bytes); - }, - ); - (Some(writer), Some(guard)) - } - _ => (None, None), - }; - - let errors_file_appender = create_log_appender(logs_dir.as_deref(), "cap-desktop-errors.log"); - - let (otel_layer, _tracer) = if cfg!(debug_assertions) { - use opentelemetry::trace::TracerProvider; - use opentelemetry_otlp::WithExportConfig; - use tracing_subscriber::Layer; - - let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder() - .with_batch_exporter( - opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_protocol(opentelemetry_otlp::Protocol::HttpJson) - .build() - .unwrap(), - ) - .with_resource( - opentelemetry_sdk::Resource::builder() - .with_service_name("cap-desktop") - .build(), - ) - .build(); - - let layer = tracing_opentelemetry::layer() - .with_tracer(tracer.tracer("cap-desktop")) - .boxed(); - - opentelemetry::global::set_tracer_provider(tracer.clone()); - (Some(layer), Some(tracer)) - } else { - (None, None) - }; - - #[cfg(debug_assertions)] - let level_filter = tracing_subscriber::filter::LevelFilter::TRACE; - #[cfg(not(debug_assertions))] - let level_filter = tracing_subscriber::filter::LevelFilter::INFO; - - tracing_subscriber::registry() - .with(tracing_subscriber::filter::filter_fn( - (|v| v.target().starts_with("cap_")) as fn(&tracing::Metadata) -> bool, - )) - .with(reload_layer) - .with(level_filter) - .with(otel_layer) - .with( - tracing_subscriber::fmt::layer() - .with_ansi(true) - .with_target(true), - ) - .with(info_file_writer.map(|writer| { - tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_target(true) - .with_writer(writer) - })) - .with(errors_file_appender.map(|appender| { - tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_target(true) - .with_writer(appender) - .with_filter(tracing_subscriber::filter::LevelFilter::WARN) - })) - .init(); - - install_panic_hook(logs_dir.clone()); - - #[cfg(debug_assertions)] - sentry::configure_scope(|scope| { - scope.set_user(Some(sentry::User { - username: Some("_DEV_".into()), - ..Default::default() - })); - }); - - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_stack_size(TOKIO_WORKER_THREAD_STACK_SIZE) - .build() - .expect("Failed to build multi threaded tokio runtime") - .block_on(async move { - drop(tokio::spawn( - cap_utils::operation_diagnostics::run_checkpoints(), - )); - cap_desktop_lib::run(handle, logs_dir).await; - }); -} - -fn resolve_log_directory(base_directory: Option) -> Option { - base_directory.map(|directory| { - #[cfg(target_os = "macos")] - let directory = directory.join("Library/Logs").join("so.cap.desktop"); - #[cfg(not(target_os = "macos"))] - let directory = directory.join("so.cap.desktop").join("logs"); - directory - }) -} - -fn create_log_appender( - directory: Option<&std::path::Path>, - prefix: &str, -) -> Option { - use std::io::Write; - - let directory = directory?; - match tracing_appender::rolling::RollingFileAppender::builder() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .filename_prefix(prefix) - .build(directory) - { - Ok(appender) => Some(appender), - Err(error) => { - let _ = writeln!( - std::io::stderr(), - "Could not open {prefix} in {}: {error}; console logging remains enabled", - directory.display() - ); - None - } - } -} - -#[cfg(debug_assertions)] -fn create_benchmark_log_directory( - output: &std::path::Path, - directory: &std::path::Path, -) -> std::io::Result { - use std::{io, path::Component}; - - if !output.is_absolute() - || !directory.is_absolute() - || output == directory - || [output, directory].into_iter().any(|path| { - path.components().any(|component| { - !matches!( - component, - Component::Prefix(_) | Component::RootDir | Component::Normal(_) - ) - }) - }) - { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Benchmark paths must be distinct absolute paths without traversal", - )); - } - let parent = directory.parent().ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "Benchmark log directory has no parent", - ) - })?; - if output.parent() != Some(parent) || parent.canonicalize()? != parent || !parent.is_dir() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Benchmark output and logs must share an existing canonical parent", - )); - } - for path in [output, directory] { - match std::fs::symlink_metadata(path) { - Ok(_) => { - return Err(io::Error::new( - io::ErrorKind::AlreadyExists, - "Benchmark output and log paths must be new", - )); - } - Err(error) if error.kind() == io::ErrorKind::NotFound => {} - Err(error) => return Err(error), - } - } - let builder = std::fs::DirBuilder::new(); - #[cfg(unix)] - let builder = { - use std::os::unix::fs::DirBuilderExt; - let mut builder = builder; - builder.mode(0o700); - builder - }; - builder.create(directory)?; - Ok(directory.to_path_buf()) -} - -fn install_panic_hook(logs_dir: Option) { - let prev = std::panic::take_hook(); - let panics_log = logs_dir.map(|directory| directory.join("panics.log")); - std::panic::set_hook(Box::new(move |info| { - let location = info - .location() - .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) - .unwrap_or_else(|| "".to_string()); - let message = info - .payload() - .downcast_ref::<&str>() - .map(|s| (*s).to_string()) - .or_else(|| info.payload().downcast_ref::().cloned()) - .unwrap_or_else(|| "".to_string()); - let backtrace = std::backtrace::Backtrace::force_capture(); - let thread = std::thread::current(); - let thread_name = thread.name().unwrap_or("").to_string(); - let timestamp = chrono::Utc::now().to_rfc3339(); - let pid = std::process::id(); - - write_panic_record( - panics_log.as_deref(), - ×tamp, - pid, - &thread_name, - &location, - &message, - &backtrace, - ); - - tracing::error!( - target: "cap_desktop_panic", - location = %location, - thread = %thread_name, - message = %message, - backtrace = %backtrace, - "panic" - ); - eprintln!( - "[cap-desktop panic] thread '{thread_name}' at {location}: {message}\nbacktrace:\n{backtrace}" - ); - prev(info); - })); -} - -fn write_panic_record( - path: Option<&std::path::Path>, - timestamp: &str, - pid: u32, - thread_name: &str, - location: &str, - message: &str, - backtrace: &std::backtrace::Backtrace, -) { - use std::io::Write; - let Some(path) = path else { - return; - }; - let Ok(mut file) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(path) - else { - return; - }; - let _ = writeln!( - file, - "[{timestamp}] pid={pid} thread='{thread_name}' at {location}: {message}\n{backtrace}\n----" - ); - let _ = file.flush(); -} - -#[cfg(test)] -mod logging_tests { - use super::{create_log_appender, resolve_log_directory, write_panic_record}; - use std::{io::Write, path::PathBuf}; - - struct LogDirectory(PathBuf); - - impl LogDirectory { - fn new() -> Self { - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(); - let directory = std::env::temp_dir().join(format!( - "cap-desktop-logging-{}-{nonce}", - std::process::id() - )); - std::fs::create_dir_all(&directory).unwrap(); - Self(directory) - } - } - - impl Drop for LogDirectory { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.0); - } - } - - #[test] - fn missing_base_directory_disables_file_logging_without_a_fallback() { - let directory = resolve_log_directory(None); - assert!(directory.is_none()); - assert!(create_log_appender(directory.as_deref(), "cap-desktop.log").is_none()); - assert!(create_log_appender(directory.as_deref(), "cap-desktop-errors.log").is_none()); - write_panic_record( - None, - "timestamp", - 1, - "test", - "test:1", - "synthetic panic", - &std::backtrace::Backtrace::disabled(), - ); - } - - #[test] - fn available_base_directory_preserves_the_platform_log_path() { - let base = LogDirectory::new(); - #[cfg(target_os = "macos")] - let expected = base.0.join("Library/Logs/so.cap.desktop"); - #[cfg(not(target_os = "macos"))] - let expected = base.0.join("so.cap.desktop/logs"); - assert_eq!(resolve_log_directory(Some(base.0.clone())), Some(expected)); - assert!(std::fs::read_dir(&base.0).unwrap().next().is_none()); - } - - #[test] - fn available_panic_log_preserves_existing_records() { - let directory = LogDirectory::new(); - let path = directory.0.join("panics.log"); - std::fs::write(&path, "existing record\n").unwrap(); - write_panic_record( - Some(&path), - "timestamp", - 1, - "test", - "test:1", - "synthetic panic", - &std::backtrace::Backtrace::disabled(), - ); - let records = std::fs::read_to_string(path).unwrap(); - assert!(records.starts_with("existing record\n")); - assert!(records.contains("synthetic panic")); - } - - #[test] - fn healthy_log_destination_preserves_existing_records() { - let directory = LogDirectory::new(); - let destination = directory.0.join("nested"); - for record in ["first\n", "second\n"] { - let mut appender = create_log_appender(Some(&destination), "cap.log").unwrap(); - appender.write_all(record.as_bytes()).unwrap(); - appender.flush().unwrap(); - } - let records: String = std::fs::read_dir(destination) - .unwrap() - .map(|entry| std::fs::read_to_string(entry.unwrap().path()).unwrap()) - .collect(); - assert!(records.contains("first\n")); - assert!(records.contains("second\n")); - } - - #[test] - fn unavailable_log_directory_disables_only_file_logging() { - let directory = LogDirectory::new(); - let destination = directory.0.join("blocked"); - std::fs::write(&destination, "existing file").unwrap(); - assert!(create_log_appender(Some(&destination), "cap.log").is_none()); - assert_eq!( - std::fs::read_to_string(destination).unwrap(), - "existing file" - ); - } - - #[test] - fn unavailable_daily_log_file_disables_only_file_logging() { - let directory = LogDirectory::new(); - let today = chrono::Utc::now().date_naive(); - for days in [-1, 0, 1] { - let date = today + chrono::Duration::days(days); - std::fs::create_dir(directory.0.join(format!("cap.log.{date}"))).unwrap(); - } - assert!(create_log_appender(Some(&directory.0), "cap.log").is_none()); - assert!(create_log_appender(Some(&directory.0), "other.log").is_some()); - } -} - -#[cfg(all(test, debug_assertions))] -mod benchmark_log_directory_tests { - use super::create_benchmark_log_directory; - - #[test] - fn benchmark_logs_use_a_new_private_sibling_without_creating_output() { - let temporary = tempfile::tempdir().unwrap(); - let root = temporary.path().canonicalize().unwrap(); - let output = root.join("benchmark.json"); - let logs = root.join("logs"); - assert_eq!( - create_benchmark_log_directory(&output, &logs).unwrap(), - logs - ); - assert!(logs.is_dir()); - assert!(!output.exists()); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!(logs.metadata().unwrap().permissions().mode() & 0o777, 0o700); - } - } - - #[test] - fn existing_or_foreign_benchmark_paths_are_preserved() { - let temporary = tempfile::tempdir().unwrap(); - let root = temporary.path().canonicalize().unwrap(); - let output = root.join("benchmark.json"); - let logs = root.join("logs"); - std::fs::write(&output, "retained evidence").unwrap(); - assert!(create_benchmark_log_directory(&output, &logs).is_err()); - assert_eq!( - std::fs::read_to_string(&output).unwrap(), - "retained evidence" - ); - assert!(!logs.exists()); - std::fs::create_dir(&logs).unwrap(); - assert!(create_benchmark_log_directory(&root.join("new.json"), &logs).is_err()); - assert!( - create_benchmark_log_directory(&root.join("new.json"), &root.join("other/logs")) - .is_err() - ); - assert!( - create_benchmark_log_directory( - std::path::Path::new("relative.json"), - &root.join("fresh") - ) - .is_err() - ); - assert!( - create_benchmark_log_directory(&root.join("new.json"), &root.join("nested/../fresh")) - .is_err() - ); - assert!(!root.join("fresh").exists()); - } - - #[cfg(unix)] - #[test] - fn symlinked_benchmark_parents_are_rejected_without_writes() { - use std::os::unix::fs::symlink; - let temporary = tempfile::tempdir().unwrap(); - let root = temporary.path().canonicalize().unwrap(); - let actual = root.join("actual"); - let alias = root.join("alias"); - std::fs::create_dir(&actual).unwrap(); - symlink(&actual, &alias).unwrap(); - assert!( - create_benchmark_log_directory(&alias.join("result.json"), &alias.join("logs")) - .is_err() - ); - assert!(!actual.join("logs").exists()); - assert!(!actual.join("result.json").exists()); - } -} diff --git a/apps/desktop/src-tauri/src/main_window_geometry.rs b/apps/desktop/src-tauri/src/main_window_geometry.rs deleted file mode 100644 index 17ed9e88b1c..00000000000 --- a/apps/desktop/src-tauri/src/main_window_geometry.rs +++ /dev/null @@ -1,54 +0,0 @@ -pub(crate) const SIZE: (f64, f64) = (330.0, 395.0); -const PADDING: f64 = 12.0; - -pub(crate) fn restored_position( - before: (f64, f64), - window_size: (f64, f64), - work_origin: (f64, f64), - work_size: (f64, f64), - scale: f64, -) -> (f64, f64) { - let padding = PADDING * scale; - let minimum = (work_origin.0 + padding, work_origin.1 + padding); - let maximum = ( - (work_origin.0 + work_size.0 - window_size.0 - padding).max(minimum.0), - (work_origin.1 + work_size.1 - window_size.1 - padding).max(minimum.1), - ); - ( - before.0.clamp(minimum.0, maximum.0), - before.1.clamp(minimum.1, maximum.1), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn preserves_top_left_and_clamps_on_scaled_negative_coordinate_screens() { - assert_eq!( - restored_position( - (-1800.0, 80.0), - (1200.0, 1320.0), - (-2880.0, 0.0), - (2880.0, 1800.0), - 2.0 - ), - (-1800.0, 80.0) - ); - assert_eq!( - restored_position( - (-400.0, 1700.0), - (1200.0, 1320.0), - (-2880.0, 0.0), - (2880.0, 1800.0), - 2.0 - ), - (-1224.0, 456.0) - ); - assert_eq!( - restored_position((0.0, 0.0), (600.0, 660.0), (0.0, 0.0), (500.0, 500.0), 1.0), - (12.0, 12.0) - ); - } -} diff --git a/apps/desktop/src-tauri/src/notifications.rs b/apps/desktop/src-tauri/src/notifications.rs deleted file mode 100644 index be0d16563d6..00000000000 --- a/apps/desktop/src-tauri/src/notifications.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::{AppSounds, general_settings::GeneralSettingsStore}; -#[cfg(not(target_os = "linux"))] -use tauri_plugin_notification::NotificationExt; - -#[allow(unused)] -pub enum NotificationType { - VideoSaved, - VideoCopiedToClipboard, - ShareableLinkCopied, - UploadFailed, - VideoSaveFailed, - VideoCopyFailed, - ShareableLinkFailed, - ScreenshotSaved, - ScreenshotCopiedToClipboard, - ScreenshotSaveFailed, - ScreenshotCopyFailed, -} - -impl NotificationType { - fn details(&self) -> (&'static str, &'static str, bool) { - match self { - NotificationType::VideoSaved => ("Video Saved", "Video saved successfully", false), - NotificationType::VideoCopiedToClipboard => { - ("Video Copied", "Video copied to clipboard", false) - } - NotificationType::ShareableLinkCopied => { - ("Link Copied", "Link copied to clipboard", false) - } - NotificationType::UploadFailed => ( - "Upload Failed", - "Unable to upload media. Please try again", - true, - ), - NotificationType::VideoSaveFailed => ( - "Save Failed", - "Unable to save video. Please try again", - true, - ), - NotificationType::VideoCopyFailed => ( - "Copy Failed", - "Unable to copy video to clipboard. Please try again", - true, - ), - NotificationType::ShareableLinkFailed => ( - "Share Failed", - "Unable to create shareable link. Please try again", - true, - ), - NotificationType::ScreenshotSaved => { - ("Screenshot Saved", "Screenshot saved successfully", false) - } - NotificationType::ScreenshotCopiedToClipboard => { - ("Screenshot Copied", "Screenshot copied to clipboard", false) - } - NotificationType::ScreenshotSaveFailed => ( - "Save Failed", - "Unable to save screenshot. Please try again", - true, - ), - NotificationType::ScreenshotCopyFailed => ( - "Copy Failed", - "Unable to copy screenshot to clipboard. Please try again", - true, - ), - } - } - - #[allow(unused)] - pub fn message(&self) -> &'static str { - match self { - NotificationType::UploadFailed => { - "Failed to upload your video after multiple attempts. Please try again later." - } - _ => "", - } - } - - #[allow(unused)] - pub fn title(&self) -> &'static str { - match self { - NotificationType::UploadFailed => "Upload Failed", - _ => "", - } - } - - pub fn send(self, app: &tauri::AppHandle) { - send_notification(app, self); - } -} - -pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType) { - let enable_notifications = GeneralSettingsStore::get(app) - .map(|settings| settings.is_some_and(|s| s.enable_notifications)) - .unwrap_or(false); - - if !enable_notifications { - return; - } - - let (title, body, _is_error) = notification_type.details(); - - #[cfg(target_os = "linux")] - tauri::async_runtime::spawn(async move { - if let Err(error) = show_linux_notification(title, body).await { - tracing::warn!(%error, "Failed to send notification"); - } - }); - - #[cfg(not(target_os = "linux"))] - app.notification() - .builder() - .title(title) - .body(body) - .show() - .ok(); - - let skip_sound = matches!( - notification_type, - NotificationType::ScreenshotSaved - | NotificationType::ScreenshotCopiedToClipboard - | NotificationType::ScreenshotSaveFailed - | NotificationType::ScreenshotCopyFailed - ); - - if !skip_sound { - AppSounds::Notification.play(); - } -} - -#[cfg(target_os = "linux")] -fn build_linux_notification(title: &str, body: &str) -> notify_rust::Notification { - let mut notification = notify_rust::Notification::new(); - notification.summary(title).body(body).auto_icon(); - notification -} - -#[cfg(target_os = "linux")] -pub(crate) async fn show_linux_notification(title: &str, body: &str) -> Result<(), String> { - build_linux_notification(title, body) - .show_async() - .await - .map(|_| ()) - .map_err(|error| format!("Failed to send notification: {error}")) -} - -#[cfg(all(test, target_os = "linux"))] -mod tests { - use super::build_linux_notification; - - #[test] - fn linux_notification_preserves_content_and_application_icon() { - let notification = build_linux_notification("Screenshot Saved", "Saved successfully"); - - assert_eq!(notification.summary, "Screenshot Saved"); - assert_eq!(notification.body, "Saved successfully"); - assert_eq!(notification.icon, notification.appname); - assert!(!notification.icon.is_empty()); - } -} diff --git a/apps/desktop/src-tauri/src/panel_manager.rs b/apps/desktop/src-tauri/src/panel_manager.rs deleted file mode 100644 index 68bc7eff07d..00000000000 --- a/apps/desktop/src-tauri/src/panel_manager.rs +++ /dev/null @@ -1,243 +0,0 @@ -#[cfg(target_os = "macos")] -use std::collections::HashMap; -#[cfg(target_os = "macos")] -use tokio::sync::RwLock; -#[cfg(target_os = "macos")] -use tracing::{debug, info, trace, warn}; - -#[cfg(target_os = "macos")] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum PanelState { - #[default] - None, - Creating, - Ready, - Destroying, -} - -#[cfg(target_os = "macos")] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PanelWindowType { - Camera, -} - -#[cfg(target_os = "macos")] -impl std::fmt::Display for PanelWindowType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Camera => write!(f, "Camera"), - } - } -} - -#[cfg(target_os = "macos")] -struct PanelEntry { - state: PanelState, - operation_id: u64, -} - -#[cfg(target_os = "macos")] -impl Default for PanelEntry { - fn default() -> Self { - Self { - state: PanelState::None, - operation_id: 0, - } - } -} - -#[cfg(target_os = "macos")] -pub struct PanelManager { - panels: RwLock>, - operation_counter: std::sync::atomic::AtomicU64, -} - -#[cfg(target_os = "macos")] -impl Default for PanelManager { - fn default() -> Self { - Self::new() - } -} - -#[cfg(target_os = "macos")] -impl PanelManager { - pub fn new() -> Self { - Self { - panels: RwLock::new(HashMap::new()), - operation_counter: std::sync::atomic::AtomicU64::new(0), - } - } - - pub async fn get_state(&self, window_type: PanelWindowType) -> PanelState { - let panels = self.panels.read().await; - panels - .get(&window_type) - .map(|e| e.state) - .unwrap_or(PanelState::None) - } - - pub async fn try_begin_create( - &self, - window_type: PanelWindowType, - ) -> Option { - let mut panels = self.panels.write().await; - let entry = panels.entry(window_type).or_default(); - - match entry.state { - PanelState::None => { - let op_id = self - .operation_counter - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - entry.state = PanelState::Creating; - entry.operation_id = op_id; - debug!( - "Panel {}: beginning create operation (op_id={})", - window_type, op_id - ); - Some(PanelOperationGuard { - operation_id: op_id, - completed: false, - }) - } - PanelState::Creating => { - debug!( - "Panel {}: create blocked - already creating (op_id={})", - window_type, entry.operation_id - ); - None - } - PanelState::Ready => { - debug!( - "Panel {}: create blocked - already ready (op_id={})", - window_type, entry.operation_id - ); - None - } - PanelState::Destroying => { - debug!( - "Panel {}: create blocked - currently destroying (op_id={})", - window_type, entry.operation_id - ); - None - } - } - } - - pub async fn mark_ready(&self, window_type: PanelWindowType, operation_id: u64) { - let mut panels = self.panels.write().await; - if let Some(entry) = panels.get_mut(&window_type) { - if entry.operation_id == operation_id && entry.state == PanelState::Creating { - entry.state = PanelState::Ready; - info!( - "Panel {}: marked ready (op_id={})", - window_type, operation_id - ); - } else { - warn!( - "Panel {}: mark_ready ignored - state mismatch (current state={:?}, current op={}, requested op={})", - window_type, entry.state, entry.operation_id, operation_id - ); - } - } - } - - pub async fn force_reset(&self, window_type: PanelWindowType) { - let mut panels = self.panels.write().await; - if let Some(entry) = panels.get_mut(&window_type) { - warn!( - "Panel {}: force reset from state {:?} (op_id={})", - window_type, entry.state, entry.operation_id - ); - entry.state = PanelState::None; - entry.operation_id = 0; - } - } - - pub async fn wait_for_state( - &self, - window_type: PanelWindowType, - target_states: &[PanelState], - timeout: std::time::Duration, - ) -> bool { - let start = std::time::Instant::now(); - let poll_interval = std::time::Duration::from_millis(10); - - while start.elapsed() < timeout { - let state = self.get_state(window_type).await; - if target_states.contains(&state) { - return true; - } - tokio::time::sleep(poll_interval).await; - } - - let state = self.get_state(window_type).await; - warn!( - "Panel {}: wait_for_state timed out after {:?}, current state={:?}, wanted one of {:?}", - window_type, timeout, state, target_states - ); - false - } -} - -#[cfg(target_os = "macos")] -pub struct PanelOperationGuard { - pub operation_id: u64, - completed: bool, -} - -#[cfg(target_os = "macos")] -impl PanelOperationGuard { - pub fn mark_completed(&mut self) { - self.completed = true; - } -} - -#[cfg(target_os = "macos")] -pub fn is_window_handle_valid(window: &tauri::WebviewWindow) -> bool { - match window.inner_size() { - Ok(_) => true, - Err(e) => { - trace!("Window handle validation failed: {}", e); - false - } - } -} - -#[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-tauri/src/permissions.rs deleted file mode 100644 index 6c37e8e9c5b..00000000000 --- a/apps/desktop/src-tauri/src/permissions.rs +++ /dev/null @@ -1,979 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[cfg(target_os = "macos")] -use crate::{general_settings::GeneralSettingsStore, windows::CapWindowId}; -#[cfg(target_os = "macos")] -use cidre::{av, sc}; -#[cfg(target_os = "macos")] -use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; -#[cfg(target_os = "macos")] -use std::{ - future::Future, - str::FromStr, - sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering}, - time::Duration, -}; -#[cfg(target_os = "macos")] -use tauri::Manager; -use tracing::instrument; - -#[cfg(target_os = "macos")] -static MACOS_DOCK_VISIBILITY_SYNC_GENERATION: AtomicU64 = AtomicU64::new(0); -#[cfg(target_os = "macos")] -static MACOS_PENDING_PANEL_WINDOWS: AtomicU32 = AtomicU32::new(0); -#[cfg(target_os = "macos")] -static MACOS_SCK_PERMISSION_MISMATCH_LOGGED: AtomicBool = AtomicBool::new(false); -#[cfg(target_os = "macos")] -static MACOS_SCK_DISPLAYS_VALIDATED: AtomicBool = AtomicBool::new(false); -#[cfg(target_os = "macos")] -static MACOS_SCK_DISPLAYS_VALIDATION_IN_FLIGHT: AtomicBool = AtomicBool::new(false); -#[cfg(target_os = "macos")] -static MACOS_SCK_LAST_STRICT_OUTCOME: AtomicU8 = AtomicU8::new(MACOS_SCK_OUTCOME_UNKNOWN); -#[cfg(target_os = "macos")] -static MACOS_SCK_LAST_VALIDATION_ATTEMPT: std::sync::Mutex> = - std::sync::Mutex::new(None); -#[cfg(target_os = "macos")] -const MACOS_SCK_VALIDATION_RETRY_INTERVAL: Duration = Duration::from_secs(5); -// Strictly less than the retry interval: the backoff stamp is refreshed when -// an attempt completes, and a timeout that consumed the whole interval would -// otherwise leave zero backoff between attempts against a wedged replayd. -#[cfg(target_os = "macos")] -const MACOS_SCK_VALIDATION_TIMEOUT: Duration = Duration::from_secs(4); -#[cfg(target_os = "macos")] -const MACOS_SCK_OUTCOME_UNKNOWN: u8 = 0; -#[cfg(target_os = "macos")] -const MACOS_SCK_OUTCOME_OK: u8 = 1; -#[cfg(target_os = "macos")] -const MACOS_SCK_OUTCOME_FAILED: u8 = 2; - -#[cfg(target_os = "macos")] -pub(crate) struct MacosPanelWindowActivationGuard { - app: tauri::AppHandle, -} - -#[cfg(target_os = "macos")] -impl Drop for MacosPanelWindowActivationGuard { - fn drop(&mut self) { - let pending = MACOS_PENDING_PANEL_WINDOWS.fetch_sub(1, Ordering::AcqRel); - if pending == 1 { - schedule_macos_dock_visibility_sync(&self.app); - } - } -} - -#[cfg(target_os = "macos")] -#[link(name = "ApplicationServices", kind = "framework")] -unsafe extern "C" { - fn AXIsProcessTrusted() -> bool; - fn AXIsProcessTrustedWithOptions(options: core_foundation::dictionary::CFDictionaryRef) - -> bool; -} - -#[cfg(target_os = "macos")] -fn macos_prompt_screen_recording_access() { - scap_screencapturekit::request_permission(); -} - -#[cfg(target_os = "macos")] -fn macos_prompt_accessibility_access() { - use core_foundation::base::TCFType; - use core_foundation::dictionary::CFDictionary; - use core_foundation::string::CFString; - - let prompt_key = CFString::new("AXTrustedCheckOptionPrompt"); - let prompt_value = core_foundation::boolean::CFBoolean::true_value(); - - let options = - CFDictionary::from_CFType_pairs(&[(prompt_key.as_CFType(), prompt_value.as_CFType())]); - - unsafe { - AXIsProcessTrustedWithOptions(options.as_concrete_TypeRef()); - } -} - -#[cfg(target_os = "macos")] -fn macos_run_on_main_thread( - app: &tauri::AppHandle, - callback: impl FnOnce() -> R + Send + 'static, -) -> Option { - use std::sync::mpsc; - - let (tx, rx) = mpsc::sync_channel(1); - - if let Err(err) = app.run_on_main_thread(move || { - let _ = tx.send(callback()); - }) { - tracing::warn!("Failed to run permission action on main thread: {err}"); - return None; - } - - rx.recv_timeout(Duration::from_secs(2)).ok() -} - -#[cfg(target_os = "macos")] -fn macos_permission_settings_url(permission: &OSPermission) -> &'static str { - match permission { - OSPermission::ScreenRecording => { - "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture" - } - OSPermission::Camera => { - "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera" - } - OSPermission::Microphone => { - "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone" - } - OSPermission::Accessibility => { - "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - } - } -} - -#[cfg(target_os = "macos")] -fn macos_permission_needs_settings_fallback(permission: &OSPermission) -> bool { - matches!( - permission, - OSPermission::ScreenRecording | OSPermission::Accessibility - ) -} - -#[cfg(target_os = "macos")] -fn macos_focus_permission_window(app: &tauri::AppHandle) { - if let Some(window) = ["onboarding", "main", "settings"] - .into_iter() - .find_map(|label| app.get_webview_window(label)) - { - let _ = window.show(); - let _ = window.set_focus(); - } -} - -#[cfg(target_os = "macos")] -fn macos_prepare_permission_request(app: &tauri::AppHandle, focus_app: bool) { - if crate::app_is_exiting(app) { - return; - } - - 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) { - tracing::warn!("Failed to set activation policy to Regular: {err}"); - } - - if !focus_app { - return; - } - - macos_focus_permission_window(app); - - if let Some(current_app) = unsafe { - NSRunningApplication::runningApplicationWithProcessIdentifier(std::process::id() as _) - } { - unsafe { - current_app - .activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); - } - } -} - -#[cfg(target_os = "macos")] -fn macos_sync_activation_policy(app: &tauri::AppHandle, should_show_dock: bool) { - let policy = if should_show_dock { - tauri::ActivationPolicy::Regular - } else { - tauri::ActivationPolicy::Accessory - }; - - if let Err(err) = app.set_activation_policy(policy) { - tracing::warn!("Failed to update activation policy: {err}"); - } -} - -// Changing the activation policy (which `set_dock_visibility` also does under -// the hood) while any window owns a fullscreen Space makes AppKit throw an -// 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 { - app.webview_windows() - .values() - .any(|window| window.is_fullscreen().unwrap_or(false)) -} - -#[cfg(target_os = "macos")] -pub(crate) fn prepare_macos_panel_window( - app: &tauri::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) { - tracing::warn!("Failed to prepare macOS panel activation policy: {err}"); - } - } - - MacosPanelWindowActivationGuard { app: app.clone() } -} - -#[cfg(target_os = "macos")] -pub(crate) fn sync_macos_dock_visibility(app: &tauri::AppHandle) { - if crate::app_is_exiting(app) || MACOS_PENDING_PANEL_WINDOWS.load(Ordering::Acquire) > 0 { - return; - } - - if macos_any_window_fullscreen(app) { - return; - } - - let should_hide_dock = GeneralSettingsStore::get(app) - .ok() - .flatten() - .is_some_and(|settings| settings.hide_dock_icon); - - let has_visible_panel_window = app.webview_windows().iter().any(|(label, window)| { - CapWindowId::from_str(label) - .map(|id| !id.activates_dock() && window.is_visible().unwrap_or(false)) - .unwrap_or(false) - }); - - if has_visible_panel_window && should_hide_dock { - return; - } - - let has_visible_dock_window = app.webview_windows().iter().any(|(label, window)| { - CapWindowId::from_str(label) - .map(|window_id| window_id.activates_dock() && window.is_visible().unwrap_or(false)) - .unwrap_or(false) - }); - - let should_show_dock = !should_hide_dock || has_visible_dock_window; - - macos_sync_activation_policy(app, should_show_dock); - - if let Err(err) = app.set_dock_visibility(should_show_dock) { - tracing::warn!("Failed to update dock visibility: {err}"); - } -} - -#[cfg(target_os = "macos")] -pub(crate) fn schedule_macos_dock_visibility_sync(app: &tauri::AppHandle) { - let generation = MACOS_DOCK_VISIBILITY_SYNC_GENERATION.fetch_add(1, Ordering::AcqRel) + 1; - let app = app.clone(); - tauri::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); - } - }); -} - -#[cfg(target_os = "macos")] -fn macos_permission_status(permission: &OSPermission, initial_check: bool) -> OSPermissionStatus { - match permission { - OSPermission::ScreenRecording => { - let granted = macos_screen_recording_available(); - match (granted, initial_check) { - (true, _) => OSPermissionStatus::Granted, - (false, true) => OSPermissionStatus::Empty, - (false, false) => OSPermissionStatus::Denied, - } - } - OSPermission::Camera => { - match av::CaptureDevice::authorization_status_for_media_type(av::MediaType::video()) { - Ok(av::AuthorizationStatus::NotDetermined) => OSPermissionStatus::Empty, - Ok(av::AuthorizationStatus::Authorized) => OSPermissionStatus::Granted, - Ok(_) => OSPermissionStatus::Denied, - Err(err) => { - tracing::error!("Failed to query AV permission status: {err}"); - OSPermissionStatus::Denied - } - } - } - OSPermission::Microphone => { - match av::CaptureDevice::authorization_status_for_media_type(av::MediaType::audio()) { - Ok(av::AuthorizationStatus::NotDetermined) => OSPermissionStatus::Empty, - Ok(av::AuthorizationStatus::Authorized) => OSPermissionStatus::Granted, - Ok(_) => OSPermissionStatus::Denied, - Err(err) => { - tracing::error!("Failed to query AV permission status: {err}"); - OSPermissionStatus::Denied - } - } - } - OSPermission::Accessibility => { - if unsafe { AXIsProcessTrusted() } { - OSPermissionStatus::Granted - } else if initial_check { - OSPermissionStatus::Empty - } else { - OSPermissionStatus::Denied - } - } - } -} - -// The SCShareableContent snapshot behind this validation materialises every -// window/app/display on the system (~1MB+ of ObjC objects per call). Polling -// callers (devices snapshot emitter every 5s, permission UIs down to 250ms) -// used to re-run it for the whole process lifetime, leaking the graph on -// pool-less tokio threads at ~15MB/min until macOS exhausted swap (issue -// #2023, the 82GB incident). A successful validation is cached for the rest -// of the process: runtime revocation is caught by the cheap CGPreflight gate, -// and macOS relaunches the app on screen-recording permission changes anyway. -// Failed validations retry at most once per MACOS_SCK_VALIDATION_RETRY_INTERVAL. -#[cfg(target_os = "macos")] -fn macos_screen_recording_available() -> bool { - macos_screen_recording_available_with( - scap_screencapturekit::has_permission(), - MACOS_SCK_DISPLAYS_VALIDATED.load(Ordering::Acquire), - MACOS_SCK_LAST_STRICT_OUTCOME.load(Ordering::Acquire), - objc2::MainThreadMarker::new().is_some(), - macos_spawn_sck_displays_validation, - || { - // block_in_place covers the mutex acquisition too: waiters serialised - // behind an in-flight validation would otherwise park a tokio worker - // without telling the runtime. - if tokio::runtime::Handle::try_current().is_ok() { - tokio::task::block_in_place(macos_validate_sck_displays) - } else { - macos_validate_sck_displays() - } - }, - ) -} - -// SCShareableContent's completion needs the main run loop, so blocking the -// main thread on the strict validation deadlocks the whole app. Both -// 2026-08-03 macOS hang reports show it: onboarding tray rebuilds -// (run_on_main_thread → build_tray_menu → do_permissions_check) parked the -// main thread in block_on for 76+s until force-quit. Main-thread callers are -// the tray menu builds (tray.rs), create_tray, the setup-time log line -// (lib.rs), and RunEvent::Reopen → should_show_onboarding (lib.rs); they get -// the last strictly-observed answer without blocking — optimistic until the -// first strict validation completes, since CGPreflight already passed. Reopen -// consuming an optimistic answer is self-correcting: ShowCapWindow::Main -// re-checks off-main (windows.rs) and redirects back to Onboarding. The -// onboarding/permission UIs poll via async commands and keep the strict path. -#[cfg(target_os = "macos")] -fn macos_screen_recording_available_with( - preflight_granted: bool, - displays_validated: bool, - last_strict_outcome: u8, - on_main_thread: bool, - spawn_background_validation: impl FnOnce(), - validate: impl FnOnce() -> bool, -) -> bool { - if !preflight_granted { - return false; - } - - if displays_validated { - return true; - } - - if on_main_thread { - spawn_background_validation(); - return last_strict_outcome != MACOS_SCK_OUTCOME_FAILED; - } - - validate() -} - -#[cfg(target_os = "macos")] -struct MacosSckValidationInFlightReset; - -#[cfg(target_os = "macos")] -impl Drop for MacosSckValidationInFlightReset { - fn drop(&mut self) { - MACOS_SCK_DISPLAYS_VALIDATION_IN_FLIGHT.store(false, Ordering::Release); - } -} - -// Blocking pool rather than a raw std::thread: pool threads carry the runtime -// handle without tokio's "entered" flag, so the validator's nested -// Handle::block_on is legal there (it panics on entered threads, which rules -// out tokio::spawn), they inherit the 16MiB stack configured in main.rs -// (SCK graph walks are why it was raised), and an idle pool thread is reused -// instead of spawning one per check. The Drop guard clears the in-flight -// latch even if the validation panics. -#[cfg(target_os = "macos")] -fn macos_spawn_sck_displays_validation() { - if MACOS_SCK_DISPLAYS_VALIDATION_IN_FLIGHT.swap(true, Ordering::AcqRel) { - return; - } - tauri::async_runtime::spawn_blocking(|| { - let _reset = MacosSckValidationInFlightReset; - macos_validate_sck_displays(); - }); -} - -#[cfg(target_os = "macos")] -fn macos_validate_sck_displays() -> bool { - // Serialise validators: concurrent callers during the first startup check - // must wait for the in-flight validation rather than tripping the backoff - // and transiently reporting a granted permission as denied. - let mut last_attempt = MACOS_SCK_LAST_VALIDATION_ATTEMPT - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if MACOS_SCK_DISPLAYS_VALIDATED.load(Ordering::Acquire) { - return true; - } - if let Some(attempted_at) = *last_attempt - && attempted_at.elapsed() < MACOS_SCK_VALIDATION_RETRY_INTERVAL - { - return false; - } - *last_attempt = Some(std::time::Instant::now()); - - let validated = objc2::rc::autoreleasepool(|_| { - tauri::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. - let content = match tokio::time::timeout( - MACOS_SCK_VALIDATION_TIMEOUT, - sc::ShareableContent::current(), - ) - .await - { - Ok(result) => result, - Err(_) => { - tracing::warn!( - timeout_ms = MACOS_SCK_VALIDATION_TIMEOUT.as_millis() as u64, - "ScreenCaptureKit shareable content query timed out during permission check" - ); - return false; - } - }; - match content { - Ok(content) => { - let display_count = content.displays().len(); - if display_count == 0 - && !MACOS_SCK_PERMISSION_MISMATCH_LOGGED.swap(true, Ordering::AcqRel) - { - tracing::debug!( - window_count = content.windows().len(), - application_count = content.apps().len(), - "ScreenCaptureKit returned no displays despite CoreGraphics screen-recording permission" - ); - } - display_count > 0 - } - Err(error) => { - tracing::debug!( - error = %error, - "ScreenCaptureKit shareable content unavailable during permission check" - ); - false - } - } - }) - }); - - if validated { - MACOS_SCK_DISPLAYS_VALIDATED.store(true, Ordering::Release); - } - MACOS_SCK_LAST_STRICT_OUTCOME.store( - if validated { - MACOS_SCK_OUTCOME_OK - } else { - MACOS_SCK_OUTCOME_FAILED - }, - Ordering::Release, - ); - // Re-stamp on completion: an attempt can consume up to the SCK timeout, - // so a start-only stamp would leave a timed-out attempt with an already - // expired backoff window (back-to-back attempts against a wedged - // replayd). The start stamp above stays as insurance in case this thread - // dies mid-attempt. - *last_attempt = Some(std::time::Instant::now()); - validated -} - -#[cfg(target_os = "macos")] -fn macos_request_permission(app: &tauri::AppHandle, permission: &OSPermission) { - match permission { - OSPermission::ScreenRecording => { - if macos_run_on_main_thread(app, macos_prompt_screen_recording_access).is_none() { - macos_prompt_screen_recording_access(); - } - } - OSPermission::Camera => { - futures::executor::block_on(av::CaptureDevice::request_access_for_media_type( - av::MediaType::video(), - )) - .ok(); - } - OSPermission::Microphone => { - futures::executor::block_on(av::CaptureDevice::request_access_for_media_type( - av::MediaType::audio(), - )) - .ok(); - } - OSPermission::Accessibility => { - if macos_run_on_main_thread(app, macos_prompt_accessibility_access).is_none() { - macos_prompt_accessibility_access(); - } - } - } -} - -#[cfg(target_os = "macos")] -async fn macos_wait_for_permission_update_with( - mut check: TCheck, - mut sleep: impl FnMut() -> TSleep, -) -> bool -where - TCheck: FnMut() -> bool, - TSleep: Future, -{ - if check() { - return true; - } - - for _ in 0..10 { - sleep().await; - if check() { - return true; - } - } - - false -} - -#[cfg(target_os = "macos")] -async fn macos_wait_for_permission_update(permission: &OSPermission) -> bool { - // The user just interacted with the permission prompt; drop the SCK - // validation backoff so this poll loop sees fresh answers instead of a - // stale negative from up to 5s ago. block_in_place because an in-flight - // validation holds this mutex for up to the SCK timeout, and waiting it - // out is deliberate — its result predates the grant, so the reset must - // land after it finishes. - if matches!(permission, OSPermission::ScreenRecording) { - tokio::task::block_in_place(|| { - *MACOS_SCK_LAST_VALIDATION_ATTEMPT - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = None; - }); - } - - macos_wait_for_permission_update_with( - || macos_permission_status(permission, false).permitted(), - || tokio::time::sleep(Duration::from_millis(200)), - ) - .await -} - -#[cfg(target_os = "macos")] -fn macos_open_permission_settings(app: &tauri::AppHandle, permission: &OSPermission) { - use std::process::Command; - - let process = Command::new("open") - .arg(macos_permission_settings_url(permission)) - .spawn(); - - match process { - Ok(mut process) => { - let app = app.clone(); - tokio::spawn(async move { - match tokio::task::spawn_blocking(move || process.wait()).await { - Ok(Err(err)) => { - tracing::error!("Error waiting for permission settings process: {err}"); - } - Err(err) => { - tracing::error!("Join error waiting for permission settings: {err}"); - } - _ => {} - } - crate::tray::refresh_tray_menu_for_app(&app); - sync_macos_dock_visibility(&app); - }); - } - Err(err) => { - tracing::error!("Failed to open permission settings: {err}"); - sync_macos_dock_visibility(app); - } - } -} - -#[derive(Debug, Serialize, Deserialize, specta::Type, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum OSPermission { - ScreenRecording, - Camera, - Microphone, - Accessibility, -} - -#[tauri::command(async)] -#[specta::specta] -pub fn open_permission_settings(_app: tauri::AppHandle, _permission: OSPermission) { - #[cfg(target_os = "macos")] - { - macos_prepare_permission_request(&_app, false); - macos_open_permission_settings(&_app, &_permission); - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(_app))] -pub async fn request_permission(_app: tauri::AppHandle, _permission: OSPermission) { - #[cfg(target_os = "macos")] - { - macos_prepare_permission_request(&_app, true); - - let permission = _permission; - let app = _app.clone(); - tauri::async_runtime::spawn_blocking(move || { - macos_request_permission(&app, &permission); - }) - .await - .ok(); - - let granted = macos_wait_for_permission_update(&_permission).await; - - if macos_permission_needs_settings_fallback(&_permission) && !granted { - macos_open_permission_settings(&_app, &_permission); - } else { - sync_macos_dock_visibility(&_app); - } - } - - crate::tray::refresh_tray_menu_for_app(&_app); -} - -pub(crate) fn check_camera_access() -> Result<(), String> { - #[cfg(target_os = "macos")] - let status = - objc2::rc::autoreleasepool(|_| macos_permission_status(&OSPermission::Camera, false)); - #[cfg(not(target_os = "macos"))] - let status = OSPermissionStatus::NotNeeded; - camera_access_result(status) -} - -fn camera_access_result(status: OSPermissionStatus) -> Result<(), String> { - match status { - OSPermissionStatus::Granted | OSPermissionStatus::NotNeeded => Ok(()), - OSPermissionStatus::Empty => Err( - "Camera access is required. Click the camera control to allow access, then select your camera again." - .into(), - ), - OSPermissionStatus::Denied => Err( - "Camera access is blocked. Allow Cap in System Settings > Privacy & Security > Camera, then select your camera again. If access is restricted, contact your administrator." - .into(), - ), - } -} - -pub(crate) fn check_microphone_access() -> Result<(), String> { - #[cfg(target_os = "macos")] - let status = - objc2::rc::autoreleasepool(|_| macos_permission_status(&OSPermission::Microphone, false)); - #[cfg(not(target_os = "macos"))] - let status = OSPermissionStatus::NotNeeded; - microphone_access_result(status) -} - -fn microphone_access_result(status: OSPermissionStatus) -> Result<(), String> { - match status { - OSPermissionStatus::Granted | OSPermissionStatus::NotNeeded => Ok(()), - OSPermissionStatus::Empty => Err( - "Microphone access is required. Click the microphone control to allow access, then select your microphone again." - .into(), - ), - OSPermissionStatus::Denied => Err( - "Microphone access is blocked. Allow Cap in System Settings > Privacy & Security > Microphone, then select your microphone again." - .into(), - ), - } -} - -#[derive(Serialize, Deserialize, Debug, specta::Type, Clone)] -#[serde(rename_all = "camelCase")] -pub enum OSPermissionStatus { - // This platform does not require this permission - NotNeeded, - // The user has neither granted nor denied permission - Empty, - // The user has explicitly granted permission - Granted, - // The user has denied permission, or has granted it but not yet restarted - Denied, -} - -impl OSPermissionStatus { - pub fn permitted(&self) -> bool { - matches!(self, Self::NotNeeded | Self::Granted) - } -} - -#[derive(Serialize, Deserialize, Debug, specta::Type, Clone)] -#[serde(rename_all = "camelCase")] -pub struct OSPermissionsCheck { - pub screen_recording: OSPermissionStatus, - pub microphone: OSPermissionStatus, - pub camera: OSPermissionStatus, - pub accessibility: OSPermissionStatus, -} - -impl OSPermissionsCheck { - pub fn necessary_granted(&self) -> bool { - self.screen_recording.permitted() && self.accessibility.permitted() - } -} - -#[tauri::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 - // no ambient NSAutoreleasePool) from polling callers; without it every - // autoreleased AVFoundation/AppKit temporary leaks for the process lifetime. - #[cfg(target_os = "macos")] - { - objc2::rc::autoreleasepool(|_| OSPermissionsCheck { - screen_recording: macos_permission_status( - &OSPermission::ScreenRecording, - _initial_check, - ), - microphone: macos_permission_status(&OSPermission::Microphone, _initial_check), - camera: macos_permission_status(&OSPermission::Camera, _initial_check), - accessibility: macos_permission_status(&OSPermission::Accessibility, _initial_check), - }) - } - - #[cfg(not(target_os = "macos"))] - { - OSPermissionsCheck { - screen_recording: OSPermissionStatus::NotNeeded, - microphone: OSPermissionStatus::NotNeeded, - camera: OSPermissionStatus::NotNeeded, - accessibility: OSPermissionStatus::NotNeeded, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn camera_setup_requires_granted_access() { - for status in [OSPermissionStatus::Granted, OSPermissionStatus::NotNeeded] { - assert!(camera_access_result(status).is_ok()); - } - assert!( - camera_access_result(OSPermissionStatus::Empty) - .unwrap_err() - .contains("camera control") - ); - let blocked = camera_access_result(OSPermissionStatus::Denied).unwrap_err(); - assert!(blocked.contains("System Settings > Privacy & Security > Camera")); - assert!(blocked.contains("restricted")); - } - - #[test] - fn microphone_setup_requires_granted_access() { - for status in [OSPermissionStatus::Granted, OSPermissionStatus::NotNeeded] { - assert!(microphone_access_result(status).is_ok()); - } - assert!( - microphone_access_result(OSPermissionStatus::Empty) - .unwrap_err() - .contains("microphone control") - ); - assert!( - microphone_access_result(OSPermissionStatus::Denied) - .unwrap_err() - .contains("System Settings > Privacy & Security > Microphone") - ); - } - - #[test] - fn permission_status_permitted_matches_granted_states() { - assert!(OSPermissionStatus::Granted.permitted()); - assert!(OSPermissionStatus::NotNeeded.permitted()); - assert!(!OSPermissionStatus::Empty.permitted()); - assert!(!OSPermissionStatus::Denied.permitted()); - } - - #[cfg(target_os = "macos")] - #[test] - fn permission_settings_urls_match_expected_privacy_pages() { - assert_eq!( - macos_permission_settings_url(&OSPermission::ScreenRecording), - "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture" - ); - assert_eq!( - macos_permission_settings_url(&OSPermission::Accessibility), - "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - ); - assert_eq!( - macos_permission_settings_url(&OSPermission::Camera), - "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera" - ); - assert_eq!( - macos_permission_settings_url(&OSPermission::Microphone), - "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone" - ); - } - - #[cfg(target_os = "macos")] - #[tokio::test] - async fn permission_update_wait_returns_true_once_permission_is_observed() { - let mut checks = [false, false, true, true].into_iter(); - - let granted = - macos_wait_for_permission_update_with(|| checks.next().unwrap_or(true), || async {}) - .await; - - assert!(granted); - } - - #[cfg(target_os = "macos")] - #[tokio::test] - async fn permission_update_wait_returns_false_when_permission_never_changes() { - let mut checks = [false, false, false].into_iter(); - - let granted = - macos_wait_for_permission_update_with(|| checks.next().unwrap_or(false), || async {}) - .await; - - assert!(!granted); - } - - #[cfg(target_os = "macos")] - #[test] - fn main_thread_never_validates_inline() { - use std::cell::Cell; - - let spawned = Cell::new(false); - let validated_inline = Cell::new(false); - - let result = macos_screen_recording_available_with( - true, - false, - MACOS_SCK_OUTCOME_UNKNOWN, - true, - || spawned.set(true), - || { - validated_inline.set(true); - true - }, - ); - - assert!(result, "optimistic before the first strict validation"); - assert!(spawned.get(), "must kick a background validation"); - assert!( - !validated_inline.get(), - "the main thread must never run the blocking SCK validation" - ); - } - - #[cfg(target_os = "macos")] - #[test] - fn main_thread_reports_last_failed_strict_outcome() { - use std::cell::Cell; - - let spawned = Cell::new(false); - - let result = macos_screen_recording_available_with( - true, - false, - MACOS_SCK_OUTCOME_FAILED, - true, - || spawned.set(true), - || unreachable!("main thread must not validate inline"), - ); - - assert!(!result, "a strictly-observed failure must not be masked"); - assert!(spawned.get(), "still revalidates in the background"); - } - - #[cfg(target_os = "macos")] - #[test] - fn off_main_thread_keeps_the_strict_path() { - use std::cell::Cell; - - let spawned = Cell::new(false); - let validated_inline = Cell::new(false); - - let result = macos_screen_recording_available_with( - true, - false, - MACOS_SCK_OUTCOME_UNKNOWN, - false, - || spawned.set(true), - || { - validated_inline.set(true); - false - }, - ); - - assert!(!result); - assert!(validated_inline.get(), "off-main callers validate inline"); - assert!(!spawned.get()); - } - - #[cfg(target_os = "macos")] - #[test] - fn preflight_denial_and_cached_validation_short_circuit() { - let denied = macos_screen_recording_available_with( - false, - false, - MACOS_SCK_OUTCOME_UNKNOWN, - true, - || unreachable!("no work when preflight is denied"), - || unreachable!("no work when preflight is denied"), - ); - assert!(!denied); - - let cached = macos_screen_recording_available_with( - true, - true, - MACOS_SCK_OUTCOME_UNKNOWN, - true, - || unreachable!("no work once displays are validated"), - || unreachable!("no work once displays are validated"), - ); - assert!(cached); - } - - #[cfg(target_os = "macos")] - #[test] - fn sck_timeout_fits_inside_the_retry_backoff() { - assert!(MACOS_SCK_VALIDATION_TIMEOUT < MACOS_SCK_VALIDATION_RETRY_INTERVAL); - } - - #[cfg(target_os = "macos")] - #[test] - fn in_flight_latch_clears_even_when_validation_panics() { - MACOS_SCK_DISPLAYS_VALIDATION_IN_FLIGHT.store(true, Ordering::Release); - - let panicked = std::panic::catch_unwind(|| { - let _reset = MacosSckValidationInFlightReset; - panic!("validation blew up"); - }); - - assert!(panicked.is_err()); - assert!(!MACOS_SCK_DISPLAYS_VALIDATION_IN_FLIGHT.load(Ordering::Acquire)); - } - - #[cfg(target_os = "macos")] - #[test] - fn settings_fallback_only_applies_to_screen_and_accessibility() { - assert!(macos_permission_needs_settings_fallback( - &OSPermission::ScreenRecording - )); - assert!(macos_permission_needs_settings_fallback( - &OSPermission::Accessibility - )); - assert!(!macos_permission_needs_settings_fallback( - &OSPermission::Camera - )); - assert!(!macos_permission_needs_settings_fallback( - &OSPermission::Microphone - )); - } -} diff --git a/apps/desktop/src-tauri/src/picker-benchmark.js b/apps/desktop/src-tauri/src/picker-benchmark.js deleted file mode 100644 index d675f323c95..00000000000 --- a/apps/desktop/src-tauri/src/picker-benchmark.js +++ /dev/null @@ -1,28 +0,0 @@ -(() => { - let armed = true; - let frames = 0; - const invoke = window.__TAURI_INTERNALS__.invoke; - globalThis.__capArmPickerBenchmark = () => { - armed = true; - frames = 0; - }; - const tick = () => { - const button = Array.from( - document.querySelectorAll('[data-disabled="false"]'), - ).find((element) => element.textContent.includes("Start Recording")); - if (armed && button?.getBoundingClientRect().width > 0) { - frames += 1; - if (frames === 2) { - armed = false; - void invoke("plugin:event|emit", { - event: "cap-picker-benchmark-ready", - payload: { navigationMs: performance.now() }, - }); - } - } else { - frames = 0; - } - requestAnimationFrame(tick); - }; - requestAnimationFrame(tick); -})(); diff --git a/apps/desktop/src-tauri/src/picker_benchmark.rs b/apps/desktop/src-tauri/src/picker_benchmark.rs deleted file mode 100644 index bd2abbf6470..00000000000 --- a/apps/desktop/src-tauri/src/picker_benchmark.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::time::{Duration, Instant}; - -use tauri::{Listener, Manager}; -use tauri_specta::Event; - -use crate::{ - RequestSetTargetMode, recording_settings::RecordingTargetMode, - target_select_overlay::WindowFocusManager, -}; - -pub const SCRIPT: &str = include_str!("picker-benchmark.js"); - -pub fn enabled() -> bool { - std::env::var_os("CAP_PICKER_BENCHMARK_OUTPUT").is_some() -} - -pub fn run(app: tauri::AppHandle) { - let Some(output) = std::env::var_os("CAP_PICKER_BENCHMARK_OUTPUT") else { - return; - }; - tokio::spawn(async move { - let delay = std::env::var("CAP_PICKER_BENCHMARK_DELAY_MS") - .ok() - .and_then(|delay| delay.parse().ok()) - .unwrap_or(2000); - tokio::time::sleep(Duration::from_millis(delay)).await; - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); - let listener = app.listen_any("cap-picker-benchmark-ready", move |event| { - let _ = sender.send(event.payload().to_owned()); - }); - // Six alternating Display/Window cycles by default; the overlay - // soak (see CAP-CHANGES in vendor/tauri-nspanel) runs hundreds. - let sample_count = std::env::var("CAP_PICKER_BENCHMARK_SAMPLES") - .ok() - .and_then(|count| count.parse::().ok()) - .filter(|count| *count > 0) - .unwrap_or(6); - let mut samples = Vec::new(); - for sample in 0..sample_count { - let mode = if sample % 2 == 0 { - RecordingTargetMode::Display - } else { - RecordingTargetMode::Window - }; - for (label, window) in app.webview_windows() { - if label.starts_with("target-select-overlay") { - let _ = window.eval("globalThis.__capArmPickerBenchmark?.()"); - } - } - let started = Instant::now(); - let _ = RequestSetTargetMode { - target_mode: Some(mode), - display_id: None, - } - .emit(&app); - let result = tokio::time::timeout(Duration::from_secs(20), receiver.recv()).await; - samples.push(serde_json::json!({ - "sample": sample, - "mode": mode, - "elapsedMs": started.elapsed().as_secs_f64() * 1000.0, - "ready": matches!(result, Ok(Some(_))), - "frontend": result.ok().flatten().and_then(|payload| serde_json::from_str::(&payload).ok()), - })); - let _ = RequestSetTargetMode { - target_mode: None, - display_id: None, - } - .emit(&app); - tokio::time::sleep(Duration::from_millis(300)).await; - } - if let Err(error) = std::fs::write(&output, serde_json::json!(samples).to_string()) { - tracing::error!(%error, "Could not write picker benchmark results"); - } - - // Optional Escape phase: open the Display picker and confirm it is - // dismissed through the Escape path. `CAP_PICKER_BENCHMARK_ESCAPE=direct` - // calls the same dismissal the global-shortcut handler runs; - // `CAP_PICKER_BENCHMARK_ESCAPE=wait:` instead holds the picker open - // for that long while an external driver sends a real Escape keypress, - // and records whether the picker session ended on its own. - if let Ok(mode) = std::env::var("CAP_PICKER_BENCHMARK_ESCAPE") { - let hold_ms = mode - .strip_prefix("wait:") - .and_then(|value| value.parse::().ok()); - let mut escape_samples = Vec::new(); - for cycle in 0..3 { - for (label, window) in app.webview_windows() { - if label.starts_with("target-select-overlay") { - let _ = window.eval("globalThis.__capArmPickerBenchmark?.()"); - } - } - let _ = RequestSetTargetMode { - target_mode: Some(RecordingTargetMode::Display), - display_id: None, - } - .emit(&app); - let ready = tokio::time::timeout(Duration::from_secs(20), receiver.recv()) - .await - .is_ok(); - let started = Instant::now(); - let picker_open = || app.state::().picker_session().is_some(); - let dismissed = if let Some(hold_ms) = hold_ms { - // Tell the external driver the picker is up so it sends - // exactly one Escape per cycle. - let marker = std::path::PathBuf::from(&output).with_extension("escape.ready"); - let _ = std::fs::write(&marker, b"ready"); - let deadline = Instant::now() + Duration::from_millis(hold_ms); - let mut dismissed = false; - while Instant::now() < deadline { - if !picker_open() { - dismissed = true; - break; - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - let _ = std::fs::remove_file(&marker); - dismissed - } else { - tokio::time::sleep(Duration::from_millis(400)).await; - crate::target_select_overlay::dismiss_picker_from_escape(&app); - tokio::time::sleep(Duration::from_millis(200)).await; - !picker_open() - }; - escape_samples.push(serde_json::json!({ - "cycle": cycle, - "ready": ready, - "dismissed": dismissed, - "dismissMs": started.elapsed().as_secs_f64() * 1000.0, - })); - if !dismissed { - let _ = RequestSetTargetMode { - target_mode: None, - display_id: None, - } - .emit(&app); - } - tokio::time::sleep(Duration::from_millis(600)).await; - } - let escape_output = std::path::PathBuf::from(&output).with_extension("escape.json"); - if let Err(error) = - std::fs::write(escape_output, serde_json::json!(escape_samples).to_string()) - { - tracing::error!(%error, "Could not write escape benchmark results"); - } - } - app.unlisten(listener); - - // Optional second phase: close and reopen the camera window, which is - // destroyed and recreated under the same label. This exercises the - // NSPanel store's stale-entry replacement and its Destroyed cleanup - // (the paths that free a converted window for real), so an ownership - // bug in the vendored tauri-nspanel surfaces here instead of in a - // user's recording session. Results land next to the picker output. - let camera_cycles = std::env::var("CAP_PICKER_BENCHMARK_CAMERA_CYCLES") - .ok() - .and_then(|count| count.parse::().ok()) - .unwrap_or(0); - if camera_cycles > 0 { - let mut camera_samples = Vec::new(); - for cycle in 0..camera_cycles { - let started = Instant::now(); - let removed = crate::windows::cleanup_camera_window(&app, None, true, true).await; - let close_ms = started.elapsed().as_secs_f64() * 1000.0; - tokio::time::sleep(Duration::from_millis(400)).await; - let started = Instant::now(); - let shown = (crate::windows::ShowCapWindow::Camera { centered: true }) - .show(&app) - .await - .is_ok(); - camera_samples.push(serde_json::json!({ - "cycle": cycle, - "removed": removed, - "closeMs": close_ms, - "shown": shown, - "showMs": started.elapsed().as_secs_f64() * 1000.0, - })); - tokio::time::sleep(Duration::from_millis(900)).await; - } - let camera_output = std::path::PathBuf::from(&output).with_extension("camera.json"); - if let Err(error) = - std::fs::write(camera_output, serde_json::json!(camera_samples).to_string()) - { - tracing::error!(%error, "Could not write camera benchmark results"); - } - } - tracing::info!("Picker benchmark complete; requesting app exit"); - app.exit(0); - }); -} diff --git a/apps/desktop/src-tauri/src/platform/macos/delegates.rs b/apps/desktop/src-tauri/src/platform/macos/delegates.rs deleted file mode 100644 index bf0bfa1b7a2..00000000000 --- a/apps/desktop/src-tauri/src/platform/macos/delegates.rs +++ /dev/null @@ -1,509 +0,0 @@ -// TODO(Ilya): Re-write all macos code to use `objc2` crates n -/// -/// Credit to @haasal, @charrondev, Hoppscotch app, Electron, Zed Editor -/// -/// https://github.com/haasal -/// https://gist.github.com/charrondev -/// https://github.com/hoppscotch/hoppscotch -/// https://github.com/clearlysid/tauri-plugin-decorum/ -/// (Issue) https://github.com/tauri-apps/tauri/issues/4789 -/// (Gist) https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9 -/// (Hoppscotch) https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs -/// (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 {} -unsafe impl Sync for UnsafeWindowHandle {} - -#[derive(Debug)] -struct WindowState { - window: Window, - controls_inset: LogicalPosition, -} - -// TODO: Respect RTL display language -// TODO: Update Height, consider supporting the scenario where the buttons are hidden by the system due to screen sharing of the window -// https://developer.apple.com/documentation/appkit/nsapplication/1428556-userinterfacelayoutdirection?language=objc -pub fn position_window_controls( - ns_window_handle: UnsafeWindowHandle, - inset: &LogicalPosition, -) { - use cocoa::{ - appkit::{NSView, NSWindow, NSWindowButton, NSWindowStyleMask}, - base::id, - foundation::NSRect, - }; - - let ns_window = ns_window_handle.0 as id; - unsafe { - // In native fullscreen the standard window buttons are hosted in a - // system-managed titlebar overlay; resizing their superviews there - // makes AppKit throw an NSException, which aborts the process once it - // unwinds into Rust. The system positions them itself in fullscreen. - if ns_window - .styleMask() - .contains(NSWindowStyleMask::NSFullScreenWindowMask) - { - return; - } - - let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); - let minimize = ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); - let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); - if close.is_null() || minimize.is_null() || zoom.is_null() { - return; - } - - let title_bar_container_view = close.superview().superview(); - if title_bar_container_view.is_null() { - return; - } - - let close_rect: NSRect = msg_send![close, frame]; - let button_height = close_rect.size.height; - - let title_bar_frame_height = button_height + inset.y; - let mut title_bar_rect = NSView::frame(title_bar_container_view); - title_bar_rect.size.height = title_bar_frame_height; - title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; - let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect]; - - let window_buttons = vec![close, minimize, zoom]; - let space_between = NSView::frame(minimize).origin.x - NSView::frame(close).origin.x; - let vertical_offset = 4.0; // Adjust this value to push buttons down - - for (i, button) in window_buttons.into_iter().enumerate() { - let mut rect: NSRect = NSView::frame(button); - rect.origin.x = inset.x + (i as f64 * space_between); - rect.origin.y = ((title_bar_frame_height - button_height) / 2.0) - vertical_offset; - button.setFrameOrigin(rect.origin); - } - } -} - -pub fn setup(window: Window, controls_inset: LogicalPosition) { - use cocoa::appkit::NSWindow; - use cocoa::base::{BOOL, id}; - use cocoa::foundation::NSUInteger; - use objc::declare::ClassDecl; - use objc::runtime::{Class, Object, Sel}; - use std::ffi::c_void; - - let Ok(ns_win) = window.ns_window() else { - tracing::warn!("Failed to get window handle for delegate setup"); - return; - }; - - // Do the initial positioning - position_window_controls(UnsafeWindowHandle(ns_win), &controls_inset); - - // Ensure they stay in place while resizing the window. - fn with_window_state) -> T, T>( - this: &Object, - func: F, - ) { - let x: *mut c_void = unsafe { *this.get_ivar("app_box") }; - // `app_box` is nulled out in `windowWillClose:`; ignore any late - // callbacks that arrive after that instead of dereferencing null. - if x.is_null() { - return; - } - let ptr = unsafe { &mut *(x as *mut WindowState) }; - func(ptr); - } - - fn suppress_delegate_panic(selector: &'static str, fallback: T, operation: F) -> T - where - F: FnOnce() -> T, - { - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation)) { - Ok(value) => value, - Err(_) => { - tracing::error!(selector, "Suppressed panic in macOS window delegate"); - fallback - } - } - } - - unsafe { - let ns_win_id = ns_win as id; - let current_delegate: id = ns_win_id.delegate(); - - extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL { - suppress_delegate_panic("windowShouldClose:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, windowShouldClose: sender] - }) - } - extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) { - let window = unsafe { objc::rc::StrongPtr::retain(*this.get_ivar("window")) }; - let super_del: id = unsafe { *this.get_ivar("super_delegate") }; - - // Forward to the previous delegate first, but don't let a panic there - // skip the cleanup below (which would bring back the leak). - suppress_delegate_panic("windowWillClose:", (), || unsafe { - let _: () = msg_send![super_del, windowWillClose: notification]; - }); - - suppress_delegate_panic("windowWillClose:cleanup", (), || unsafe { - // Tao clears the delegate before Destroyed; preserve that even if forwarding panics. - let _: () = msg_send![*window, setDelegate: cocoa::base::nil]; - - // Drop the boxed `WindowState` (and the `Window` handle it holds) - // that was leaked via `Box::into_raw` when this delegate was created. - let app_box: *mut c_void = *this.get_ivar("app_box"); - if !app_box.is_null() { - let this_mut = this as *const Object as *mut Object; - (*this_mut).set_ivar("app_box", std::ptr::null_mut::()); - drop(Box::from_raw(app_box as *mut WindowState)); - } - - // NSWindow does not retain its delegate, so the reference taken when - // this delegate was created (`new`) is the only owning one. Release - // it now that the window is closing. - let this_id = this as *const Object as id; - let _: () = msg_send![this_id, release]; - }); - } - extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidResize:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Ok(window_handle) = state.window.ns_window() { - position_window_controls( - UnsafeWindowHandle(window_handle), - &state.controls_inset, - ); - } else { - tracing::warn!("Failed to get handle to NSWindow during resize"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidResize: notification]; - }); - } - extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidMove:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidMove: notification]; - }); - } - extern "C" fn on_window_did_change_backing_properties( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowDidChangeBackingProperties:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification]; - }); - } - extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidBecomeKey:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidBecomeKey: notification]; - }); - } - extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidResignKey:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidResignKey: notification]; - }); - } - extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL { - suppress_delegate_panic("draggingEntered:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, draggingEntered: notification] - }) - } - extern "C" fn on_prepare_for_drag_operation( - this: &Object, - _cmd: Sel, - notification: id, - ) -> BOOL { - suppress_delegate_panic("prepareForDragOperation:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, prepareForDragOperation: notification] - }) - } - extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL { - suppress_delegate_panic("performDragOperation:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, performDragOperation: sender] - }) - } - extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("concludeDragOperation:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, concludeDragOperation: notification]; - }); - } - extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("draggingExited:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, draggingExited: notification]; - }); - } - extern "C" fn on_window_will_use_full_screen_presentation_options( - this: &Object, - _cmd: Sel, - window: id, - proposed_options: NSUInteger, - ) -> NSUInteger { - suppress_delegate_panic( - "window:willUseFullScreenPresentationOptions:", - proposed_options, - || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options] - }, - ) - } - extern "C" fn on_window_did_enter_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowDidEnterFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("did-enter-fullscreen", ()) { - tracing::warn!("Failed to emit did-enter-fullscreen: {err}"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidEnterFullScreen: notification]; - }); - } - extern "C" fn on_window_will_enter_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowWillEnterFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("will-enter-fullscreen", ()) { - tracing::warn!("Failed to emit will-enter-fullscreen: {err}"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowWillEnterFullScreen: notification]; - }); - } - extern "C" fn on_window_did_exit_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowDidExitFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("did-exit-fullscreen", ()) { - tracing::warn!("Failed to emit did-exit-fullscreen: {err}"); - } - - if let Ok(window_handle) = state.window.ns_window() { - position_window_controls( - UnsafeWindowHandle(window_handle), - &state.controls_inset, - ); - } else { - tracing::warn!("Failed to get handle to NSWindow after exiting fullscreen"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidExitFullScreen: notification]; - }); - } - extern "C" fn on_window_will_exit_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowWillExitFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("will-exit-fullscreen", ()) { - tracing::warn!("Failed to emit will-exit-fullscreen: {err}"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowWillExitFullScreen: notification]; - }); - } - extern "C" fn on_window_did_fail_to_enter_full_screen( - this: &Object, - _cmd: Sel, - window: id, - ) { - suppress_delegate_panic("windowDidFailToEnterFullScreen:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window]; - }); - } - extern "C" fn on_effective_appearance_did_change( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("effectiveAppearanceDidChange:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification]; - }); - } - extern "C" fn on_effective_appearance_did_changed_on_main_thread( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic( - "effectiveAppearanceDidChangedOnMainThread:", - (), - || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![ - super_del, - effectiveAppearanceDidChangedOnMainThread: notification - ]; - }, - ); - } - - // Register the delegate class once and reuse it for every window. Previously a brand - // new class was registered (with a randomized name) on every call to `setup`, which - // permanently leaked Objective-C class metadata for the lifetime of the process. - // - // 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 - // 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 { - static CLASS: std::sync::OnceLock<&'static Class> = std::sync::OnceLock::new(); - CLASS.get_or_init(|| { - if let Some(existing) = Class::get("CapWindowDelegate") { - return existing; - } - - let mut decl = match ClassDecl::new("CapWindowDelegate", class!(NSObject)) { - Some(decl) => decl, - None => { - return Class::get("CapWindowDelegate") - .expect("CapWindowDelegate should exist if already registered"); - } - }; - - decl.add_ivar::("window"); - decl.add_ivar::<*mut c_void>("app_box"); - decl.add_ivar::("toolbar"); - decl.add_ivar::("super_delegate"); - - unsafe { - decl.add_method( - sel!(windowShouldClose:), - on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(windowWillClose:), - on_window_will_close:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResize:), - on_window_did_resize:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidMove:), - on_window_did_move as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidChangeBackingProperties:), - on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidBecomeKey:), - on_window_did_become_key as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResignKey:), - on_window_did_resign_key as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(draggingEntered:), - on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(prepareForDragOperation:), - on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(performDragOperation:), - on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(concludeDragOperation:), - on_conclude_drag_operation as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(draggingExited:), - on_dragging_exited as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(window:willUseFullScreenPresentationOptions:), - on_window_will_use_full_screen_presentation_options - as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, - ); - decl.add_method( - sel!(windowDidEnterFullScreen:), - on_window_did_enter_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillEnterFullScreen:), - on_window_will_enter_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidExitFullScreen:), - on_window_did_exit_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillExitFullScreen:), - on_window_will_exit_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidFailToEnterFullScreen:), - on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(effectiveAppearanceDidChange:), - on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(effectiveAppearanceDidChangedOnMainThread:), - on_effective_appearance_did_changed_on_main_thread - as extern "C" fn(&Object, Sel, id), - ); - } - - decl.register() - }) - } - - let app_state = WindowState { - window, - controls_inset, - }; - let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; - - let delegate_class = get_or_register_delegate_class::(); - let delegate: id = msg_send![delegate_class, new]; - (*delegate).set_ivar("window", ns_win_id); - (*delegate).set_ivar("app_box", app_box); - (*delegate).set_ivar("toolbar", cocoa::base::nil); - (*delegate).set_ivar("super_delegate", current_delegate); - - ns_win_id.setDelegate_(delegate) - } -} 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 e4072177d7c..00000000000 --- a/apps/desktop/src-tauri/src/platform/macos/mod.rs +++ /dev/null @@ -1,788 +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, - ) -} - -fn constrain_frame_to_visible_area( - frame: objc2_foundation::NSRect, - screen_frame: objc2_foundation::NSRect, - visible_frame: objc2_foundation::NSRect, - safe_area_top_inset: f64, -) -> Option { - use objc2_foundation::{NSPoint, NSRect, NSSize}; - - let minimum_x = visible_frame.origin.x; - let minimum_y = visible_frame.origin.y; - let maximum_x = minimum_x + visible_frame.size.width; - let maximum_y = (minimum_y + visible_frame.size.height) - .min(screen_frame.origin.y + screen_frame.size.height - safe_area_top_inset.max(0.0)); - if ![ - frame.origin.x, - frame.origin.y, - frame.size.width, - frame.size.height, - minimum_x, - minimum_y, - maximum_x, - maximum_y, - ] - .into_iter() - .all(f64::is_finite) - || frame.size.width <= 0.0 - || frame.size.height <= 0.0 - || maximum_x <= minimum_x - || maximum_y <= minimum_y - { - return None; - } - - let width = frame.size.width.min(maximum_x - minimum_x); - let height = frame.size.height.min(maximum_y - minimum_y); - let constrained = NSRect::new( - NSPoint::new( - frame.origin.x.clamp(minimum_x, maximum_x - width), - frame.origin.y.clamp(minimum_y, maximum_y - height), - ), - NSSize::new(width, height), - ); - (constrained != frame).then_some(constrained) -} - -pub fn constrain_main_window_to_visible_frame(window: &tauri::Window) -> Result<(), String> { - use objc2::{MainThreadMarker, runtime::NSObjectProtocol, sel}; - use objc2_app_kit::{NSScreen, NSWindow}; - - let main_thread = - MainThreadMarker::new().ok_or("Main window bounds must be checked on the main thread")?; - let ns_window = window.ns_window().map_err(|error| error.to_string())? as *const NSWindow; - let ns_window = unsafe { ns_window.as_ref() }.ok_or("Main window is unavailable")?; - let screen = ns_window - .screen() - .or_else(|| NSScreen::mainScreen(main_thread)) - .ok_or("Main window screen is unavailable")?; - let safe_area_top_inset = if screen.respondsToSelector(sel!(safeAreaInsets)) { - unsafe { screen.safeAreaInsets().top } - } else { - 0.0 - }; - if let Some(frame) = constrain_frame_to_visible_area( - ns_window.frame(), - screen.frame(), - screen.visibleFrame(), - safe_area_top_inset, - ) { - ns_window.setFrame_display(frame, true); - } - Ok(()) -} - -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_frame_to_visible_area, constrain_position_to_visible_top}; - use objc2_foundation::{NSPoint, NSRect, NSSize}; - use tauri::PhysicalPosition; - - fn rect(x: f64, y: f64, width: f64, height: f64) -> NSRect { - NSRect::new(NSPoint::new(x, y), NSSize::new(width, height)) - } - - #[test] - fn fits_the_actual_resized_frame_inside_the_work_area() { - let screen = rect(0.0, 0.0, 1440.0, 900.0); - let visible = rect(0.0, 24.0, 1440.0, 851.0); - let constrained = constrain_frame_to_visible_area( - rect(1300.0, 800.0, 330.0, 395.0), - screen, - visible, - 0.0, - ) - .unwrap(); - - assert_eq!(constrained, rect(1110.0, 480.0, 330.0, 395.0)); - assert_eq!( - constrain_frame_to_visible_area(constrained, screen, visible, 0.0), - None - ); - } - - #[test] - fn fits_expanded_frames_below_a_notch_with_a_hidden_menu_bar() { - let screen = rect(0.0, 0.0, 1440.0, 900.0); - assert_eq!( - constrain_frame_to_visible_area(rect(100.0, 650.0, 480.0, 700.0), screen, screen, 37.0,), - Some(rect(100.0, 163.0, 480.0, 700.0)) - ); - } - - #[test] - fn keeps_native_coordinates_on_a_display_above_and_left_of_primary() { - assert_eq!( - constrain_frame_to_visible_area( - rect(-2000.0, 1800.0, 480.0, 700.0), - rect(-1920.0, 900.0, 1920.0, 1080.0), - rect(-1880.0, 900.0, 1880.0, 1050.0), - 37.0, - ), - Some(rect(-1880.0, 1243.0, 480.0, 700.0)) - ); - } - - #[test] - fn fits_an_oversized_restored_frame_without_negative_clamp_ranges() { - let visible = rect(40.0, 24.0, 1400.0, 851.0); - assert_eq!( - constrain_frame_to_visible_area( - rect(-100.0, -100.0, 2000.0, 1200.0), - rect(0.0, 0.0, 1440.0, 900.0), - visible, - 0.0, - ), - Some(visible) - ); - } - - #[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/platform/macos/sc_shareable_content.rs b/apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs deleted file mode 100644 index 8bc5eebe713..00000000000 --- a/apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs +++ /dev/null @@ -1,198 +0,0 @@ -use cidre::{arc, ns, sc}; -use std::sync::{Arc, OnceLock, RwLock}; -use tokio::sync::{Mutex, Notify}; -use tracing::{error, trace}; - -#[derive(Default)] -struct CacheState { - cache: RwLock>, - warmup: Mutex>, -} - -type WarmupResult = Result<(), arc::R>; - -#[derive(Clone)] -struct WarmupTask { - notify: Arc, - result: Arc>>, -} - -static STATE: OnceLock = OnceLock::new(); - -fn state() -> &'static CacheState { - STATE.get_or_init(CacheState::default) -} - -pub async fn prewarm_shareable_content() -> Result<(), arc::R> { - prewarm_shareable_content_inner(false).await -} - -pub async fn refresh_shareable_content() -> Result<(), arc::R> { - prewarm_shareable_content_inner(true).await -} - -async fn prewarm_shareable_content_inner(force_refresh: bool) -> Result<(), arc::R> { - if force_refresh { - state().cache.write().unwrap().take(); - } else if state().cache.read().unwrap().is_some() { - trace!("ScreenCaptureKit shareable content already warmed"); - return Ok(()); - } - - let warmup = { - let mut guard = state().warmup.lock().await; - if let Some(task) = guard.clone() { - trace!("Awaiting in-flight ScreenCaptureKit warmup"); - task - } else { - let task = WarmupTask { - notify: Arc::new(Notify::new()), - result: Arc::new(Mutex::new(None)), - }; - *guard = Some(task.clone()); - tokio::spawn(run_warmup(task.clone())); - task - } - }; - - warmup.notify.notified().await; - warmup.result.lock().await.clone().unwrap_or_else(|| { - error!("ScreenCaptureKit warmup task finished without a result"); - Ok(()) - }) -} - -pub async fn get_shareable_content() --> Result>, arc::R> { - if let Some(content) = state() - .cache - .read() - .unwrap() - .as_ref() - .map(|v| v.content.retained()) - { - return Ok(Some(content)); - } - - prewarm_shareable_content().await?; - - let content = state().cache.read().unwrap(); - Ok(content.as_ref().map(|v| v.content.retained())) -} - -async fn run_warmup(task: WarmupTask) { - let result = async { - let content = sc::ShareableContent::current().await?; - let cache = ShareableContentCache::new(content); - - let mut guard = state().cache.write().unwrap(); - *guard = Some(cache); - - Ok::<(), arc::R>(()) - } - .await; - - { - let mut res_guard = task.result.lock().await; - *res_guard = Some(result); - } - - task.notify.notify_waiters(); - - let mut guard = state().warmup.lock().await; - if let Some(current) = guard.as_ref() - && Arc::ptr_eq(¤t.notify, &task.notify) - { - *guard = None; - } -} - -#[derive(Debug)] -struct ShareableContentCache { - #[allow(dead_code)] - content: arc::R, -} - -unsafe impl Send for ShareableContentCache {} -unsafe impl Sync for ShareableContentCache {} - -impl ShareableContentCache { - fn new(content: arc::R) -> Self { - Self { content } - } -} - -pub(crate) struct ScreenCapturePrewarmer { - state: Mutex, -} - -impl Default for ScreenCapturePrewarmer { - fn default() -> Self { - Self { - state: Mutex::new(PrewarmState::Idle), - } - } -} - -impl ScreenCapturePrewarmer { - pub async fn request(&self, force: bool) { - let should_start = { - let mut state = self.state.lock().await; - - if force { - *state = PrewarmState::Idle; - } - - match *state { - PrewarmState::Idle => { - *state = PrewarmState::Warming; - true - } - PrewarmState::Warming => { - trace!("ScreenCaptureKit prewarm already in progress"); - false - } - PrewarmState::Warmed => { - if force { - *state = PrewarmState::Warming; - true - } else { - trace!("ScreenCaptureKit cache already warmed"); - false - } - } - } - }; - - if !should_start { - return; - } - - let warm_start = std::time::Instant::now(); - let result = if force { - crate::platform::refresh_shareable_content().await - } else { - crate::platform::prewarm_shareable_content().await - }; - - let mut state = self.state.lock().await; - match result { - Ok(()) => { - let elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0; - *state = PrewarmState::Warmed; - trace!(elapsed_ms, "ScreenCaptureKit cache warmed"); - } - Err(error) => { - *state = PrewarmState::Idle; - tracing::warn!(error = %error, "ScreenCaptureKit prewarm failed"); - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum PrewarmState { - Idle, - Warming, - Warmed, -} diff --git a/apps/desktop/src-tauri/src/platform/mod.rs b/apps/desktop/src-tauri/src/platform/mod.rs deleted file mode 100644 index e51d3e709a3..00000000000 --- a/apps/desktop/src-tauri/src/platform/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -use serde::{Deserialize, Serialize}; -use specta::Type; -#[cfg(target_os = "windows")] -pub mod win; - -#[cfg(target_os = "macos")] -pub mod macos; - -#[cfg(target_os = "macos")] -pub use macos::*; -use tracing::instrument; - -#[derive(Debug, Serialize, Deserialize, Type, Default)] -#[serde(rename_all = "camelCase")] -#[repr(isize)] -pub enum HapticPattern { - Alignment = 0, - LevelChange = 1, - #[default] - Generic = 2, -} - -#[derive(Debug, Serialize, Deserialize, Type, Default)] -#[serde(rename_all = "camelCase")] -#[repr(usize)] -pub enum HapticPerformanceTime { - Default = 0, - #[default] - Now = 1, - DrawCompleted = 2, -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub fn perform_haptic_feedback( - _pattern: Option, - _time: Option, -) -> Result<(), String> { - #[cfg(target_os = "macos")] - unsafe { - use objc2_app_kit::{ - NSHapticFeedbackManager, NSHapticFeedbackPattern, NSHapticFeedbackPerformanceTime, - NSHapticFeedbackPerformer, - }; - - NSHapticFeedbackManager::defaultPerformer().performFeedbackPattern_performanceTime( - NSHapticFeedbackPattern(_pattern.unwrap_or_default() as isize), - NSHapticFeedbackPerformanceTime(_time.unwrap_or_default() as usize), - ); - Ok(()) - } - - #[cfg(not(target_os = "macos"))] - Err("Haptics are only supported on macOS.".into()) -} - -/// 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] -#[specta::specta] -#[instrument] -pub fn is_system_audio_capture_supported() -> bool { - #[cfg(target_os = "macos")] - { - scap_screencapturekit::is_system_audio_supported() - } - - #[cfg(not(target_os = "macos"))] - { - // On Windows/Linux, we assume system audio capture is available - // This can be refined later based on platform-specific requirements - true - } -} diff --git a/apps/desktop/src-tauri/src/platform/win.rs b/apps/desktop/src-tauri/src/platform/win.rs deleted file mode 100644 index e5c47140d7f..00000000000 --- a/apps/desktop/src-tauri/src/platform/win.rs +++ /dev/null @@ -1,258 +0,0 @@ -use winreg::RegKey; -use winreg::enums::HKEY_LOCAL_MACHINE; - -const ENV_OVERRIDE: &str = "CAP_WINDOW_CAPTURE_EXCLUSION"; - -// Shadow reports S:102 for protected windows; EC2/DCV still displays excluded windows. -const SMBIOS_MARKERS: &[&str] = &["shadow"]; - -const VIRTUAL_DISPLAY_MARKERS: &[&str] = &[ - "parsec", - "spacedesk", - "iddsample", - "virtual display", - "usbmmidd", - "amyuni", - "shadow", -]; - -pub fn capture_streamed_display_reason() -> Option { - streamed_display_reason_with( - std::env::var(ENV_OVERRIDE).ok().as_deref(), - remote_session_active, - streamed_computer_marker, - virtual_display_adapter, - ) -} - -fn exclusion_override(value: &str) -> Option { - match value.trim().to_ascii_lowercase().as_str() { - "on" | "always" | "1" => Some(true), - "off" | "never" | "0" => Some(false), - _ => None, - } -} - -fn streamed_display_reason_with( - override_value: Option<&str>, - remote_session: impl FnOnce() -> bool, - streamed_computer: impl FnOnce() -> Option, - virtual_display: impl FnOnce() -> Option, -) -> Option { - match override_value.and_then(exclusion_override) { - Some(true) => return None, - Some(false) => return Some(format!("{ENV_OVERRIDE} env override")), - None => {} - } - if remote_session() { - return Some("remote desktop session (SM_REMOTESESSION)".to_string()); - } - if let Some(marker) = streamed_computer() { - return Some(format!("streamed computer SMBIOS ({marker})")); - } - virtual_display().map(|device| format!("virtual display adapter ({device})")) -} - -fn remote_session_active() -> bool { - use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_REMOTESESSION}; - unsafe { GetSystemMetrics(SM_REMOTESESSION) != 0 } -} - -fn streamed_computer_marker() -> Option { - let key = RegKey::predef(HKEY_LOCAL_MACHINE) - .open_subkey("HARDWARE\\DESCRIPTION\\System\\BIOS") - .ok()?; - - for value in [ - "SystemManufacturer", - "SystemProductName", - "SystemFamily", - "BIOSVendor", - ] { - let Ok(text) = key.get_value::(value) else { - continue; - }; - if let Some(marker) = find_marker(&text, SMBIOS_MARKERS) { - return Some(format!("{value}=\"{text}\" matched \"{marker}\"")); - } - } - - None -} - -fn virtual_display_adapter() -> Option { - use windows::Win32::Graphics::Gdi::{ - DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, DISPLAY_DEVICEW, EnumDisplayDevicesW, - }; - use windows::core::PCWSTR; - - let mut index = 0u32; - loop { - let mut device = DISPLAY_DEVICEW { - cb: std::mem::size_of::() as u32, - ..Default::default() - }; - if !unsafe { EnumDisplayDevicesW(PCWSTR::null(), index, &mut device, 0) }.as_bool() { - return None; - } - index += 1; - - if !device - .StateFlags - .contains(DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) - { - continue; - } - - let name = String::from_utf16_lossy(&device.DeviceString); - let name = name.trim_matches(char::from(0)); - if let Some(marker) = find_marker(name, VIRTUAL_DISPLAY_MARKERS) { - return Some(format!("\"{name}\" matched \"{marker}\"")); - } - } -} - -fn find_marker(text: &str, markers: &[&'static str]) -> Option<&'static str> { - let lower = text.to_lowercase(); - markers.iter().copied().find(|m| lower.contains(m)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn overrides_preserve_auto_and_explicit_choices() { - for value in ["on", " ALWAYS ", "1"] { - assert_eq!(exclusion_override(value), Some(true)); - } - for value in ["off", " NEVER ", "0"] { - assert_eq!(exclusion_override(value), Some(false)); - } - for value in ["", "auto", "unknown"] { - assert_eq!(exclusion_override(value), None); - } - } - - #[test] - fn explicit_override_does_not_probe_the_environment() { - for (value, expected) in [ - ("on", None), - ("off", Some(format!("{ENV_OVERRIDE} env override"))), - ] { - assert_eq!( - streamed_display_reason_with( - Some(value), - || panic!("override must skip remote-session detection"), - || panic!("override must skip SMBIOS detection"), - || panic!("override must skip display detection"), - ), - expected - ); - } - } - - #[test] - fn remote_session_keeps_its_compatibility_exception() { - assert_eq!( - streamed_display_reason_with( - None, - || true, - || panic!("remote session must skip SMBIOS detection"), - || panic!("remote session must skip display detection"), - ), - Some("remote desktop session (SM_REMOTESESSION)".to_string()) - ); - } - - #[test] - fn shadow_keeps_its_compatibility_exception() { - for computer in ["Shadow", "SHADOW COMPUTER"] { - assert_eq!( - streamed_display_reason_with( - None, - || false, - || find_marker(computer, SMBIOS_MARKERS).map(str::to_string), - || panic!("Shadow must skip display detection"), - ), - Some("streamed computer SMBIOS (shadow)".to_string()) - ); - } - } - - #[test] - fn existing_streamed_adapters_keep_their_compatibility_exception() { - for adapter in [ - "Parsec Virtual Display Adapter", - "spacedesk", - "IddSampleDriver", - "Virtual Display", - "usbmmidd", - "Amyuni", - "Shadow", - ] { - assert!( - streamed_display_reason_with( - None, - || false, - || None, - || find_marker(adapter, VIRTUAL_DISPLAY_MARKERS).map(str::to_string), - ) - .is_some(), - "{adapter}" - ); - } - } - - #[test] - fn virtual_machine_hardware_keeps_capture_exclusion() { - for computer in [ - "Amazon EC2", - "QEMU Standard PC (Q35 + ICH9, 2009)", - "KVM", - "VMware", - "VirtualBox", - "innotek", - "Xen", - "Bochs", - "Parallels", - "Microsoft Corporation Virtual Machine", - "HVM domU", - "Google Compute Engine", - "OpenStack", - ] { - assert_eq!( - streamed_display_reason_with( - None, - || false, - || find_marker(computer, SMBIOS_MARKERS).map(str::to_string), - || None, - ), - None, - "{computer}" - ); - } - } - - #[test] - fn physical_hardware_keeps_capture_exclusion() { - for name in [ - "Dell Inc.", - "ASUSTeK COMPUTER INC.", - "NVIDIA GeForce RTX 3080", - "AMD Radeon RX 7900 XTX", - "Intel(R) UHD Graphics 770", - "LENOVO", - "Micro-Star International Co., Ltd.", - ] { - assert_eq!(find_marker(name, SMBIOS_MARKERS), None, "{name}"); - assert_eq!(find_marker(name, VIRTUAL_DISPLAY_MARKERS), None, "{name}"); - } - for override_value in [None, Some("auto"), Some("unknown")] { - assert_eq!( - streamed_display_reason_with(override_value, || false, || None, || None), - None - ); - } - } -} diff --git a/apps/desktop/src-tauri/src/power_observer.rs b/apps/desktop/src-tauri/src/power_observer.rs deleted file mode 100644 index 4bf8e0b7fe4..00000000000 --- a/apps/desktop/src-tauri/src/power_observer.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use tauri::AppHandle; -#[cfg(any(target_os = "macos", target_os = "windows"))] -use tracing::info; - -static SYSTEM_ASLEEP: AtomicBool = AtomicBool::new(false); - -pub fn is_system_asleep() -> bool { - SYSTEM_ASLEEP.load(Ordering::Acquire) -} - -#[cfg(any(target_os = "macos", target_os = "windows"))] -fn mark_sleeping() { - SYSTEM_ASLEEP.store(true, Ordering::Release); -} - -#[cfg(any(target_os = "macos", target_os = "windows"))] -fn mark_awake() { - SYSTEM_ASLEEP.store(false, Ordering::Release); -} - -#[cfg(any(target_os = "macos", target_os = "windows"))] -pub fn on_system_will_sleep(_app: &AppHandle) { - mark_sleeping(); - info!("System going to sleep"); -} - -#[cfg(any(target_os = "macos", target_os = "windows"))] -pub fn on_system_did_wake(app: &AppHandle) { - mark_awake(); - info!("System woke from sleep; scheduling recovery refresh"); - crate::schedule_resume_recovery(app.clone()); -} - -pub fn install(app: &AppHandle) { - #[cfg(target_os = "macos")] - macos::install(app); - - #[cfg(target_os = "windows")] - windows::install(app); - - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - { - let _ = app; - } -} - -pub fn uninstall(app: &AppHandle) { - #[cfg(target_os = "macos")] - macos::uninstall(app); - - #[cfg(target_os = "windows")] - windows::uninstall(app); - - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - { - let _ = app; - } -} - -#[cfg(target_os = "macos")] -mod macos { - use super::{on_system_did_wake, on_system_will_sleep}; - use objc2::{ - AnyThread, DeclaredClass, define_class, msg_send, - rc::{Retained, autoreleasepool}, - runtime::{AnyObject, NSObject}, - }; - use objc2_app_kit::NSWorkspace; - use objc2_foundation::{NSNotification, ns_string}; - use std::sync::{Mutex, OnceLock}; - use tauri::AppHandle; - use tracing::warn; - - struct ObserverIvars { - app: AppHandle, - } - - define_class!( - #[unsafe(super(NSObject))] - #[name = "CapPowerObserver"] - #[ivars = ObserverIvars] - struct CapPowerObserver; - - impl CapPowerObserver { - #[unsafe(method(handleWillSleep:))] - fn handle_will_sleep(&self, _notification: &NSNotification) { - on_system_will_sleep(&self.ivars().app); - } - - #[unsafe(method(handleDidWake:))] - fn handle_did_wake(&self, _notification: &NSNotification) { - on_system_did_wake(&self.ivars().app); - } - - #[unsafe(method(handleScreensDidSleep:))] - fn handle_screens_did_sleep(&self, _notification: &NSNotification) { - on_system_will_sleep(&self.ivars().app); - } - - #[unsafe(method(handleScreensDidWake:))] - fn handle_screens_did_wake(&self, _notification: &NSNotification) { - on_system_did_wake(&self.ivars().app); - } - } - ); - - impl CapPowerObserver { - fn new(app: AppHandle) -> Retained { - let this = Self::alloc().set_ivars(ObserverIvars { app }); - unsafe { msg_send![super(this), init] } - } - } - - static OBSERVER: OnceLock>>> = OnceLock::new(); - - fn slot() -> &'static Mutex>> { - OBSERVER.get_or_init(|| Mutex::new(None)) - } - - pub fn install(app: &AppHandle) { - let app = app.clone(); - let Err(err) = app.clone().run_on_main_thread(move || { - autoreleasepool(|_| unsafe { - let mut guard = slot().lock().unwrap_or_else(|e| e.into_inner()); - if guard.is_some() { - return; - } - - let workspace = NSWorkspace::sharedWorkspace(); - let center = workspace.notificationCenter(); - let observer = CapPowerObserver::new(app.clone()); - - let observer_obj: &AnyObject = &observer; - - center.addObserver_selector_name_object( - observer_obj, - objc2::sel!(handleWillSleep:), - Some(ns_string!("NSWorkspaceWillSleepNotification")), - None, - ); - center.addObserver_selector_name_object( - observer_obj, - objc2::sel!(handleDidWake:), - Some(ns_string!("NSWorkspaceDidWakeNotification")), - None, - ); - center.addObserver_selector_name_object( - observer_obj, - objc2::sel!(handleScreensDidSleep:), - Some(ns_string!("NSWorkspaceScreensDidSleepNotification")), - None, - ); - center.addObserver_selector_name_object( - observer_obj, - objc2::sel!(handleScreensDidWake:), - Some(ns_string!("NSWorkspaceScreensDidWakeNotification")), - None, - ); - - *guard = Some(observer); - }); - }) else { - return; - }; - warn!("Failed to install power observer on main thread: {err}"); - } - - pub fn uninstall(app: &AppHandle) { - let Err(err) = app.clone().run_on_main_thread(move || { - autoreleasepool(|_| unsafe { - let mut guard = slot().lock().unwrap_or_else(|e| e.into_inner()); - if let Some(observer) = guard.take() { - let workspace = NSWorkspace::sharedWorkspace(); - let center = workspace.notificationCenter(); - let observer_obj: &AnyObject = &observer; - center.removeObserver(observer_obj); - } - }); - }) else { - return; - }; - warn!("Failed to uninstall power observer on main thread: {err}"); - } -} - -#[cfg(target_os = "windows")] -mod windows { - use super::{on_system_did_wake, on_system_will_sleep}; - use ::windows::Win32::Foundation::{HANDLE, WIN32_ERROR}; - use ::windows::Win32::System::Power::{ - DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS, HPOWERNOTIFY, PowerRegisterSuspendResumeNotification, - PowerUnregisterSuspendResumeNotification, - }; - use ::windows::Win32::UI::WindowsAndMessaging::{ - DEVICE_NOTIFY_CALLBACK, PBT_APMRESUMEAUTOMATIC, PBT_APMSUSPEND, - }; - use std::ffi::c_void; - use std::sync::{Mutex, OnceLock}; - use tauri::AppHandle; - use tracing::warn; - - struct RegistrationHandle { - handle: HPOWERNOTIFY, - app: Box, - } - - unsafe impl Send for RegistrationHandle {} - unsafe impl Sync for RegistrationHandle {} - - static REGISTRATION: OnceLock>> = OnceLock::new(); - - fn slot() -> &'static Mutex> { - REGISTRATION.get_or_init(|| Mutex::new(None)) - } - - unsafe extern "system" fn power_callback( - context: *const c_void, - event_type: u32, - _setting: *const c_void, - ) -> u32 { - if context.is_null() { - return 0; - } - let app = unsafe { &*(context as *const AppHandle) }; - match event_type { - PBT_APMSUSPEND => on_system_will_sleep(app), - PBT_APMRESUMEAUTOMATIC => on_system_did_wake(app), - _ => {} - } - 0 - } - - pub fn install(app: &AppHandle) { - let mut guard = slot().lock().unwrap_or_else(|e| e.into_inner()); - if guard.is_some() { - return; - } - - let boxed_app = Box::new(app.clone()); - let context = Box::as_ref(&boxed_app) as *const AppHandle as *mut c_void; - - let params = DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS { - Callback: Some(power_callback), - Context: context, - }; - - let mut raw_handle: *mut c_void = std::ptr::null_mut(); - let result = unsafe { - PowerRegisterSuspendResumeNotification( - DEVICE_NOTIFY_CALLBACK, - HANDLE(¶ms as *const _ as *mut c_void), - &mut raw_handle, - ) - }; - - if result != WIN32_ERROR(0) { - warn!( - code = result.0, - "PowerRegisterSuspendResumeNotification failed" - ); - return; - } - - *guard = Some(RegistrationHandle { - handle: HPOWERNOTIFY(raw_handle as isize), - app: boxed_app, - }); - } - - pub fn uninstall(_app: &AppHandle) { - let mut guard = slot().lock().unwrap_or_else(|e| e.into_inner()); - if let Some(reg) = guard.take() { - let _ = unsafe { PowerUnregisterSuspendResumeNotification(reg.handle) }; - drop(reg.app); - } - } -} diff --git a/apps/desktop/src-tauri/src/preparing_finalization.rs b/apps/desktop/src-tauri/src/preparing_finalization.rs deleted file mode 100644 index 31abc24de62..00000000000 --- a/apps/desktop/src-tauri/src/preparing_finalization.rs +++ /dev/null @@ -1,588 +0,0 @@ -use super::{ - FinalizationAttempt, FinalizationOrigin, FinalizationProject, FinalizationToken, - FinalizingRecordings, FinalizingRecordingsMap, -}; -use cap_recording::{ - recovery::{PreparingStudioJob, PreparingStudioObserver}, - studio_recording::CompletedRecording, -}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(super) enum PreparingFinalizationState { - #[default] - Waiting, - Declined, - Offered(PreparingStudioObserver), -} - -#[derive(Clone)] -pub(crate) struct FinalizationPreparing { - recordings: Arc>, - attempt: Arc, -} - -impl FinalizationToken { - pub(crate) fn preparing(&self) -> FinalizationPreparing { - FinalizationPreparing { - recordings: self.recordings.clone(), - attempt: self.attempt.clone(), - } - } -} - -impl FinalizingRecordings { - pub(crate) fn preparing_for_project( - &self, - project: &FinalizationProject, - ) -> Option { - let recordings = self.recordings.lock().unwrap(); - let attempt = recordings.attempts.get(&project.identity)?; - if attempt.origin != FinalizationOrigin::Recording || attempt.result.borrow().is_some() { - return None; - } - Some(FinalizationPreparing { - recordings: self.recordings.clone(), - attempt: attempt.clone(), - }) - } -} - -impl FinalizationPreparing { - pub(crate) fn allows_preparing_continuation(&self) -> bool { - let recordings = self.recordings.lock().unwrap(); - self.attempt.origin == FinalizationOrigin::Recording - && recordings - .attempts - .get(&self.attempt.project.identity) - .map_or_else( - || matches!(&*self.attempt.result.borrow(), Some(Ok(()))), - |current| Arc::ptr_eq(current, &self.attempt), - ) - && self - .attempt - .result - .borrow() - .as_ref() - .is_none_or(Result::is_ok) - && matches!(&*self.attempt.preparing.borrow(), PreparingFinalizationState::Offered(observer) if observer.publication_succeeded()) - } - - pub(crate) fn is_pending(&self) -> bool { - self.is_current(&self.recordings.lock().unwrap()) - } - - pub(crate) fn set_presentation( - &self, - presentation: Result, - ) { - let recordings = self.recordings.lock().unwrap(); - if self.is_current(&recordings) { - self.attempt - .preparing_presentation - .send_if_modified(|state| { - if state.is_some() { - return false; - } - *state = Some(presentation.map(Arc::new)); - true - }); - } - } - - pub(crate) async fn wait_for_presentation( - &self, - ) -> Option> { - let mut presentation = self.attempt.preparing_presentation.subscribe(); - let mut result = self.attempt.result.subscribe(); - loop { - let state = { - let recordings = self.recordings.lock().unwrap(); - if !self.is_current(&recordings) { - return None; - } - presentation.borrow_and_update().clone() - }; - if let Some(state) = state { - return state.ok(); - } - tokio::select! { - changed = presentation.changed() => if changed.is_err() { return None; }, - changed = result.changed() => if changed.is_err() { return None; }, - } - } - } - - #[cfg(test)] - fn same_attempt(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.attempt, &other.attempt) - } - - fn is_current(&self, recordings: &FinalizingRecordingsMap) -> bool { - self.attempt.origin == FinalizationOrigin::Recording - && recordings - .attempts - .get(&self.attempt.project.identity) - .is_some_and(|current| Arc::ptr_eq(current, &self.attempt)) - && self.attempt.result.borrow().is_none() - } - - pub(crate) fn claim(&self, completed: &CompletedRecording) -> Option { - self.claim_with(completed, PreparingStudioJob::claim) - } - - fn claim_with( - &self, - completed: &CompletedRecording, - claim: impl FnOnce( - &CompletedRecording, - u64, - ) -> Option<(PreparingStudioJob, PreparingStudioObserver)>, - ) -> Option { - let paths_match = completed.project_path == self.attempt.project.display_path() - && self.attempt.project.display_path() == self.attempt.project.work_path(); - let directory_valid = paths_match - && self.attempt.preparing_generation.is_some() - && self.attempt.project.validate().is_ok(); - let recordings = self.recordings.lock().unwrap(); - if !self.is_current(&recordings) { - return None; - } - let mut job = None; - self.attempt.preparing.send_if_modified(|state| { - if !matches!(state, PreparingFinalizationState::Waiting) { - return false; - } - *state = PreparingFinalizationState::Declined; - if directory_valid - && let Some(generation) = self.attempt.preparing_generation - && let Some((claimed, observer)) = claim(completed, generation) - { - job = Some(claimed); - *state = PreparingFinalizationState::Offered(observer); - } - true - }); - job - } - - pub(crate) async fn wait_for_preparing(&self) -> Option { - let mut preparing = self.attempt.preparing.subscribe(); - let mut result = self.attempt.result.subscribe(); - loop { - let state = { - let recordings = self.recordings.lock().unwrap(); - if !self.is_current(&recordings) { - return None; - } - preparing.borrow_and_update().clone() - }; - match state { - PreparingFinalizationState::Waiting => {} - PreparingFinalizationState::Declined => return None, - PreparingFinalizationState::Offered(observer) => return Some(observer), - } - tokio::select! { - changed = preparing.changed() => if changed.is_err() { return None; }, - changed = result.changed() => if changed.is_err() { return None; }, - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{FinalizationAccess, await_finalization_result, has_pending_finalizations}; - use cap_project::RecordingMeta; - use std::path::Path; - use std::time::Duration; - - fn project() -> (tempfile::TempDir, Arc) { - let directory = tempfile::tempdir().unwrap(); - let project = FinalizationProject::capture( - directory.path().canonicalize().unwrap(), - FinalizationAccess::Write, - ) - .unwrap(); - (directory, project) - } - - async fn wait_for_preparing( - preparing: &FinalizationPreparing, - ) -> Option { - tokio::time::timeout(Duration::from_secs(1), preparing.wait_for_preparing()) - .await - .unwrap() - } - - fn completed_without_receipt(path: &Path) -> CompletedRecording { - let metadata: RecordingMeta = serde_json::from_value(serde_json::json!({ - "pretty_name": "Preparing finalization test", "sharing": null, - "segments": [{"display": {"path": "content/segments/segment-0/display", "fps": 30}}], - "status": {"status": "NeedsRemux"} - })) - .unwrap(); - CompletedRecording { - project_path: path.to_path_buf(), - meta: metadata.studio_meta().unwrap().clone(), - cursor_data: Default::default(), - clean_stopped: None, - } - } - - #[tokio::test] - async fn declined_receipt_is_not_finalization_success() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project.clone()).unwrap(); - let preparing = recordings.preparing_for_project(&project).unwrap(); - assert!(preparing.same_attempt(&token.preparing())); - assert!( - preparing - .claim(&completed_without_receipt(project.display_path())) - .is_none() - ); - assert!(wait_for_preparing(&preparing).await.is_none()); - assert!(has_pending_finalizations( - &recordings.recordings.lock().unwrap() - )); - let result = token.attempt.result.subscribe(); - token.finish(Err("configuration write failed".into())); - assert!( - await_finalization_result(result) - .await - .unwrap_err() - .contains("configuration write failed") - ); - assert!(recordings.preparing_for_project(&project).is_none()); - } - - #[tokio::test] - async fn completion_before_admission_does_not_invoke_claim() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project.clone()).unwrap(); - let preparing = token.preparing(); - token.finish(Ok(())); - assert!( - preparing - .claim_with( - &completed_without_receipt(project.display_path()), - |_, _| panic!("Completed attempts cannot claim") - ) - .is_none() - ); - assert!(wait_for_preparing(&preparing).await.is_none()); - assert!(!has_pending_finalizations( - &recordings.recordings.lock().unwrap() - )); - } - - #[tokio::test] - async fn failed_retry_has_a_new_attempt_and_rejects_stale_claims() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - let first = recordings.start_finalizing(project.clone()).unwrap(); - assert_eq!(first.attempt.preparing_generation, Some(1)); - let stale = first.preparing(); - first.finish(Err("first failure".into())); - let second = recordings.start_finalizing(project.clone()).unwrap(); - assert_eq!(second.attempt.preparing_generation, Some(2)); - let current = second.preparing(); - assert!(!stale.same_attempt(¤t)); - assert!( - stale - .claim_with( - &completed_without_receipt(project.display_path()), - |_, _| panic!("Stale attempts cannot claim") - ) - .is_none() - ); - assert!(wait_for_preparing(&stale).await.is_none()); - assert!( - tokio::time::timeout(Duration::from_millis(20), current.wait_for_preparing()) - .await - .is_err() - ); - second.finish(Ok(())); - assert!(wait_for_preparing(¤t).await.is_none()); - } - - #[tokio::test] - async fn token_drop_wakes_pending_observer_and_preserves_interrupted_error() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project).unwrap(); - let preparing = token.preparing(); - let result = token.attempt.result.subscribe(); - assert!( - tokio::time::timeout(Duration::from_millis(20), preparing.wait_for_preparing()) - .await - .is_err() - ); - drop(token); - assert!( - tokio::time::timeout(Duration::from_secs(1), preparing.wait_for_preparing()) - .await - .unwrap() - .is_none() - ); - assert!( - await_finalization_result(result) - .await - .unwrap_err() - .contains("interrupted") - ); - } - - #[test] - fn native_recording_attempt_allocates_generation_without_capture_coordinator() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project.clone()).unwrap(); - let preparing = token.preparing(); - let clone = preparing.clone(); - let completed = completed_without_receipt(project.display_path()); - let called = std::cell::Cell::new(false); - assert!( - preparing - .claim_with(&completed, |actual, generation| { - called.set(true); - assert_eq!(actual.project_path, project.display_path()); - assert_eq!(generation, 1); - None - }) - .is_none() - ); - assert!(called.get()); - assert!( - clone - .claim_with(&completed, |_, _| panic!( - "Duplicate admission cannot claim" - )) - .is_none() - ); - assert!(token.attempt.result.borrow().is_none()); - token.finish(Ok(())); - } - - #[test] - fn lazy_open_and_recovery_origin_never_allocate_or_invoke_claim() { - let (_directory, project) = project(); - for (origin, preparing_requested) in [ - (FinalizationOrigin::Recording, false), - (FinalizationOrigin::Recovery, true), - ] { - let recordings = FinalizingRecordings::default(); - let token = recordings - .start_with_origin(project.clone(), origin, preparing_requested) - .unwrap(); - assert!( - token - .preparing() - .claim_with( - &completed_without_receipt(project.display_path()), - |_, _| panic!("Ineligible finalization cannot claim") - ) - .is_none() - ); - assert_eq!( - recordings - .recordings - .lock() - .unwrap() - .last_preparing_generation, - 0 - ); - if origin == FinalizationOrigin::Recovery { - assert!(recordings.preparing_for_project(&project).is_none()); - } - token.finish(Ok(())); - } - } - - #[test] - fn duplicate_finalization_keeps_the_original_attempt_generation() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - let first = recordings.start_finalizing(project.clone()).unwrap(); - assert!(recordings.start_finalizing(project.clone()).is_err()); - let existing = recordings - .request(project.clone(), false, FinalizationOrigin::Recording, false) - .unwrap(); - assert!(matches!(existing, crate::FinalizationRequest::Existing(_))); - assert_eq!(first.attempt.preparing_generation, Some(1)); - assert_eq!( - recordings - .recordings - .lock() - .unwrap() - .last_preparing_generation, - 1 - ); - assert!( - first - .preparing() - .same_attempt(&recordings.preparing_for_project(&project).unwrap()) - ); - first.finish(Err("retained retry".into())); - let next = recordings.start_finalizing(project).unwrap(); - assert_eq!(next.attempt.preparing_generation, Some(2)); - next.finish(Ok(())); - } - - #[tokio::test] - async fn generation_exhaustion_declines_preview_and_preserves_finalization() { - let (_directory, project) = project(); - let recordings = FinalizingRecordings::default(); - recordings - .recordings - .lock() - .unwrap() - .last_preparing_generation = u64::MAX - 1; - let last = recordings.start_finalizing(project.clone()).unwrap(); - assert_eq!(last.attempt.preparing_generation, Some(u64::MAX)); - last.finish(Err("retained retry".into())); - let exhausted = recordings.start_finalizing(project.clone()).unwrap(); - assert_eq!(exhausted.attempt.preparing_generation, None); - assert_eq!( - recordings - .recordings - .lock() - .unwrap() - .last_preparing_generation, - u64::MAX - ); - let preparing = exhausted.preparing(); - assert!( - preparing - .claim_with( - &completed_without_receipt(project.display_path()), - |_, _| { panic!("An exhausted sequence cannot claim") } - ) - .is_none() - ); - assert!(wait_for_preparing(&preparing).await.is_none()); - assert!(has_pending_finalizations( - &recordings.recordings.lock().unwrap() - )); - let result = exhausted.attempt.result.subscribe(); - exhausted.finish(Ok(())); - assert_eq!(await_finalization_result(result).await, Ok(())); - assert!(!has_pending_finalizations( - &recordings.recordings.lock().unwrap() - )); - } - - #[test] - fn a_different_stopped_project_never_invokes_claim() { - let (_directory, project) = project(); - let other = tempfile::tempdir().unwrap(); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project).unwrap(); - assert!( - token - .preparing() - .claim_with(&completed_without_receipt(other.path()), |_, _| panic!( - "Wrong project cannot claim" - )) - .is_none() - ); - token.finish(Ok(())); - } - - #[test] - fn canonical_alias_declines_without_rewriting_the_stopped_path() { - let directory = tempfile::tempdir().unwrap(); - std::fs::create_dir(directory.path().join("alias")).unwrap(); - std::fs::create_dir(directory.path().join("project")).unwrap(); - let display = directory.path().join("alias/../project"); - let project = - FinalizationProject::capture(display.clone(), FinalizationAccess::Write).unwrap(); - assert_ne!(project.display_path(), project.work_path()); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project).unwrap(); - let completed = completed_without_receipt(&display); - assert!( - token - .preparing() - .claim_with(&completed, |_, _| panic!( - "Alias must not rebind a clean receipt" - )) - .is_none() - ); - assert_eq!(completed.project_path, display); - token.finish(Ok(())); - } - - #[test] - fn replaced_directory_declines_before_claim() { - let directory = tempfile::tempdir().unwrap(); - let original = directory.path().join("project"); - let moved = directory.path().join("moved"); - std::fs::create_dir(&original).unwrap(); - let project = FinalizationProject::capture( - original.canonicalize().unwrap(), - FinalizationAccess::Write, - ) - .unwrap(); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project.clone()).unwrap(); - std::fs::rename(&original, &moved).unwrap(); - std::fs::create_dir(&original).unwrap(); - assert!( - token - .preparing() - .claim_with( - &completed_without_receipt(project.display_path()), - |_, _| panic!("Replaced directory cannot claim") - ) - .is_none() - ); - assert!(token.attempt.result.borrow().is_none()); - token.finish(Err("directory replaced".into())); - } - - #[tokio::test] - async fn missing_fragments_keep_the_existing_error_and_do_not_claim() { - let (_directory, project) = project(); - let sentinel = project.work_path().join("retained-original.bin"); - std::fs::write(&sentinel, b"retain original bytes").unwrap(); - let completed = completed_without_receipt(project.display_path()); - let recordings = FinalizingRecordings::default(); - let token = recordings.start_finalizing(project.clone()).unwrap(); - let preparing = token.preparing(); - let ordinary = crate::recording::remux_fragmented_recording_with_trigger( - project.work_path(), - project.display_path(), - "recording_stop", - None, - ); - let candidate = crate::recording::remux_fragmented_recording_with_preparing( - project.work_path(), - project.display_path(), - "recording_stop", - None, - Some((&preparing, &completed)), - ); - assert_eq!(candidate, ordinary); - assert_eq!( - candidate.as_ref().unwrap_err(), - "Could not find fragments to remux" - ); - assert!(matches!( - *token.attempt.preparing.borrow(), - PreparingFinalizationState::Waiting - )); - assert_eq!(std::fs::read(sentinel).unwrap(), b"retain original bytes"); - assert!( - !project - .work_path() - .join(crate::recording::FRAGMENTED_EXPORT_FFMPEG_MARKER) - .exists() - ); - token.finish(candidate); - assert!(wait_for_preparing(&preparing).await.is_none()); - } -} diff --git a/apps/desktop/src-tauri/src/presets.rs b/apps/desktop/src-tauri/src/presets.rs deleted file mode 100644 index c3d8d2f3c8c..00000000000 --- a/apps/desktop/src-tauri/src/presets.rs +++ /dev/null @@ -1,88 +0,0 @@ -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)] -#[serde(rename_all = "camelCase")] -pub struct PresetsStore { - presets: Vec, - default: Option, -} - -#[derive(Serialize, Deserialize, Type, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Preset { - name: String, - pub config: ProjectConfiguration, -} - -impl PresetsStore { - fn get(app: &AppHandle) -> Result, String> { - match app.store("store").map(|s| s.get("presets")) { - Ok(Some(store)) => match serde_json::from_value(store.clone()) { - Ok(settings) => Ok(Some(settings)), - Err(e) => { - error!( - "Failed to deserialize presets store: {}. Raw value: {}", - e, - serde_json::to_string_pretty(&store).unwrap_or_default() - ); - Ok(None) - } - }, - _ => Ok(None), - } - } - - pub fn clear(app: &AppHandle) -> Result<(), String> { - let store = app.store("store").map_err(|e| e.to_string())?; - store.delete("presets"); - store.save().map_err(|e| e.to_string())?; - Ok(()) - } - - pub fn get_default_preset(app: &AppHandle) -> Result, String> { - let Some(this) = Self::get(app)? else { - return Ok(None); - }; - - let Some(default_i) = this.default else { - return Ok(None); - }; - - Ok(this.presets.get(default_i as usize).cloned()) - } - - pub fn get_by_name(app: &AppHandle, name: &str) -> Result, String> { - let Some(this) = Self::get(app)? else { - return Ok(None); - }; - - Ok(this.presets.into_iter().find(|p| p.name == name)) - } - - #[allow(unused)] - pub fn update(app: &AppHandle, update: impl FnOnce(&mut Self)) -> Result<(), String> { - let Ok(store) = app.store("store") else { - return Err("Store not found".to_string()); - }; - - let mut settings = Self::get(app)?.unwrap_or_default(); - update(&mut settings); - store.set("presets", json!(settings)); - store.save().map_err(|e| e.to_string()) - } -} - -impl Preset { - #[allow(unused)] - fn resolve(&self, timeline: TimelineConfiguration) -> ProjectConfiguration { - let mut ret = self.config.clone(); - ret.timeline = Some(timeline); - ret - } -} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs deleted file mode 100644 index 03b8a1d9447..00000000000 --- a/apps/desktop/src-tauri/src/recording.rs +++ /dev/null @@ -1,10476 +0,0 @@ -use anyhow::anyhow; -use cap_fail::fail; -use cap_media_info::ffmpeg_sample_format_for; -use cap_project::CursorMoveEvent; -use cap_project::cursor::SHORT_CURSOR_SHAPE_DEBOUNCE_MS; -use cap_project::{ - CameraShape, CornerStyle, CursorClickEvent, GlideDirection, InstantRecordingMeta, - MultipleSegments, Platform, ProjectConfiguration, RecordingMeta, RecordingMetaInner, - SharingMeta, StudioRecordingMeta, StudioRecordingStatus, TimelineConfiguration, - TimelineSegment, ZoomMode, ZoomSegment, cursor::CursorEvents, -}; -#[cfg(target_os = "macos")] -use cap_recording::SendableShareableContent; -use cap_recording::feeds::camera::CameraFeedLock; -#[cfg(target_os = "macos")] -use cap_recording::sources::screen_capture::SourceError; -use cap_recording::{ - RecordingMode, - feeds::{camera, microphone}, - instant_recording, - recovery::RecoveryManager, - sources::MicrophoneSourceError, - sources::{ - screen_capture, - screen_capture::{CaptureDisplay, CaptureWindow, ScreenCaptureTarget}, - }, - studio_recording, -}; -use cap_rendering::ProjectRecordingsMeta; -use cap_utils::{ensure_dir, moment_format_to_chrono, spawn_actor}; -use cpal::traits::DeviceTrait; -use futures::{FutureExt, stream}; -use lazy_static::lazy_static; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::borrow::Cow; -#[cfg(target_os = "macos")] -use std::error::Error as StdError; -use std::{ - any::Any, - collections::BTreeSet, - panic::AssertUnwindSafe, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, - time::Duration, -}; -use tauri::{AppHandle, Listener, Manager, path::BaseDirectory}; -use tauri_plugin_dialog::{ - DialogExt, MessageDialogBuilder, MessageDialogButtons, MessageDialogKind, -}; -use tauri_plugin_store::StoreExt; -use tauri_specta::Event; -use tokio_util::sync::CancellationToken; -use tracing::*; - -use crate::camera::CameraPreviewShape; -#[cfg(target_os = "macos")] -use crate::general_settings; -use crate::permissions; -use crate::web_api::AuthedApiError; -#[cfg(target_os = "macos")] -use crate::window_exclusion::WindowExclusion; -use crate::{ - App, CameraWindowOperationLock, CurrentRecordingChanged, EditorRecordingAdded, - FinalizingRecordings, MutableState, NewStudioRecordingAdded, RecordingStarted, RecordingState, - RecordingStopped, VideoUploadInfo, - api::PresignedS3PutRequestMethod, - audio::AppSounds, - auth::AuthStore, - create_screenshot, create_screenshot_source_from_segments, - general_settings::{GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour}, - open_external_link, - presets::PresetsStore, - thumbnails::*, - upload::{SegmentUploader, compress_image}, - web_api::ManagerExt, - windows::{ - CapWindowId, EditorRecordingTarget, ShowCapWindow, editor_window_for_path, hide_overlay, - }, -}; - -#[cfg(not(target_os = "linux"))] -use crate::upload::InstantMultipartUpload; - -fn recording_stopped_share_url(link: &str) -> String { - if link.contains('?') { - format!("{link}&recordingStopped=1") - } else { - format!("{link}?recordingStopped=1") - } -} - -const CURRENT_DESKTOP_BACKGROUND_BASENAME: &str = "current-desktop-background"; -const CURRENT_DESKTOP_BACKGROUND_FILENAME: &str = "current-desktop-background.jpg"; -const CURRENT_DESKTOP_BACKGROUND_PENDING_FILENAME: &str = "current-desktop-background.pending.jpg"; -const DESKTOP_BACKGROUND_MAX_DIMENSION: u32 = 2560; -const DESKTOP_BACKGROUND_JPEG_QUALITY: u8 = 82; - -fn current_desktop_background_snapshot_path(recording_dir: &Path) -> PathBuf { - recording_dir - .join("assets") - .join(CURRENT_DESKTOP_BACKGROUND_FILENAME) -} - -fn stored_current_desktop_background_path(recording_dir: &Path) -> Option { - let path = current_desktop_background_snapshot_path(recording_dir); - path.exists().then(|| path.to_string_lossy().into_owned()) -} - -fn pending_current_desktop_background_snapshot_path(recording_dir: &Path) -> PathBuf { - recording_dir - .join("assets") - .join(CURRENT_DESKTOP_BACKGROUND_PENDING_FILENAME) -} - -fn spawn_current_desktop_background_snapshot( - recording_dir: PathBuf, - capture_target: ScreenCaptureTarget, -) { - if matches!(capture_target, ScreenCaptureTarget::CameraOnly) { - return; - } - - tokio::spawn(async move { - match store_current_desktop_background_snapshot(recording_dir, capture_target).await { - Ok(CurrentDesktopBackgroundSnapshot::Stored(path)) => debug!( - path = %path.display(), - "Stored current desktop background for recording" - ), - Ok(CurrentDesktopBackgroundSnapshot::SkippedProtectedLocation(path)) => debug!( - path = %path.display(), - "Skipped current desktop background from protected location" - ), - Err(reason) => debug!( - %reason, - "Current desktop background snapshot unavailable" - ), - } - }); -} - -enum CurrentDesktopBackgroundSnapshot { - Stored(PathBuf), - SkippedProtectedLocation(PathBuf), -} - -enum CurrentDesktopBackgroundWrite { - Stored, - SkippedProtectedLocation(PathBuf), -} - -async fn store_current_desktop_background_snapshot( - recording_dir: PathBuf, - capture_target: ScreenCaptureTarget, -) -> Result { - let display_id = capture_target - .display() - .map(|display| display.id().to_string()); - - tokio::task::spawn_blocking(move || { - let output_path = current_desktop_background_snapshot_path(&recording_dir); - let pending_path = pending_current_desktop_background_snapshot_path(&recording_dir); - write_current_desktop_background_to( - &output_path, - &pending_path, - display_id.as_deref(), - true, - ) - .map(|result| match result { - CurrentDesktopBackgroundWrite::Stored => { - CurrentDesktopBackgroundSnapshot::Stored(output_path) - } - CurrentDesktopBackgroundWrite::SkippedProtectedLocation(path) => { - CurrentDesktopBackgroundSnapshot::SkippedProtectedLocation(path) - } - }) - }) - .await - .map_err(|err| format!("Desktop background snapshot task failed: {err}"))? -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub async fn import_current_desktop_background(project_path: String) -> Result { - let project_dir = PathBuf::from(project_path); - - tokio::task::spawn_blocking(move || { - let source_path = current_desktop_background_source_path(None) - .ok_or_else(|| "Current desktop background path not found".to_string())?; - import_current_desktop_background_from_source(&project_dir, &source_path) - }) - .await - .map_err(|err| format!("Desktop background snapshot task failed: {err}"))? -} - -fn import_current_desktop_background_from_source( - project_dir: &Path, - source_path: &Path, -) -> Result { - let assets_dir = project_dir.join("assets"); - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|elapsed| elapsed.as_nanos()) - .unwrap_or(0); - let output_name = format!("{CURRENT_DESKTOP_BACKGROUND_BASENAME}-{timestamp}.jpg"); - let output_path = assets_dir.join(&output_name); - let pending_path = assets_dir.join(format!( - "{CURRENT_DESKTOP_BACKGROUND_BASENAME}-{timestamp}.pending.jpg" - )); - - if !matches!( - write_desktop_background_source_to(source_path, &output_path, &pending_path)?, - CurrentDesktopBackgroundWrite::Stored - ) { - return Err("Current desktop background snapshot was skipped".to_string()); - } - - Ok(output_path.to_string_lossy().into_owned()) -} - -fn write_current_desktop_background_to( - output_path: &Path, - pending_path: &Path, - display_id: Option<&str>, - enforce_protected_check: bool, -) -> Result { - let source_path = current_desktop_background_source_path(display_id) - .ok_or_else(|| "Current desktop background path not found".to_string())?; - - if enforce_protected_check && desktop_background_source_requires_user_prompt(&source_path) { - return Ok(CurrentDesktopBackgroundWrite::SkippedProtectedLocation( - source_path, - )); - } - - write_desktop_background_source_to(&source_path, output_path, pending_path) -} - -fn write_desktop_background_source_to( - source_path: &Path, - output_path: &Path, - pending_path: &Path, -) -> Result { - if !source_path.exists() { - return Err(format!( - "Current desktop background does not exist: {}", - source_path.display() - )); - } - - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent) - .map_err(|err| format!("Failed to create background assets directory: {err}"))?; - } - - let _ = std::fs::remove_file(pending_path); - if let Err(error) = write_desktop_background_snapshot(source_path, pending_path) { - let _ = std::fs::remove_file(pending_path); - return Err(error); - } - - if output_path.exists() { - std::fs::remove_file(output_path) - .map_err(|err| format!("Failed to replace current desktop background: {err}"))?; - } - - std::fs::rename(pending_path, output_path) - .map_err(|err| format!("Failed to store current desktop background: {err}"))?; - - Ok(CurrentDesktopBackgroundWrite::Stored) -} - -#[cfg(target_os = "macos")] -fn current_desktop_background_source_path(display_id: Option<&str>) -> Option { - use cocoa::appkit::NSScreen; - use cocoa::base::{id, nil}; - use cocoa::foundation::NSString; - use objc::{class, msg_send, sel, sel_impl}; - use std::ffi::CStr; - - unsafe { - let screen = - macos_screen_for_display_id(display_id).unwrap_or_else(|| NSScreen::mainScreen(nil)); - if screen == nil { - return None; - } - - let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; - if workspace == nil { - return None; - } - - let url: id = msg_send![workspace, desktopImageURLForScreen: screen]; - if url == nil { - return None; - } - - let path: id = msg_send![url, path]; - if path == nil { - return None; - } - - let path = CStr::from_ptr(NSString::UTF8String(path)) - .to_string_lossy() - .to_string(); - (!path.is_empty()).then(|| PathBuf::from(path)) - } -} - -#[cfg(target_os = "macos")] -fn macos_screen_for_display_id(display_id: Option<&str>) -> Option { - use cocoa::appkit::NSScreen; - use cocoa::base::{id, nil}; - use cocoa::foundation::{NSArray, NSDictionary, NSString}; - use objc::{msg_send, sel, sel_impl}; - - let expected_id = display_id?.parse::().ok()?; - - unsafe { - let screens = NSScreen::screens(nil); - if screens == nil { - return None; - } - - let screen_number_key = NSString::alloc(nil).init_str("NSScreenNumber"); - for index in 0..NSArray::count(screens) { - let screen: id = screens.objectAtIndex(index); - if screen == nil { - continue; - } - - let device_description = NSScreen::deviceDescription(screen); - let number = NSDictionary::valueForKey_(device_description, screen_number_key) as id; - if number == nil { - continue; - } - - let number_value: u32 = msg_send![number, unsignedIntValue]; - if number_value == expected_id { - return Some(screen); - } - } - } - - None -} - -#[cfg(target_os = "windows")] -fn current_desktop_background_source_path(_display_id: Option<&str>) -> Option { - use std::{ffi::OsString, os::windows::ffi::OsStringExt}; - use windows::Win32::UI::WindowsAndMessaging::{ - SPI_GETDESKWALLPAPER, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW, - }; - - let mut buffer = vec![0u16; 32_768]; - unsafe { - SystemParametersInfoW( - SPI_GETDESKWALLPAPER, - u32::try_from(buffer.len()).ok()?, - Some(buffer.as_mut_ptr().cast()), - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), - ) - .ok()?; - } - - let len = buffer - .iter() - .position(|character| *character == 0) - .unwrap_or(buffer.len()); - (len > 0).then(|| PathBuf::from(OsString::from_wide(&buffer[..len]))) -} - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -fn current_desktop_background_source_path(_display_id: Option<&str>) -> Option { - None -} - -fn desktop_background_source_requires_user_prompt(source_path: &Path) -> bool { - #[cfg(target_os = "macos")] - { - dirs::home_dir().is_some_and(|home_dir| { - desktop_background_source_requires_user_prompt_for_home(source_path, &home_dir) - }) - } - - #[cfg(not(target_os = "macos"))] - { - let _ = source_path; - false - } -} - -#[cfg(any(target_os = "macos", test))] -fn desktop_background_source_requires_user_prompt_for_home( - source_path: &Path, - home_dir: &Path, -) -> bool { - [ - home_dir.join("Desktop"), - home_dir.join("Documents"), - home_dir.join("Downloads"), - home_dir.join("Library/Mobile Documents"), - home_dir.join("Library/CloudStorage"), - ] - .iter() - .any(|protected_dir| source_path.starts_with(protected_dir)) -} - -#[cfg(target_os = "macos")] -fn macos_image_pixel_dimensions(path: &Path) -> Option<(u32, u32)> { - let output = std::process::Command::new("sips") - .arg("-g") - .arg("pixelWidth") - .arg("-g") - .arg("pixelHeight") - .arg(path) - .output() - .ok()?; - - if !output.status.success() { - return None; - } - - let text = String::from_utf8_lossy(&output.stdout); - let mut width = None; - let mut height = None; - for line in text.lines() { - let line = line.trim(); - if let Some(value) = line.strip_prefix("pixelWidth:") { - width = value.trim().parse::().ok(); - } else if let Some(value) = line.strip_prefix("pixelHeight:") { - height = value.trim().parse::().ok(); - } - } - - Some((width?, height?)) -} - -#[cfg(target_os = "macos")] -fn write_desktop_background_snapshot(source_path: &Path, output_path: &Path) -> Result<(), String> { - // `sips -Z` resizes in both directions, so it upscales sources smaller than the - // target. Only cap dimensions when the source actually exceeds the limit. - let needs_downscale = - macos_image_pixel_dimensions(source_path).is_none_or(|(width, height)| { - width > DESKTOP_BACKGROUND_MAX_DIMENSION || height > DESKTOP_BACKGROUND_MAX_DIMENSION - }); - - let mut command = std::process::Command::new("sips"); - command - .arg("-s") - .arg("format") - .arg("jpeg") - .arg("-s") - .arg("formatOptions") - .arg(DESKTOP_BACKGROUND_JPEG_QUALITY.to_string()); - - if needs_downscale { - command - .arg("-Z") - .arg(DESKTOP_BACKGROUND_MAX_DIMENSION.to_string()); - } - - let sips_result = command - .arg(source_path) - .arg("--out") - .arg(output_path) - .output(); - - if let Ok(output) = sips_result - && output.status.success() - { - return Ok(()); - } - - write_desktop_background_snapshot_with_image_crate(source_path, output_path) -} - -#[cfg(not(target_os = "macos"))] -fn write_desktop_background_snapshot(source_path: &Path, output_path: &Path) -> Result<(), String> { - write_desktop_background_snapshot_with_image_crate(source_path, output_path) -} - -fn write_desktop_background_snapshot_with_image_crate( - source_path: &Path, - output_path: &Path, -) -> Result<(), String> { - use image::ImageEncoder; - use std::io::Write; - - let image = image::open(source_path) - .map_err(|err| format!("Failed to decode current desktop background: {err}"))?; - - let image = if image.width() > DESKTOP_BACKGROUND_MAX_DIMENSION - || image.height() > DESKTOP_BACKGROUND_MAX_DIMENSION - { - image.resize( - DESKTOP_BACKGROUND_MAX_DIMENSION, - DESKTOP_BACKGROUND_MAX_DIMENSION, - image::imageops::FilterType::Triangle, - ) - } else { - image - }; - - let rgb = image.to_rgb8(); - let file = std::fs::File::create(output_path) - .map_err(|err| format!("Failed to create current desktop background: {err}"))?; - let mut writer = std::io::BufWriter::new(file); - - image::codecs::jpeg::JpegEncoder::new_with_quality( - &mut writer, - DESKTOP_BACKGROUND_JPEG_QUALITY, - ) - .write_image( - rgb.as_raw(), - rgb.width(), - rgb.height(), - image::ExtendedColorType::Rgb8, - ) - .map_err(|err| format!("Failed to save current desktop background: {err}"))?; - - writer - .flush() - .map_err(|err| format!("Failed to finalize current desktop background: {err}")) -} - -pub fn spawn_heal_oversized_desktop_background_snapshots(recording_dir: PathBuf) { - tokio::task::spawn_blocking(move || { - heal_oversized_desktop_background_snapshots(&recording_dir); - }); -} - -fn heal_oversized_desktop_background_snapshots(recording_dir: &Path) { - let assets_dir = recording_dir.join("assets"); - let Ok(entries) = std::fs::read_dir(&assets_dir) else { - return; - }; - - for entry in entries.flatten() { - let path = entry.path(); - let Some(name) = path.file_name().and_then(|name| name.to_str()) else { - continue; - }; - - if !name.starts_with(CURRENT_DESKTOP_BACKGROUND_BASENAME) - || name.contains(".pending.") - || !name.ends_with(".jpg") - { - continue; - } - - match downscale_background_snapshot_in_place(&path) { - Ok(true) => { - info!(path = %path.display(), "Recompressed oversized desktop background snapshot") - } - Ok(false) => {} - Err(error) => { - debug!(%error, path = %path.display(), "Failed to recompress desktop background snapshot") - } - } - } -} - -fn downscale_background_snapshot_in_place(path: &Path) -> Result { - let (width, height) = image::image_dimensions(path) - .map_err(|err| format!("Failed to read background dimensions: {err}"))?; - - if width <= DESKTOP_BACKGROUND_MAX_DIMENSION && height <= DESKTOP_BACKGROUND_MAX_DIMENSION { - return Ok(false); - } - - let pending_path = path.with_extension("pending.jpg"); - let _ = std::fs::remove_file(&pending_path); - - if let Err(error) = write_desktop_background_snapshot_with_image_crate(path, &pending_path) { - let _ = std::fs::remove_file(&pending_path); - return Err(error); - } - - std::fs::rename(&pending_path, path) - .map_err(|err| format!("Failed to replace desktop background snapshot: {err}"))?; - - Ok(true) -} - -#[derive(Clone)] -pub struct InProgressRecordingCommon { - pub target_name: String, - pub inputs: StartRecordingInputs, - pub recording_dir: PathBuf, - pub health: Arc, - camera_snapshot: Arc>, -} - -#[cfg(target_os = "linux")] -type InstantUploader = Arc>; -#[cfg(not(target_os = "linux"))] -type InstantUploader = SegmentUploader; - -async fn await_instant_upload( - upload: InstantUploader, -) -> Result, String> { - #[cfg(target_os = "linux")] - { - (&mut upload.lock().await.handle) - .await - .map_err(|error| error.to_string()) - } - #[cfg(not(target_os = "linux"))] - { - upload.handle.await.map_err(|error| error.to_string()) - } -} - -pub struct StopFailureContext { - pub segment_upload: InstantUploader, - pub video_upload_info: VideoUploadInfo, -} - -#[cfg(target_os = "linux")] -type InstantActorHandle = Arc; -#[cfg(not(target_os = "linux"))] -type InstantActorHandle = instant_recording::ActorHandle; - -pub enum InProgressRecording { - Instant { - handle: InstantActorHandle, - segment_upload: InstantUploader, - video_upload_info: VideoUploadInfo, - common: InProgressRecordingCommon, - mic_feed: Option>, - camera_feed: Option>, - }, - Studio { - handle: studio_recording::ActorHandle, - common: InProgressRecordingCommon, - mic_feed: Option>, - camera_feed: Option>, - }, -} - -#[cfg(target_os = "macos")] -async fn acquire_shareable_content_for_target( - capture_target: &ScreenCaptureTarget, -) -> anyhow::Result { - let mut available_display_ids = Vec::new(); - - for attempt in 0..3 { - let shareable_content = read_recording_shareable_content().await?; - available_display_ids = shareable_content_display_ids(&shareable_content); - if !shareable_content_missing_target_display(capture_target, &shareable_content) { - return Ok(shareable_content); - } - - if attempt < 2 { - tokio::time::sleep(Duration::from_millis(150)).await; - } - } - - let requested_display = capture_target - .display() - .map(|display| display.id().to_string()) - .unwrap_or_else(|| "none".to_string()); - - Err(anyhow!( - "ScreenCaptureKit shareable content missing target display {requested_display}. Available display ids: {available_display_ids:?}" - )) -} - -#[cfg(target_os = "macos")] -async fn read_recording_shareable_content() -> anyhow::Result { - let content = cidre::sc::ShareableContent::current() - .await - .map_err(|e| anyhow!(format!("ReadShareableContent: {e}")))?; - if !content.displays().is_empty() { - return Ok(SendableShareableContent::from(content)); - } - - let process_content = cidre::sc::ShareableContent::current_process() - .await - .map_err(|e| anyhow!(format!("ReadCurrentProcessShareableContent: {e}")))?; - if !process_content.displays().is_empty() { - return Ok(SendableShareableContent::from(process_content)); - } - - Ok(SendableShareableContent::from(content)) -} - -#[cfg(target_os = "macos")] -fn shareable_content_display_ids(shareable_content: &SendableShareableContent) -> Vec { - shareable_content - .retained() - .displays() - .iter() - .map(|display| display.display_id().0.to_string()) - .collect() -} -#[cfg(target_os = "macos")] -fn shareable_content_missing_target_display( - capture_target: &ScreenCaptureTarget, - shareable_content: &SendableShareableContent, -) -> bool { - match capture_target.display() { - Some(display) => display - .raw_handle() - .as_sc(shareable_content.retained()) - .is_none(), - None => false, - } -} - -#[cfg(target_os = "macos")] -fn is_shareable_content_error(err: &anyhow::Error) -> bool { - err.chain().any(|cause| { - let cause: &dyn StdError = cause; - if let Some(source_error) = cause.downcast_ref::() { - matches!(source_error, SourceError::AsContentFilter) - } else { - false - } - }) -} - -impl InProgressRecording { - pub fn capture_target(&self) -> &ScreenCaptureTarget { - match self { - Self::Instant { handle, .. } => &handle.capture_target, - Self::Studio { handle, .. } => &handle.capture_target, - } - } - - pub fn inputs(&self) -> &StartRecordingInputs { - match self { - Self::Instant { common, .. } => &common.inputs, - Self::Studio { common, .. } => &common.inputs, - } - } - - pub fn common(&self) -> &InProgressRecordingCommon { - match self { - Self::Instant { common, .. } => common, - Self::Studio { common, .. } => common, - } - } - - pub async fn pause(&self) -> anyhow::Result<()> { - match self { - Self::Instant { handle, .. } => { - #[cfg(target_os = "linux")] - { - let _ = handle; - Err(anyhow!( - "Instant recordings cannot pause or resume; use Stop" - )) - } - #[cfg(not(target_os = "linux"))] - { - handle.pause().await - } - } - Self::Studio { handle, .. } => handle.pause().await, - } - } - - pub async fn resume(&self) -> anyhow::Result<()> { - match self { - Self::Instant { handle, .. } => { - #[cfg(target_os = "linux")] - { - let _ = handle; - Err(anyhow!( - "Instant recordings cannot pause or resume; use Stop" - )) - } - #[cfg(not(target_os = "linux"))] - { - handle.resume().await - } - } - Self::Studio { handle, .. } => handle.resume().await, - } - } - - pub async fn is_paused(&self) -> anyhow::Result { - match self { - Self::Instant { handle, .. } => handle.is_paused().await, - Self::Studio { handle, .. } => handle.is_paused().await, - } - } - - pub fn recording_dir(&self) -> &PathBuf { - match self { - Self::Instant { common, .. } => &common.recording_dir, - Self::Studio { common, .. } => &common.recording_dir, - } - } - - pub async fn stop( - self, - ) -> Result)> { - match self { - Self::Instant { - handle, - segment_upload, - video_upload_info, - common, - .. - } => match handle.stop().await { - Ok(recording) => Ok(CompletedRecording::Instant { - #[cfg(target_os = "linux")] - finalized: false, - recording, - segment_upload, - video_upload_info, - target_name: common.target_name, - }), - Err(e) => Err(( - e, - Some(StopFailureContext { - segment_upload, - video_upload_info, - }), - )), - }, - Self::Studio { handle, common, .. } => match handle.stop().await { - Ok(recording) => Ok(CompletedRecording::Studio { - recording, - target_name: common.target_name, - capture_target: common.inputs.capture_target, - }), - Err(e) => Err((e, None)), - }, - } - } - - pub fn done_fut(&self) -> cap_recording::DoneFut { - match self { - Self::Instant { handle, .. } => handle.done_fut(), - Self::Studio { handle, .. } => handle.done_fut(), - } - } - - pub fn take_health_rx(&mut self) -> Option { - match self { - Self::Instant { handle, .. } => { - #[cfg(target_os = "linux")] - { - Arc::get_mut(handle).and_then(instant_recording::ActorHandle::take_health_rx) - } - #[cfg(not(target_os = "linux"))] - { - handle.take_health_rx() - } - } - Self::Studio { .. } => None, - } - } - - pub async fn cancel(self) -> anyhow::Result<()> { - match self { - Self::Instant { handle, .. } => handle.cancel().await, - Self::Studio { handle, .. } => handle.cancel().await, - } - } - - pub fn mode(&self) -> RecordingMode { - match self { - Self::Instant { .. } => RecordingMode::Instant, - Self::Studio { .. } => RecordingMode::Studio, - } - } -} - -pub enum CompletedRecording { - Instant { - #[cfg(target_os = "linux")] - finalized: bool, - recording: instant_recording::CompletedRecording, - target_name: String, - segment_upload: InstantUploader, - video_upload_info: VideoUploadInfo, - }, - Studio { - recording: studio_recording::CompletedRecording, - target_name: String, - capture_target: ScreenCaptureTarget, - }, -} - -impl CompletedRecording { - pub fn project_path(&self) -> &PathBuf { - match self { - Self::Instant { recording, .. } => &recording.project_path, - Self::Studio { recording, .. } => &recording.project_path, - } - } - - pub fn target_name(&self) -> &String { - match self { - Self::Instant { target_name, .. } => target_name, - Self::Studio { target_name, .. } => target_name, - } - } -} - -#[tauri::command(async)] -#[specta::specta] -pub async fn list_capture_displays() -> Vec { - screen_capture::list_displays() - .into_iter() - .map(|(v, _)| v) - .collect() -} - -#[tauri::command(async)] -#[specta::specta] -pub async fn list_capture_windows(window: tauri::Window) -> Vec { - let windows = if window.label() == CapWindowId::Settings.label() { - screen_capture::list_excludable_windows() - } else { - screen_capture::list_windows() - }; - - windows.into_iter().map(|(v, _)| v).collect() -} - -#[tauri::command(async)] -#[specta::specta] -pub fn list_cameras() -> Vec { - if !permissions::do_permissions_check(false).camera.permitted() { - return vec![]; - } - cap_camera::list_cameras().collect() -} - -#[derive(Debug, Clone, serde::Serialize, specta::Type)] -#[serde(rename_all = "camelCase")] -pub struct CameraFormatInfo { - pub width: u32, - pub height: u32, - pub frame_rate: f32, -} - -#[derive(Debug, Clone, serde::Serialize, specta::Type)] -#[serde(rename_all = "camelCase")] -pub struct CameraWithFormats { - pub device_id: String, - pub display_name: String, - pub model_id: Option, - pub formats: Vec, - pub best_format: Option, -} - -fn get_best_format(formats: &[CameraFormatInfo]) -> Option { - let preferred_rate = 59.0..=60.0; - let supported_rate = 24.0..=60.0; - - let mut ideal_formats = formats - .iter() - .filter(|f| preferred_rate.contains(&f.frame_rate) && f.width <= 1280 && f.height <= 720) - .collect::>(); - - if ideal_formats.is_empty() { - ideal_formats = formats - .iter() - .filter(|f| preferred_rate.contains(&f.frame_rate) && f.width < 2000 && f.height < 2000) - .collect(); - } - - if ideal_formats.is_empty() { - ideal_formats = formats - .iter() - .filter(|f| { - supported_rate.contains(&f.frame_rate) && f.width <= 1280 && f.height <= 720 - }) - .collect(); - } - - if ideal_formats.is_empty() { - ideal_formats = formats - .iter() - .filter(|f| supported_rate.contains(&f.frame_rate) && f.width < 2000 && f.height < 2000) - .collect(); - } - - if ideal_formats.is_empty() { - ideal_formats = formats.iter().collect(); - } - - ideal_formats.sort_by(|a, b| { - let target_aspect_ratio = 16.0 / 9.0; - let aspect_ratio_a = a.width as f32 / a.height as f32; - let aspect_ratio_b = b.width as f32 / b.height as f32; - let aspect_cmp_a = (aspect_ratio_a - target_aspect_ratio).abs(); - let aspect_cmp_b = (aspect_ratio_b - target_aspect_ratio).abs(); - let resolution_cmp = (a.width * a.height).cmp(&(b.width * b.height)); - let fr_cmp_a = (a.frame_rate - 60.0).abs(); - let fr_cmp_b = (b.frame_rate - 60.0).abs(); - - aspect_cmp_a - .partial_cmp(&aspect_cmp_b) - .unwrap_or(std::cmp::Ordering::Equal) - .then(resolution_cmp.reverse()) - .then( - fr_cmp_a - .partial_cmp(&fr_cmp_b) - .unwrap_or(std::cmp::Ordering::Equal), - ) - }); - - ideal_formats.into_iter().next().cloned() -} - -#[tauri::command(async)] -#[specta::specta] -pub fn get_camera_formats(device_id: String) -> Option { - if !permissions::do_permissions_check(false).camera.permitted() { - return None; - } - - cap_camera::list_cameras() - .find(|c| c.device_id() == device_id) - .map(|camera| { - let formats: Vec = camera - .formats() - .unwrap_or_default() - .into_iter() - .map(|f| CameraFormatInfo { - width: f.width(), - height: f.height(), - frame_rate: f.frame_rate(), - }) - .collect(); - - let best_format = get_best_format(&formats); - - CameraWithFormats { - device_id: camera.device_id().to_string(), - display_name: camera.display_name().to_string(), - model_id: camera.model_id().map(|m| m.to_string()), - formats, - best_format, - } - }) -} - -#[derive(Debug, Clone, serde::Serialize, specta::Type)] -#[serde(rename_all = "camelCase")] -pub struct MicrophoneInfo { - pub name: String, - pub sample_rate: u32, - pub channels: u16, - pub formats: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, specta::Type)] -#[serde(rename_all = "camelCase")] -pub struct MicrophoneFormatInfo { - pub sample_rate: u32, - pub channels: u16, -} - -#[tauri::command(async)] -#[specta::specta] -pub fn get_microphone_info(name: String) -> Option { - if !permissions::do_permissions_check(false) - .microphone - .permitted() - { - return None; - } - - microphone::MicrophoneFeed::device_with_settings(&name, None).map(|(device, config)| { - let formats = microphone_format_infos(&device); - MicrophoneInfo { - name, - sample_rate: config.sample_rate().0, - channels: config.channels(), - formats, - } - }) -} - -fn microphone_format_infos(device: &cpal::Device) -> Vec { - let Ok(configs) = device.supported_input_configs() else { - return vec![]; - }; - let mut formats = BTreeSet::new(); - - for config in configs { - if ffmpeg_sample_format_for(config.sample_format()).is_none() { - continue; - } - - for sample_rate in [ - config.min_sample_rate().0, - 44_100, - 48_000, - 96_000, - config.max_sample_rate().0, - ] { - if config.min_sample_rate().0 <= sample_rate - && sample_rate <= config.max_sample_rate().0 - { - formats.insert((sample_rate, config.channels())); - } - } - } - - formats - .into_iter() - .map(|(sample_rate, channels)| MicrophoneFormatInfo { - sample_rate, - channels, - }) - .collect() -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub async fn list_displays_with_thumbnails() -> Result, String> { - run_non_send_thumbnail_future(collect_displays_with_thumbnails()) -} - -#[tauri::command] -#[specta::specta] -#[instrument] -pub async fn list_windows_with_thumbnails() -> Result, String> { - run_non_send_thumbnail_future(collect_windows_with_thumbnails()) -} - -fn run_non_send_thumbnail_future(future: F) -> Result -where - F: std::future::Future>, -{ - 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)) - } else { - tauri::async_runtime::block_on(future) - } - })); - - match result { - Ok(result) => result, - Err(panic) => { - let message = crate::panic_payload_message(&panic); - error!(panic = %message, "Suppressed panic while collecting capture thumbnails"); - Err(format!("Failed to collect capture thumbnails: {message}")) - } - } -} - -#[derive(Deserialize, Type, Clone, Debug)] -pub struct StartRecordingInputs { - pub capture_target: ScreenCaptureTarget, - #[serde(default)] - pub capture_system_audio: bool, - pub mode: RecordingMode, - #[serde(default)] - pub organization_id: Option, -} - -fn desktop_recording_defaults( - general_settings: Option<&GeneralSettingsStore>, -) -> cap_recording::RecordingDefaults { - match general_settings { - Some(settings) => cap_recording::RecordingDefaults { - custom_cursor_capture: settings.custom_cursor_capture, - capture_keyboard_events: settings.capture_keyboard_events, - crash_recovery_recording: settings.crash_recovery_recording, - max_fps: settings.max_fps, - studio_recording_quality: settings.studio_recording_quality.into(), - out_of_process_muxer: settings.out_of_process_muxer, - instant_mode_max_resolution: cap_recording::DEFAULT_INSTANT_MODE_MAX_RESOLUTION, - }, - None => cap_recording::RecordingDefaults::default(), - } -} - -#[derive(Deserialize, Type, Serialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase")] -pub enum RecordingInputKind { - Microphone, - Camera, -} - -#[derive(tauri_specta::Event, specta::Type, Clone, Debug, serde::Serialize)] -#[serde(tag = "variant")] -pub enum RecordingEvent { - Countdown { value: u32 }, - Started, - Stopped, - Paused, - Resumed, - Failed { error: String }, - // Emitted when start_recording aborts before any recording exists. Distinct from - // `Failed` because the in-progress window treats `Failed` as "the active recording - // died", which would misreport a healthy recording when a second start is refused. - StartFailed { error: String }, - InputLost { input: RecordingInputKind }, - InputRestored { input: RecordingInputKind }, - Degraded { reason: String }, - Recovered, -} - -/// Every abort path out of `start_recording` must be observable: in the log, and as an -/// event the main window surfaces to the user. The picker overlay that invoked the -/// command is often already closed (or being torn down) when the error comes back, so -/// an error returned to the caller alone can vanish without a trace. -fn notify_recording_start_failed(app: &AppHandle, error: &str) { - error!(%error, "Recording failed to start"); - let _ = RecordingEvent::StartFailed { - error: error.to_string(), - } - .emit(app); -} - -fn recording_start_mode_error(mode: RecordingMode, authenticated: bool) -> Option<&'static str> { - match mode { - RecordingMode::Instant if !authenticated => Some("Please sign in to use instant recording"), - RecordingMode::Screenshot => Some("Use take_screenshot for screenshots"), - RecordingMode::Studio | RecordingMode::Instant => None, - } -} - -const RECORDING_START_CANCELLED: &str = "Recording cancelled before starting."; - -#[derive(Clone, Default)] -struct RecordingStoragePrompt(Arc>>>); - -impl RecordingStoragePrompt { - fn begin(&self) -> Option { - let mut slot = self.0.lock().unwrap(); - if slot.is_some() { - return None; - } - let cancelled = Arc::new(CancellationToken::new()); - *slot = Some(cancelled.clone()); - Some(RecordingStoragePromptLease { - slot: self.clone(), - cancelled, - }) - } - - fn cancel(&self) -> bool { - let slot = self.0.lock().unwrap(); - if let Some(cancelled) = slot.as_ref() { - cancelled.cancel(); - true - } else { - false - } - } -} - -fn recording_storage_prompt(app: &AppHandle) -> RecordingStoragePrompt { - if app.try_state::().is_none() { - app.manage(RecordingStoragePrompt::default()); - } - app.state::().inner().clone() -} - -struct RecordingStoragePromptLease { - slot: RecordingStoragePrompt, - cancelled: Arc, -} - -impl Drop for RecordingStoragePromptLease { - fn drop(&mut self) { - let mut slot = self.slot.0.lock().unwrap(); - if slot - .as_ref() - .is_some_and(|current| Arc::ptr_eq(current, &self.cancelled)) - { - *slot = None; - } - } -} - -struct RecordingStorageEvents { - app: AppHandle, - id: tauri::EventId, -} - -impl Drop for RecordingStorageEvents { - fn drop(&mut self) { - self.app.unlisten(self.id); - } -} - -async fn recording_storage_answer( - cancelled: &CancellationToken, - answer: tokio::sync::oneshot::Receiver, -) -> Result { - tokio::select! { - biased; - _ = cancelled.cancelled() => Err(RECORDING_START_CANCELLED.to_string()), - answer = answer => Ok(answer.unwrap_or(false)), - } -} - -async fn check_recording_start_storage( - directory: &Path, - mut sample: impl FnMut(&Path) -> std::io::Result, - confirm: impl FnOnce(u64) -> F, -) -> Result<(), String> -where - F: std::future::Future>, -{ - use cap_utils::disk_space::{DiskSpaceStatus, RecordingStorage}; - - let mut read = || { - sample(directory).map_err(|error| { - format!( - "Could not check available disk space at {}: {error}", - directory.display() - ) - }) - }; - let status = |available_bytes| { - RecordingStorage { - available_bytes, - recording_bytes: 0, - } - .status() - }; - let exhausted = |bytes| { - format!( - "Not enough disk space to start recording ({:.2} GiB free). Free up space so more than {} MiB is available at {} and try again.", - bytes as f64 / 1_073_741_824.0, - cap_utils::disk_space::RECORDING_DISK_RESERVE_BYTES / (1024 * 1024), - directory.display(), - ) - }; - let bytes = read()?; - match status(bytes) { - DiskSpaceStatus::Ok => Ok(()), - DiskSpaceStatus::Exhausted => Err(exhausted(bytes)), - DiskSpaceStatus::Low => { - if !confirm(bytes).await? { - return Err(RECORDING_START_CANCELLED.to_string()); - } - let bytes = read()?; - if status(bytes) == DiskSpaceStatus::Exhausted { - Err(exhausted(bytes)) - } else { - Ok(()) - } - } - } -} - -async fn cancel_recording_storage_prompt(app: &AppHandle, state: &MutableState<'_, App>) -> bool { - let state = state.read().await; - matches!(state.recording_state, RecordingState::Pending { .. }) - && app - .try_state::() - .is_some_and(|prompt| prompt.cancel()) -} - -#[cfg(any(target_os = "linux", test))] -fn storage_preflight_control_result( - result: Result<(), String>, - has_capture: bool, -) -> Result<(), String> { - match result { - Err(error) if error == RECORDING_START_CANCELLED && !has_capture => Ok(()), - result => result, - } -} - -#[cfg(test)] -mod recording_storage_preflight_tests { - use super::*; - use cap_utils::disk_space::{RECORDING_DISK_RESERVE_BYTES, RECORDING_DISK_WARN_BYTES}; - use std::cell::Cell; - - #[tokio::test] - async fn thresholds_match_recording_storage_policy() { - for (bytes, asks, starts) in [ - (RECORDING_DISK_WARN_BYTES + 1, false, true), - (RECORDING_DISK_WARN_BYTES, true, true), - (RECORDING_DISK_RESERVE_BYTES + 1, true, true), - (RECORDING_DISK_RESERVE_BYTES, false, false), - (0, false, false), - ] { - let prompted = Cell::new(false); - let result = check_recording_start_storage( - Path::new("recordings"), - |_| Ok(bytes), - |_| { - prompted.set(true); - async { Ok(true) } - }, - ) - .await; - assert_eq!(prompted.get(), asks); - assert_eq!(result.is_ok(), starts); - } - } - - #[tokio::test] - async fn confirmation_rechecks_same_recordings_drive() { - let directory = Path::new("external-drive/custom-recordings"); - let reads = Cell::new(0); - check_recording_start_storage( - directory, - |path| { - assert_eq!(path, directory); - reads.set(reads.get() + 1); - Ok(RECORDING_DISK_WARN_BYTES) - }, - |_| async { Ok(true) }, - ) - .await - .unwrap(); - assert_eq!(reads.get(), 2); - } - - #[tokio::test] - async fn confirmation_cannot_override_new_reserve_exhaustion() { - let mut bytes = [RECORDING_DISK_WARN_BYTES, RECORDING_DISK_RESERVE_BYTES].into_iter(); - let result = check_recording_start_storage( - Path::new("recordings"), - |_| Ok(bytes.next().unwrap()), - |_| async { Ok(true) }, - ) - .await; - assert!(result.unwrap_err().contains("more than 512 MiB")); - } - - #[tokio::test] - async fn go_back_skips_second_probe() { - let reads = Cell::new(0); - let result = check_recording_start_storage( - Path::new("recordings"), - |_| { - reads.set(reads.get() + 1); - Ok(RECORDING_DISK_WARN_BYTES) - }, - |_| async { Ok(false) }, - ) - .await; - assert_eq!(result.unwrap_err(), RECORDING_START_CANCELLED); - assert_eq!(reads.get(), 1); - } - - #[tokio::test] - async fn unknown_storage_never_admits_capture() { - for fail_at in [1, 2] { - let mut reads = 0; - let result = check_recording_start_storage( - Path::new("recordings"), - |_| { - reads += 1; - if reads == fail_at { - Err(std::io::Error::from_raw_os_error(5)) - } else { - Ok(RECORDING_DISK_WARN_BYTES) - } - }, - |_| async { Ok(true) }, - ) - .await; - assert!( - result - .unwrap_err() - .starts_with("Could not check available disk space") - ); - } - } - - #[tokio::test] - async fn closed_native_callback_declines() { - let (sender, receiver) = tokio::sync::oneshot::channel(); - drop(sender); - assert!( - !recording_storage_answer(&CancellationToken::new(), receiver) - .await - .unwrap() - ); - } - - #[tokio::test] - async fn cancellation_wins_simultaneous_affirmative() { - let token = CancellationToken::new(); - let (sender, receiver) = tokio::sync::oneshot::channel(); - sender.send(true).unwrap(); - token.cancel(); - assert_eq!( - recording_storage_answer(&token, receiver) - .await - .unwrap_err(), - RECORDING_START_CANCELLED - ); - } - - #[tokio::test] - async fn stop_unblocks_wait_and_late_answer_is_inert() { - let slot = RecordingStoragePrompt::default(); - let lease = slot.begin().unwrap(); - let (sender, receiver) = tokio::sync::oneshot::channel(); - let answer = recording_storage_answer(&lease.cancelled, receiver); - tokio::pin!(answer); - tokio::select! { - biased; - _ = &mut answer => panic!("Prompt resolved without an answer"), - _ = std::future::ready(()) => {}, - } - assert!(slot.cancel()); - assert_eq!(answer.await.unwrap_err(), RECORDING_START_CANCELLED); - assert!(sender.send(true).is_err()); - } - - #[test] - fn repeated_requests_do_not_replace_active_prompt() { - let slot = RecordingStoragePrompt::default(); - let first = slot.begin().unwrap(); - assert!(slot.begin().is_none()); - assert!(slot.cancel()); - assert!(first.cancelled.is_cancelled()); - assert!(slot.begin().is_none()); - drop(first); - let second = slot.begin().unwrap(); - assert!(!second.cancelled.is_cancelled()); - } - - #[test] - fn retired_prompt_cannot_cancel_admitted_recording() { - let slot = RecordingStoragePrompt::default(); - let lease = slot.begin().unwrap(); - let token = lease.cancelled.clone(); - drop(lease); - assert!(!slot.cancel()); - assert!(!token.is_cancelled()); - } - - #[test] - fn stale_drop_does_not_remove_replacement_prompt() { - let slot = RecordingStoragePrompt::default(); - let first = slot.begin().unwrap(); - *slot.0.lock().unwrap() = None; - let second = slot.begin().unwrap(); - drop(first); - assert!(slot.cancel()); - assert!(second.cancelled.is_cancelled()); - } - - #[test] - fn instant_control_normalizes_only_confirmed_pre_capture_cancellation() { - assert!( - storage_preflight_control_result(Err(RECORDING_START_CANCELLED.into()), false).is_ok() - ); - assert_eq!( - storage_preflight_control_result(Err(RECORDING_START_CANCELLED.into()), true) - .unwrap_err(), - RECORDING_START_CANCELLED - ); - assert_eq!( - storage_preflight_control_result(Err("cleanup unconfirmed".into()), false).unwrap_err(), - "cleanup unconfirmed" - ); - assert!(storage_preflight_control_result(Ok(()), true).is_ok()); - } -} - -#[derive(Serialize, Type)] -pub enum RecordingAction { - Started, - InvalidAuthentication, - UpgradeRequired, -} - -const MICROPHONE_INPUT_PROBE_TIMEOUT: Duration = Duration::from_millis(1500); -const CAMERA_INPUT_PROBE_TIMEOUT: Duration = Duration::from_millis(1500); - -pub(crate) fn camera_id_label(id: &camera::DeviceOrModelID) -> String { - match id { - camera::DeviceOrModelID::DeviceID(device_id) => device_id.clone(), - camera::DeviceOrModelID::ModelID(model_id) => format!("{model_id:?}"), - } -} - -fn validate_selected_camera_for_start( - selected_id: Option<&camera::DeviceOrModelID>, - is_available: impl FnOnce(&camera::DeviceOrModelID) -> bool, -) -> anyhow::Result<()> { - if let Some(id) = selected_id - && !is_available(id) - { - return Err(anyhow!( - "Selected camera '{}' is no longer available. Reconnect it or choose another camera before recording.", - camera_id_label(id) - )); - } - Ok(()) -} - -fn selected_microphone_for_start( - selected_label: Option, - available_names: &[String], -) -> anyhow::Result> { - let Some(label) = selected_label else { - return Ok(None); - }; - if !available_names.contains(&label) { - return Err(anyhow!( - "Selected microphone '{label}' is no longer available. Reconnect it or choose another microphone before recording." - )); - } - Ok(Some(label)) -} - -fn camera_lock_matches_id(lock: &CameraFeedLock, selected_id: &camera::DeviceOrModelID) -> bool { - let camera_info = lock.camera_info(); - match selected_id { - camera::DeviceOrModelID::DeviceID(device_id) => camera_info.device_id() == device_id, - camera::DeviceOrModelID::ModelID(model_id) => camera_info.model_id() == Some(model_id), - } -} - -async fn initialize_selected_camera( - camera_feed: &kameo::actor::ActorRef, - id: &camera::DeviceOrModelID, - settings: Option, -) -> anyhow::Result<()> { - let label = camera_id_label(id); - let ready = camera_feed - .ask(camera::SetInput { - id: id.clone(), - settings, - }) - .await - .map_err(|err| anyhow!("Failed to initialize selected camera '{label}': {err}"))?; - - ready.await.map(|_| ()).map_err(|err| match err { - camera::SetInputError::DeviceNotFound => { - anyhow!("Selected camera '{label}' is no longer available") - } - err => anyhow!("Failed to initialize selected camera '{label}': {err}"), - }) -} - -async fn lock_initialized_camera( - camera_feed: &kameo::actor::ActorRef, - id: &camera::DeviceOrModelID, -) -> anyhow::Result { - let label = camera_id_label(id); - match camera_feed.ask(camera::Lock).await { - Ok(lock) if camera_lock_matches_id(&lock, id) => Ok(lock), - Ok(_) => Err(anyhow!( - "Selected camera '{label}' changed during initialization. Select the camera again before recording." - )), - Err(kameo::error::SendError::HandlerError(camera::LockFeedError::NoInput)) => Err(anyhow!( - "Selected camera '{label}' did not become ready after initialization" - )), - Err(err) => Err(anyhow!("Failed to lock selected camera '{label}': {err}")), - } -} - -#[cfg(not(target_os = "macos"))] -async fn validate_camera_receiving( - lock: &CameraFeedLock, - id: &camera::DeviceOrModelID, -) -> anyhow::Result<()> { - let label = camera_id_label(id); - let (tx, rx) = flume::bounded(1); - let remove_sender = tx.clone(); - - tokio::time::timeout(CAMERA_INPUT_PROBE_TIMEOUT, lock.ask(camera::AddSender(tx))) - .await - .map_err(|_| anyhow!("Timed out attaching selected camera '{label}' probe"))? - .map_err(|err| anyhow!("Failed to probe selected camera '{label}': {err}"))?; - - let result = tokio::time::timeout(CAMERA_INPUT_PROBE_TIMEOUT, rx.recv_async()).await; - let _ = tokio::time::timeout( - CAMERA_INPUT_PROBE_TIMEOUT, - lock.ask(camera::RemoveSender(remove_sender)), - ) - .await; - - match result { - Ok(Ok(_)) => Ok(()), - Ok(Err(_)) => Err(anyhow!( - "Selected camera '{label}' stopped before sending a frame" - )), - Err(_) => Err(anyhow!( - "Selected camera '{label}' is not sending video frames" - )), - } -} - -#[cfg(target_os = "macos")] -async fn validate_camera_receiving( - lock: &CameraFeedLock, - id: &camera::DeviceOrModelID, -) -> anyhow::Result<()> { - let label = camera_id_label(id); - let (tx, rx) = flume::bounded(1); - let remove_sender = tx.clone(); - - tokio::time::timeout( - CAMERA_INPUT_PROBE_TIMEOUT, - lock.ask(camera::AddNativeSender(tx)), - ) - .await - .map_err(|_| anyhow!("Timed out attaching selected camera '{label}' probe"))? - .map_err(|err| anyhow!("Failed to probe selected camera '{label}': {err}"))?; - - let result = tokio::time::timeout(CAMERA_INPUT_PROBE_TIMEOUT, rx.recv_async()).await; - let _ = tokio::time::timeout( - CAMERA_INPUT_PROBE_TIMEOUT, - lock.ask(camera::RemoveNativeSender(remove_sender)), - ) - .await; - - match result { - Ok(Ok(_)) => Ok(()), - Ok(Err(_)) => Err(anyhow!( - "Selected camera '{label}' stopped before sending a frame" - )), - Err(_) => Err(anyhow!( - "Selected camera '{label}' is not sending video frames" - )), - } -} - -async fn lock_selected_camera( - camera_feed: &kameo::actor::ActorRef, - selected_id: Option, - selected_settings: Option, - capture_target: &ScreenCaptureTarget, -) -> anyhow::Result>> { - let Some(id) = selected_id else { - if matches!(capture_target, ScreenCaptureTarget::CameraOnly) { - return Err(anyhow!( - "Camera-only recording requires a selected camera. Please select a camera before starting." - )); - } - - return Ok(None); - }; - - crate::permissions::check_camera_access().map_err(anyhow::Error::msg)?; - - let existing_lock = match camera_feed.ask(camera::Lock).await { - Ok(lock) if camera_lock_matches_id(&lock, &id) => Some(lock), - Ok(lock) => { - drop(lock); - tokio::time::sleep(Duration::from_millis(50)).await; - None - } - Err(kameo::error::SendError::HandlerError(camera::LockFeedError::NoInput)) => None, - Err(err) => { - return Err(anyhow!( - "Failed to lock selected camera '{}': {err}", - camera_id_label(&id) - )); - } - }; - - let lock = if let Some(lock) = existing_lock { - lock - } else { - initialize_selected_camera(camera_feed, &id, selected_settings).await?; - lock_initialized_camera(camera_feed, &id).await? - }; - - validate_camera_receiving(&lock, &id).await?; - Ok(Some(Arc::new(lock))) -} - -async fn initialize_selected_microphone( - mic_feed: &kameo::actor::ActorRef, - label: &str, - settings: Option, -) -> anyhow::Result<()> { - let ready = mic_feed - .ask(microphone::SetInput { - label: label.to_string(), - settings, - }) - .await - .map_err(|err| anyhow!("Failed to initialize selected microphone '{label}': {err}"))?; - - ready.await.map(|_| ()).map_err(|err| match err { - microphone::SetInputError::DeviceNotFound => { - anyhow!("Selected microphone '{label}' is no longer available") - } - err => anyhow!("Failed to initialize selected microphone '{label}': {err}"), - }) -} - -async fn lock_initialized_microphone( - mic_feed: &kameo::actor::ActorRef, - label: &str, -) -> anyhow::Result { - match mic_feed.ask(microphone::Lock).await { - Ok(lock) if lock.device_name() == label => Ok(lock), - Ok(_) => Err(anyhow!( - "Selected microphone '{label}' changed during initialization. Select the microphone again before recording." - )), - Err(kameo::error::SendError::HandlerError(microphone::LockFeedError::NoInput)) => Err( - anyhow!("Selected microphone '{label}' did not become ready after initialization"), - ), - Err(err) => Err(anyhow!( - "Failed to lock selected microphone '{label}': {err}" - )), - } -} - -async fn validate_microphone_receiving( - lock: µphone::MicrophoneFeedLock, - label: &str, -) -> anyhow::Result<()> { - let (tx, rx) = flume::bounded(1); - let remove_sender = tx.clone(); - - lock.ask(microphone::AddSender(tx)) - .await - .map_err(|err| anyhow!("Failed to probe selected microphone '{label}': {err}"))?; - - let result = tokio::time::timeout(MICROPHONE_INPUT_PROBE_TIMEOUT, rx.recv_async()).await; - let _ = lock.ask(microphone::RemoveSender(remove_sender)).await; - - match result { - Ok(Ok(_)) => Ok(()), - Ok(Err(_)) => Err(anyhow!( - "Selected microphone '{label}' stopped before sending audio" - )), - Err(_) => Err(anyhow!( - "Selected microphone '{label}' is not sending audio" - )), - } -} - -pub fn format_project_name<'a>( - template: Option<&str>, - target_name: &'a str, - target_kind: &'a str, - recording_mode: RecordingMode, - datetime: Option>, -) -> String { - const DEFAULT_FILENAME_TEMPLATE: &str = "{target_name} ({target_kind}) {date} {time}"; - const MAX_TARGET_NAME_CHARS: usize = 180; - let datetime = datetime.unwrap_or(chrono::Local::now()); - - let truncated_target_name: std::borrow::Cow<'_, str> = - if target_name.chars().count() > MAX_TARGET_NAME_CHARS { - std::borrow::Cow::Owned( - target_name - .chars() - .take(MAX_TARGET_NAME_CHARS) - .collect::() - + "...", - ) - } else { - std::borrow::Cow::Borrowed(target_name) - }; - - lazy_static! { - static ref DATE_REGEX: Regex = Regex::new(r"\{date(?::([^}]+))?\}").unwrap(); - static ref TIME_REGEX: Regex = Regex::new(r"\{time(?::([^}]+))?\}").unwrap(); - static ref MOMENT_REGEX: Regex = Regex::new(r"\{moment(?::([^}]+))?\}").unwrap(); - static ref RANDOM_REGEX: Regex = Regex::new(r"\{random(?::(\d+))?\}").unwrap(); - static ref AC: aho_corasick::AhoCorasick = { - aho_corasick::AhoCorasick::new([ - "{recording_mode}", - "{mode}", - "{target_kind}", - "{target_name}", - ]) - .expect("Failed to build AhoCorasick automaton") - }; - } - let haystack = template.unwrap_or(DEFAULT_FILENAME_TEMPLATE); - let haystack = RANDOM_REGEX.replace_all(haystack, |caps: ®ex::Captures| { - let length = match caps.get(1) { - Some(value) => match value.as_str().parse::() { - Ok(length) => length, - Err(_) => return caps.get(0).unwrap().as_str().to_owned(), - }, - None => 10, - }; - if !(1..=32).contains(&length) { - return caps.get(0).unwrap().as_str().to_owned(); - } - uuid::Uuid::new_v4().simple().to_string()[..length].to_owned() - }); - - // Get recording mode information - let (recording_mode, mode) = match recording_mode { - RecordingMode::Studio => ("Studio", "studio"), - RecordingMode::Instant => ("Instant", "instant"), - RecordingMode::Screenshot => ("Screenshot", "screenshot"), - }; - - let result = AC - .try_replace_all( - &haystack, - &[recording_mode, mode, target_kind, &truncated_target_name], - ) - .expect("AhoCorasick replace should never fail with default configuration"); - - let result = DATE_REGEX.replace_all(&result, |caps: ®ex::Captures| { - datetime - .format( - &caps - .get(1) - .map(|m| m.as_str()) - .map(moment_format_to_chrono) - .unwrap_or(Cow::Borrowed("%Y-%m-%d")), - ) - .to_string() - }); - - let result = TIME_REGEX.replace_all(&result, |caps: ®ex::Captures| { - datetime - .format( - &caps - .get(1) - .map(|m| m.as_str()) - .map(moment_format_to_chrono) - .unwrap_or(Cow::Borrowed("%I:%M %p")), - ) - .to_string() - }); - - let result = MOMENT_REGEX.replace_all(&result, |caps: ®ex::Captures| { - datetime - .format( - &caps - .get(1) - .map(|m| m.as_str()) - .map(moment_format_to_chrono) - .unwrap_or(Cow::Borrowed("%Y-%m-%d %H:%M")), - ) - .to_string() - }); - - result.into_owned() -} - -#[tauri::command] -#[specta::specta] -#[tracing::instrument(name = "recording", skip_all)] -pub async fn start_recording( - app: AppHandle, - state_mtx: MutableState<'_, App>, - inputs: StartRecordingInputs, -) -> Result { - #[cfg(target_os = "linux")] - if inputs.mode != RecordingMode::Instant && linux_instant::current(&app).is_some() { - return Err("Finish the pending Instant cleanup before recording".into()); - } - #[cfg(target_os = "linux")] - if inputs.mode == RecordingMode::Instant && EditorRecordingTarget::current(&app).is_none() { - return linux_instant::start(app, state_mtx, inputs).await; - } - start_recording_inner(app, state_mtx, inputs).await -} - -async fn start_recording_inner( - app: AppHandle, - state_mtx: MutableState<'_, App>, - inputs: StartRecordingInputs, -) -> Result { - let mut inputs = inputs; - - #[cfg(target_os = "linux")] - let has_instant_owner = linux_instant::current(&app).is_some(); - #[cfg(not(target_os = "linux"))] - let has_instant_owner = false; - if EditorRecordingTarget::current(&app).is_some() && !has_instant_owner { - inputs.mode = RecordingMode::Studio; - } - - let requested_state = app.state::(); - let requested_inputs = match requested_state.ready_snapshot() { - Ok(snapshot) => snapshot, - Err(error) => { - notify_recording_start_failed(&app, &error); - return Err(error); - } - }; - #[cfg(target_os = "linux")] - linux_instant::validate_screen_request(&inputs, &requested_inputs).inspect_err(|error| { - notify_recording_start_failed(&app, error); - })?; - let clean_generation = crate::clean_capture::prepare(&app, &inputs, None) - .await - .inspect_err(|error| { - notify_recording_start_failed(&app, error); - })?; - let result = start_recording_prepared( - app.clone(), - state_mtx.clone(), - inputs, - requested_inputs, - clean_generation, - ) - .await; - if !matches!(&result, Ok(RecordingAction::Started)) - && let Some(generation) = clean_generation - { - let mut state = state_mtx.write().await; - if crate::clean_capture::is_current(&app, generation) { - state.clear_pending_recording(); - drop(state); - crate::clean_capture::release(&app, generation, false); - } - } - result -} - -async fn start_recording_prepared( - app: AppHandle, - state_mtx: MutableState<'_, App>, - mut inputs: StartRecordingInputs, - requested_inputs: crate::RequestedInputs, - clean_generation: Option, -) -> Result { - let requested_state = app.state::(); - let mut _input_operation = Some(requested_state.operation.lock().await); - if let Err(error) = requested_state.ready_snapshot() { - notify_recording_start_failed(&app, &error); - return Err(error); - } - if !requested_state.is_current(&requested_inputs) { - let error = "Input selection changed before recording could start. Try recording again." - .to_string(); - notify_recording_start_failed(&app, &error); - return Err(error); - } - - let is_camera_only = matches!(inputs.capture_target, ScreenCaptureTarget::CameraOnly); - - #[cfg(target_os = "linux")] - linux_instant::validate_inputs(&inputs)?; - - if is_camera_only { - inputs.capture_system_audio = false; - } - - { - let mut app_state = state_mtx.write().await; - let pending_result = if let Some(generation) = clean_generation { - if crate::clean_capture::is_current(&app, generation) - && matches!(app_state.recording_state, RecordingState::Pending { .. }) - { - Ok(()) - } else { - Err("Recording preflight was cancelled or superseded".to_string()) - } - } else { - app_state.set_pending_recording(inputs.mode, inputs.capture_target.clone()) - }; - if let Err(error) = pending_result { - drop(app_state); - // Deliberately no clear_pending_recording: the pending/active state that - // caused the refusal belongs to another recording and must survive. - notify_recording_start_failed(&app, &error); - return Err(error); - } - if is_camera_only { - app_state.was_camera_only_recording = true; - } - } - - let storage_generation = crate::clean_capture::generation(&app); - #[cfg(target_os = "linux")] - let storage_instant = linux_instant::current(&app); - - if cfg!(target_os = "linux") && inputs.mode == RecordingMode::Instant { - drop(_input_operation.take()); - } - - let instant_auth = if matches!(inputs.mode, RecordingMode::Instant) { - AuthStore::get(&app).ok().flatten() - } else { - None - }; - if let Some(error) = recording_start_mode_error(inputs.mode, instant_auth.is_some()) { - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed(&app, error); - return Err(error.to_string()); - } - - macro_rules! pending_try { - ($expr:expr, $map_err:expr) => { - match $expr { - Ok(value) => value, - Err(err) => { - let error = ($map_err)(err); - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed(&app, &error); - return Err(error); - } - } - }; - } - - if is_camera_only { - let operation_lock = app.state::(); - let _operation_guard = operation_lock.lock().await; - if let Err(err) = (ShowCapWindow::Camera { centered: true }).show(&app).await { - let error = format!("Failed to show centered camera window: {err}"); - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed(&app, &error); - return Err(error); - } - } - - let general_settings = GeneralSettingsStore::get(&app).ok().flatten(); - let general_settings = general_settings.as_ref(); - - let project_name = format_project_name( - general_settings - .and_then(|s| s.default_project_name_template.clone()) - .as_deref(), - inputs - .capture_target - .title() - .as_deref() - .unwrap_or("Unknown"), - inputs.capture_target.kind_str(), - inputs.mode, - None, - ); - - let filename = project_name.replace(":", "."); - let filename = format!("{}.cap", sanitize_filename::sanitize(&filename)); - - let recordings_base_dir = GeneralSettingsStore::recordings_dir(&app); - - pending_try!(ensure_dir(&recordings_base_dir), |e| format!( - "Failed to create recordings directory: {e}" - )); - - let storage_prompt = recording_storage_prompt(&app) - .begin() - .ok_or(RECORDING_START_CANCELLED)?; - let event_app = app.clone(); - let cancelled = storage_prompt.cancelled.clone(); - let storage_events = RecordingStorageEvents { - app: app.clone(), - id: app.listen(CurrentRecordingChanged::NAME, move |_| { - if crate::clean_capture::generation(&event_app) != storage_generation - || clean_generation.is_some_and(|generation| { - crate::clean_capture::stop_requested(&event_app, generation) - }) - { - cancelled.cancel(); - } - }), - }; - if crate::clean_capture::generation(&app) != storage_generation - || clean_generation - .is_some_and(|generation| crate::clean_capture::stop_requested(&app, generation)) - { - storage_prompt.cancelled.cancel(); - } - let prompt_app = &app; - let prompt_cancelled = &storage_prompt.cancelled; - let storage_work = check_recording_start_storage( - &recordings_base_dir, - cap_utils::disk_space::free_bytes_for_path, - |bytes| async move { - if prompt_cancelled.is_cancelled() { - return Err(RECORDING_START_CANCELLED.to_string()); - } - let (sender, receiver) = tokio::sync::oneshot::channel(); - prompt_app.dialog() - .message(format!( - "Only {:.2} GiB is available on your recordings drive. The recording may stop early if space runs out. Free up space, or record anyway.", - bytes as f64 / 1_073_741_824.0, - )) - .title("Low storage space") - .kind(MessageDialogKind::Warning) - .buttons(MessageDialogButtons::OkCancelCustom( - "Record anyway".to_string(), - "Go back".to_string(), - )) - .show(move |confirmed| { - let _ = sender.send(confirmed); - }); - recording_storage_answer(prompt_cancelled, receiver).await - }, - ); - #[cfg(target_os = "linux")] - let storage_result = if let Some(attempt) = &storage_instant { - attempt.while_active(storage_work).await.map_err(|error| { - if attempt.cancelled() { - RECORDING_START_CANCELLED.to_string() - } else { - error - } - }) - } else { - storage_work.await - }; - #[cfg(not(target_os = "linux"))] - let storage_result = storage_work.await; - { - let mut app_state = state_mtx.write().await; - let owns_pending = matches!(app_state.recording_state, RecordingState::Pending { .. }) - && crate::clean_capture::generation(&app) == storage_generation - && clean_generation - .is_none_or(|generation| crate::clean_capture::is_current(&app, generation)); - #[cfg(target_os = "linux")] - let owns_pending = owns_pending - && match (&storage_instant, linux_instant::current(&app)) { - (Some(expected), Some(current)) => expected.same(¤t), - (None, None) => true, - _ => false, - }; - #[cfg(target_os = "linux")] - let instant_cancelled = storage_instant - .as_ref() - .is_some_and(|attempt| attempt.cancelled()); - #[cfg(not(target_os = "linux"))] - let instant_cancelled = false; - let storage_result = if !owns_pending - || storage_prompt.cancelled.is_cancelled() - || instant_cancelled - || clean_generation - .is_some_and(|generation| crate::clean_capture::stop_requested(&app, generation)) - { - Err(RECORDING_START_CANCELLED.to_string()) - } else if !requested_state.is_current(&requested_inputs) { - Err( - "Input selection changed before recording could start. Try recording again." - .to_string(), - ) - } else { - storage_result.and_then(|()| requested_state.ready_snapshot().map(|_| ())) - }; - // Stop checks Pending under the same App lock before cancelling this lease. - // Retire it here so a successful Stop cannot race admission to capture setup. - drop(storage_prompt); - if let Err(error) = storage_result { - if owns_pending { - app_state.clear_pending_recording(); - } - drop(app_state); - drop(storage_events); - if owns_pending { - notify_recording_start_failed(&app, &error); - } - return Err(error); - } - } - drop(storage_events); - - let project_file_path = recordings_base_dir.join(&pending_try!( - cap_utils::ensure_unique_filename(&filename, &recordings_base_dir,), - |e| e - )); - #[cfg(target_os = "linux")] - if inputs.mode == RecordingMode::Instant - && let Some(attempt) = linux_instant::current(&app) - { - attempt.set_directory(project_file_path.clone()); - if attempt.cancelled() { - return Err("Instant startup cancelled".into()); - } - } - - pending_try!(ensure_dir(&project_file_path), |e| format!( - "Failed to create recording directory: {e}" - )); - if let Some(generation) = clean_generation { - crate::clean_capture::set_start_directory(&app, generation, project_file_path.clone())?; - } - pending_try!( - state_mtx - .write() - .await - .add_recording_logging_handle(&project_file_path.join("recording-logs.log")) - .await, - |e| e - ); - - if let Some(window) = CapWindowId::Camera.get(&app) - && let Err(error) = window.set_content_protected( - matches!(inputs.mode, RecordingMode::Studio) - && !crate::windows::capture_exclusion_hides_ui(), - ) - { - warn!(%error, "Failed to update camera window content protection"); - } - - let (video_upload_info, instant_mode_max_resolution) = match inputs.mode { - RecordingMode::Instant => { - let Some(auth) = instant_auth else { - let error = "Please sign in to use instant recording".to_string(); - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed(&app, &error); - return Err(error); - }; - let instant_mode_max_resolution = if auth.is_upgraded() { - general_settings - .map_or(cap_recording::PRO_INSTANT_MODE_MAX_RESOLUTION, |settings| { - settings.instant_mode_max_resolution - }) - } else { - cap_recording::FREE_INSTANT_MODE_MAX_RESOLUTION - }; - let upload_mode = if matches!(inputs.capture_target, ScreenCaptureTarget::CameraOnly) { - "desktopMP4" - } else { - "desktopSegments" - }; - - let created = match crate::upload::create_or_get_video_with_mode( - &app, - false, - None, - Some(project_name.clone()), - None, - inputs.organization_id.clone(), - upload_mode, - ) - .await - { - Ok(meta) => meta, - Err(AuthedApiError::InvalidAuthentication) => { - state_mtx.write().await.clear_pending_recording(); - // Returned as an action rather than an error, but the picker that - // invoked us may already be gone — surface it as a start failure too. - notify_recording_start_failed( - &app, - "Your session has expired. Please sign in again to use instant recording.", - ); - return Ok(RecordingAction::InvalidAuthentication); - } - Err(AuthedApiError::UpgradeRequired) => { - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed( - &app, - "Instant recording requires an upgraded plan.", - ); - return Ok(RecordingAction::UpgradeRequired); - } - Err(err) => { - let error = format!("Could not create the shareable link: {err}"); - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed(&app, &error); - return Err(error); - } - }; - - let link = created.link(&app).await; - let s3_config = created.config; - info!("Pre-created shareable link: {}", link); - - ( - Some(VideoUploadInfo { - id: s3_config.id.to_string(), - link: link.clone(), - config: s3_config, - }), - instant_mode_max_resolution, - ) - } - RecordingMode::Studio => (None, cap_recording::PRO_INSTANT_MODE_MAX_RESOLUTION), - RecordingMode::Screenshot => { - let error = "Use take_screenshot for screenshots".to_string(); - state_mtx.write().await.clear_pending_recording(); - notify_recording_start_failed(&app, &error); - return Err(error); - } - }; - - let meta = RecordingMeta { - platform: Some(Platform::default()), - project_path: project_file_path.clone(), - pretty_name: project_name.clone(), - inner: match inputs.mode { - RecordingMode::Studio => { - RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::MultipleSegments { - inner: MultipleSegments { - segments: Default::default(), - cursors: Default::default(), - status: Some(StudioRecordingStatus::InProgress), - }, - })) - } - RecordingMode::Instant => { - RecordingMetaInner::Instant(InstantRecordingMeta::InProgress { recording: true }) - } - RecordingMode::Screenshot => { - state_mtx.write().await.clear_pending_recording(); - return Err("Use take_screenshot for screenshots".to_string()); - } - }, - sharing: None, - upload: None, - }; - - pending_try!(meta.save_for_project(), |e| format!( - "Failed to save recording meta: {e}" - )); - - if clean_generation.is_none() { - match &inputs.capture_target { - ScreenCaptureTarget::Window { id: _id } => { - if let Some(show) = inputs - .capture_target - .display() - .map(|d| ShowCapWindow::WindowCaptureOccluder { screen_id: d.id() }) - { - let _ = show.show(&app).await; - } - } - ScreenCaptureTarget::Area { screen, .. } => { - let _ = ShowCapWindow::WindowCaptureOccluder { - screen_id: screen.clone(), - } - .show(&app) - .await; - } - _ => {} - } - } - let countdown = general_settings.and_then(|v| v.recording_countdown); - crate::target_select_overlay::close_target_select_overlay_windows(&app); - if clean_generation.is_none() { - let _ = ShowCapWindow::InProgressRecording { - countdown, - capture_target: Some(inputs.capture_target.clone()), - } - .show(&app) - .await; - - if let Some(window) = CapWindowId::Main.get(&app) { - let _ = general_settings - .map(|v| v.main_window_recording_start_behaviour) - .unwrap_or_default() - .perform(&window); - } - } - let start_gate = cap_recording::RecordingStartGate::new(); - let start_cue = crate::audio::prime_recording_start_sound(); - let start_cancelled: Arc> = Arc::default(); - crate::windows::apply_content_protection(&app, true); - - if let Some(editor_target) = EditorRecordingTarget::current(&app) - && let Some(editor_window) = editor_window_for_path(&app, &editor_target) - { - let _ = editor_window.set_content_protected(!crate::windows::capture_exclusion_hides_ui()); - let _ = editor_window.minimize(); - } - - let start_cancel_reason = { - let app = app.clone(); - #[cfg(target_os = "linux")] - let mode = inputs.mode; - move || -> Option<&'static str> { - if clean_generation - .is_some_and(|generation| crate::clean_capture::stop_requested(&app, generation)) - { - return Some("Recording cancelled"); - } - #[cfg(target_os = "linux")] - if mode == RecordingMode::Instant - && linux_instant::current(&app).is_none_or(|attempt| attempt.cancelled()) - { - return Some("Instant startup cancelled"); - } - None - } - }; - - let countdown = countdown.unwrap_or(0); - // Every countdown second but the last elapses before the pipeline is - // primed; the last one overlaps its warm-up so capture is live at the cue. - for t in 0..countdown.saturating_sub(1) { - if let Some(reason) = start_cancel_reason() { - return Err(reason.into()); - } - let _ = RecordingEvent::Countdown { - value: countdown - t, - } - .emit(&app); - countdown_tick(&start_cancel_reason).await?; - } - - let start_cue_flow = { - let app = app.clone(); - let start_gate = start_gate.clone(); - let start_cancelled = start_cancelled.clone(); - let start_cancel_reason = start_cancel_reason.clone(); - async move { - if countdown >= 1 { - let _ = RecordingEvent::Countdown { value: 1 }.emit(&app); - if let Err(reason) = countdown_tick(&start_cancel_reason).await { - let _ = start_cancelled.set(reason); - start_gate.arm(); - return; - } - let _ = RecordingEvent::Countdown { value: 0 }.emit(&app); - } - let cue = crate::audio::play_recording_start_sound(start_cue, start_gate); - tokio::pin!(cue); - loop { - tokio::select! { - _ = &mut cue => break, - _ = tokio::time::sleep(Duration::from_millis(50)) => {} - } - if let Some(reason) = start_cancel_reason() { - let _ = start_cancelled.set(reason); - break; - } - } - } - }; - - let (finish_upload_tx, finish_upload_rx) = flume::bounded(1); - #[cfg(target_os = "linux")] - drop(finish_upload_rx); - - if _input_operation.is_none() { - _input_operation = Some(requested_state.operation.lock().await); - if !requested_state.is_current(&requested_inputs) { - return Err("Input selection changed during startup".into()); - } - } - - debug!("spawning start_recording actor"); - - let app_handle = app.clone(); - let actor_task = { - let state_mtx = Arc::clone(&state_mtx); - let general_settings = general_settings.cloned(); - let recording_dir = project_file_path.clone(); - let inputs = inputs.clone(); - let start_gate = start_gate.clone(); - let start_cancelled = start_cancelled.clone(); - async move { - fail!("recording::spawn_actor"); - - let (camera_feed_actor, selected_camera_id, selected_camera_settings) = { - let state = state_mtx.read().await; - let selected_camera_settings = - requested_inputs.camera.value.as_ref().and_then(|id| { - crate::recording_settings::RecordingSettingsStore::camera_settings_for( - &state.handle, - id, - ) - }); - ( - state.camera_feed.clone(), - requested_inputs.camera.value.clone(), - selected_camera_settings, - ) - }; - - crate::check_requested_camera_permission( - selected_camera_id.as_ref(), - crate::permissions::check_camera_access, - ) - .map_err(anyhow::Error::msg)?; - validate_selected_camera_for_start( - selected_camera_id.as_ref(), - crate::is_camera_available, - )?; - - let camera_feed = lock_selected_camera( - &camera_feed_actor, - selected_camera_id, - selected_camera_settings, - &inputs.capture_target, - ) - .await?; - debug!( - camera_selected = camera_feed.is_some(), - "Selected camera locked for recording" - ); - - let has_camera_feed = camera_feed.is_some(); - - #[cfg(target_os = "macos")] - let mut shareable_content = match inputs.capture_target { - ScreenCaptureTarget::CameraOnly => None, - _ => { - debug!("Acquiring shareable content for recording target"); - let content = - acquire_shareable_content_for_target(&inputs.capture_target).await?; - debug!("Acquired shareable content for recording target"); - Some(content) - } - }; - - let health = crate::recording_telemetry::RecordingHealthAccumulator::new(); - let common = InProgressRecordingCommon { - target_name: project_name, - inputs: inputs.clone(), - recording_dir: recording_dir.clone(), - health, - camera_snapshot: Arc::new(std::sync::OnceLock::new()), - }; - - #[cfg(target_os = "macos")] - let excluded_windows = { - let window_exclusions = general_settings - .as_ref() - .map_or_else(general_settings::default_excluded_windows, |settings| { - settings.excluded_windows.clone() - }); - - let window_exclusions = if matches!(inputs.mode, RecordingMode::Instant) { - let camera_title = CapWindowId::Camera.title(); - crate::window_exclusion::filter_for_instant_mode( - window_exclusions, - &camera_title, - ) - } else { - window_exclusions - }; - - let teleprompter_exclusion = WindowExclusion { - bundle_identifier: None, - owner_name: None, - window_title: Some(CapWindowId::Teleprompter.title()), - }; - let mut window_exclusions = window_exclusions; - if !window_exclusions.contains(&teleprompter_exclusion) { - window_exclusions.push(teleprompter_exclusion); - } - - let mut excluded_window_ids = - crate::window_exclusion::resolve_window_ids(&window_exclusions); - crate::window_exclusion::append_matching_webview_window_ids( - &mut excluded_window_ids, - &app_handle, - &window_exclusions, - ); - info!( - configured_exclusions = window_exclusions.len(), - resolved_window_ids = excluded_window_ids.len(), - "Resolved macOS recording window exclusions" - ); - excluded_window_ids - }; - - let mut mic_restart_attempts = 0; - - let (done_fut, health_rx, automatic_stop) = loop { - let actor_result: Result = async { - if !app_handle - .state::() - .is_current(&requested_inputs) - { - return Err(anyhow!( - "Input selection changed during recording startup. Try recording again." - )); - } - let selected_mic_label = match requested_inputs.microphone.value.clone() { - Some(label) => selected_microphone_for_start( - Some(label), - µphone::MicrophoneFeed::list_names(), - )?, - None => None, - }; - let (mic_actor, selected_mic_settings) = { - let mut state = state_mtx.write().await; - let settings = selected_mic_label - .as_ref() - .and_then(|label| state.microphone_settings_for_label(label)); - state.applied_mic_input.invalidate(); - (state.mic_feed.clone(), settings) - }; - debug!( - mic_selected = selected_mic_label.is_some(), - "Locking selected microphone for recording" - ); - let mic_feed = lock_selected_microphone( - &mic_actor, - selected_mic_label, - selected_mic_settings, - ) - .await?; - debug!( - mic_selected = mic_feed.is_some(), - "Selected microphone locked for recording" - ); - let defaults = desktop_recording_defaults(general_settings.as_ref()); - - match inputs.mode { - RecordingMode::Studio => { - let mut builder = defaults.apply_to_studio_builder( - studio_recording::Actor::builder( - recording_dir.clone(), - inputs.capture_target.clone(), - ) - .with_system_audio(inputs.capture_system_audio), - camera_feed.is_some(), - None, - ); - - builder = builder.with_start_gate(start_gate.clone()); - - #[cfg(target_os = "macos")] - { - builder = builder.with_excluded_windows(excluded_windows.clone()); - } - - if let Some(camera_feed) = camera_feed.clone() { - builder = builder.with_camera_feed(camera_feed); - } - - if let Some(mic_feed) = mic_feed.clone() { - builder = builder.with_mic_feed(mic_feed); - } - - debug!("Building studio recording actor"); - let handle = builder - .build( - #[cfg(target_os = "macos")] - shareable_content.clone(), - ) - .await - .map_err(|e| { - error!("Failed to spawn studio recording actor: {e:#}"); - e - })?; - - debug!("Studio recording actor built"); - Ok(InProgressRecording::Studio { - handle, - common: common.clone(), - mic_feed: mic_feed.clone(), - camera_feed: camera_feed.clone(), - }) - } - RecordingMode::Instant => { - let Some(video_upload_info) = video_upload_info.clone() else { - return Err(anyhow!("Video upload info not found")); - }; - - let mut builder = instant_recording::Actor::builder( - recording_dir.clone(), - inputs.capture_target.clone(), - ) - .with_system_audio(inputs.capture_system_audio) - .with_max_output_size(instant_mode_max_resolution); - - builder = builder.with_start_gate(start_gate.clone()); - - #[cfg(target_os = "macos")] - { - builder = builder.with_excluded_windows(excluded_windows.clone()); - } - - if let Some(camera_feed) = camera_feed.clone() { - builder = builder.with_camera_feed(camera_feed); - } - - if let Some(mic_feed) = mic_feed.clone() { - builder = builder.with_mic_feed(mic_feed); - } - - #[cfg(target_os = "linux")] - let attempt = linux_instant::current(&app_handle) - .ok_or_else(|| anyhow!("Instant startup owner was lost"))?; - #[cfg(target_os = "linux")] - attempt - .attach(recording_dir.clone(), builder.lifecycle()) - .await - .map_err(anyhow::Error::msg)?; - #[cfg(target_os = "linux")] - { - let prepared = match clean_generation { - Some(generation) => linux_instant::prepare_screen_camera( - &app_handle, - &attempt, - generation, - &inputs, - &requested_inputs, - camera_feed.clone(), - ) - .await - .map_err(anyhow::Error::msg)?, - None => None, - }; - builder = linux_instant::configure_screen_camera( - builder, - &inputs.capture_target, - requested_inputs.camera.value.is_some(), - prepared, - ) - .map_err(anyhow::Error::msg)?; - attempt.checked(Ok(())).map_err(anyhow::Error::msg)?; - } - let upload_session = crate::upload::lifecycle::prepare( - &app_handle, - &recording_dir, - &video_upload_info.id, - inputs.capture_system_audio || mic_feed.is_some(), - ) - .await - .map_err(anyhow::Error::from)?; - let handle = builder - .build( - #[cfg(target_os = "macos")] - shareable_content.clone(), - ) - .await - .map_err(|e| { - error!("Failed to spawn instant recording actor: {e:#}"); - e - })?; - - #[cfg(target_os = "linux")] - let handle = Arc::new(handle); - #[cfg(target_os = "linux")] - let segment_upload = { - let events = handle.take_segment_rx(); - linux_instant::persist_upload_start( - &recording_dir, - &video_upload_info, - events.is_some(), - ) - .map_err(anyhow::Error::msg)?; - attempt.upload_started(); - Arc::new(tokio::sync::Mutex::new( - crate::upload::strict_instant::spawn( - app_handle.clone(), - upload_session, - video_upload_info.clone(), - events, - attempt.upload(), - inputs.capture_system_audio || mic_feed.is_some(), - ), - )) - }; - #[cfg(not(target_os = "linux"))] - let segment_upload = { - let segment_rx = handle.take_segment_rx(); - - if let Some(rx) = segment_rx { - SegmentUploader::spawn( - app_handle.clone(), - rx, - Some(finish_upload_rx.clone()), - upload_session, - video_upload_info.clone(), - inputs.capture_system_audio || mic_feed.is_some(), - ) - } else { - let progressive_upload = InstantMultipartUpload::spawn( - app_handle.clone(), - recording_dir.join("content/output.mp4"), - video_upload_info.clone(), - upload_session, - Some(finish_upload_rx.clone()), - inputs.capture_system_audio || mic_feed.is_some(), - ); - SegmentUploader { - handle: progressive_upload.handle, - session: progressive_upload.session, - } - } - }; - - Ok(InProgressRecording::Instant { - handle, - segment_upload, - video_upload_info, - common: common.clone(), - mic_feed: mic_feed.clone(), - camera_feed: camera_feed.clone(), - }) - } - RecordingMode::Screenshot => Err(anyhow!( - "Screenshot mode should be handled via take_screenshot" - )), - } - } - .await; - - match actor_result { - Ok(mut actor) => { - // The recording stays out of app state until the cue has - // armed the gate, so nothing reports "recording" while the - // primed pipeline is still discarding frames. - while !start_gate.is_armed() && start_cancelled.get().is_none() { - tokio::time::sleep(Duration::from_millis(10)).await; - } - let mut state = state_mtx.write().await; - if let Some(reason) = start_cancelled.get().copied() { - drop(state); - let _ = cancel_discarded_recording(&app_handle, actor).await; - return Err(anyhow!(reason)); - } - if clean_generation.is_some_and(|generation| { - !crate::clean_capture::is_current(&app_handle, generation) - }) || !matches!(state.recording_state, RecordingState::Pending { .. }) - { - drop(state); - let _ = cancel_discarded_recording(&app_handle, actor).await; - return Err(anyhow!("Recording startup was cancelled or superseded")); - } - #[cfg(target_os = "linux")] - if inputs.mode == RecordingMode::Instant - && (linux_instant::current(&app_handle).is_none_or(|attempt| { - attempt.cancelled() || !attempt.owns_directory(&recording_dir) - }) || clean_generation.is_some_and(|generation| { - crate::clean_capture::stop_requested(&app_handle, generation) - }) || !app_handle - .state::() - .is_current(&requested_inputs)) - { - drop(state); - let _ = cancel_discarded_recording(&app_handle, actor).await; - return Err(anyhow!("Instant startup was cancelled")); - } - let done_fut = actor.done_fut(); - let health_rx = actor.take_health_rx(); - let mut candidate = Some(actor); - let published = app_handle - .state::() - .publish_if_current(&requested_inputs, || { - state.selected_mic_label = - requested_inputs.microphone.value.clone(); - state.selected_camera_id = requested_inputs.camera.value.clone(); - state.camera_in_use = has_camera_feed; - state.applied_mic_input.confirm(); - state.set_current_recording(candidate.take().unwrap()); - if let Some(generation) = clean_generation { - crate::clean_capture::publish( - &app_handle, - generation, - recording_dir.clone(), - ); - } - }); - if !published { - drop(state); - let _ = - cancel_discarded_recording(&app_handle, candidate.take().unwrap()) - .await; - return Err(anyhow!( - "Input selection changed during recording startup. Try recording again." - )); - } - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - let automatic_stop = match state.current_recording() { - Some(InProgressRecording::Studio { handle, common, .. }) => { - Some(enroll_studio_stop( - &app_handle, - &state_mtx, - handle, - &common.recording_dir, - crate::clean_capture::owner(&app_handle, &common.recording_dir), - StudioStopOrigin::Automatic, - )) - } - _ => None, - }; - #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - let automatic_stop = (); - break (done_fut, health_rx, automatic_stop); - } - #[cfg(target_os = "macos")] - Err(err) if is_shareable_content_error(&err) => { - shareable_content = Some( - acquire_shareable_content_for_target(&inputs.capture_target).await?, - ); - continue; - } - Err(err) - if mic_restart_attempts < 3 - && !(inputs.mode == RecordingMode::Instant - && clean_generation.is_some()) - && (mic_actor_not_running(&err) || mic_feed_locked(&err)) => - { - mic_restart_attempts += 1; - warn!( - attempt = mic_restart_attempts, - error = %err, - "Recovering microphone feed before retrying recording start" - ); - if clean_generation.is_some() { - if mic_feed_locked(&err) { - tokio::time::sleep(Duration::from_millis(50)).await; - continue; - } - return Err(anyhow!( - "The selected microphone stopped during startup. Reselect it before recording: {err:#}" - )); - } - state_mtx - .write() - .await - .restart_mic_feed() - .await - .map_err(|restart_err| anyhow!(restart_err))?; - tokio::time::sleep(Duration::from_millis(250)).await; - } - Err(err) => return Err(err), - } - }; - - Ok::<_, anyhow::Error>((done_fut, health_rx, automatic_stop)) - } - }; - - let (actor_task_res, ()) = - futures::join!(AssertUnwindSafe(actor_task).catch_unwind(), start_cue_flow); - - let (actor_done_fut, health_rx, automatic_stop) = match actor_task_res { - Ok(Ok(v)) => v, - Ok(Err(err)) - if start_cancelled - .get() - .is_some_and(|reason| err.to_string() == *reason) => - { - return Err(err.to_string()); - } - Ok(Err(err)) => { - let message = format!("{err:#}"); - handle_spawn_failure( - &app, - &state_mtx, - project_file_path.as_path(), - message.clone(), - ) - .await?; - return Err(message); - } - Err(panic) => { - let panic_msg = panic_message(panic); - let message = format!("Failed to spawn recording actor: {panic_msg}"); - handle_spawn_failure( - &app, - &state_mtx, - project_file_path.as_path(), - message.clone(), - ) - .await?; - return Err(message); - } - }; - - let (watcher_started_tx, watcher_started_rx) = tokio::sync::oneshot::channel::<()>(); - spawn_actor({ - let app = app.clone(); - let state_mtx = Arc::clone(&state_mtx); - let project_file_path = project_file_path.clone(); - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - let instant_watch = inputs.mode == RecordingMode::Instant; - async move { - let _ = watcher_started_rx.await; - fail!("recording::wait_actor_done"); - let disposition = { - let res = actor_done_fut.await; - #[cfg(target_os = "linux")] - if instant_watch { - if let Some(attempt) = linux_instant::current(&app) - && attempt.owns_directory(&project_file_path) - && attempt.terminal_needs_cleanup() - { - let _ = linux_instant::control(app.clone(), attempt, false).await; - } - return; - } - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - if !instant_watch { - let terminal_started = { - let state = state_mtx.read().await; - let Some(InProgressRecording::Studio { handle, common, .. }) = - state.current_recording() - else { - return; - }; - if common.recording_dir != project_file_path - || automatic_stop.as_ref().is_none_or(|participant| { - !participant.cohort.identity.matches( - handle, - &common.recording_dir, - crate::clean_capture::owner(&app, &common.recording_dir), - ) - }) - { - return; - } - #[cfg(target_os = "linux")] - let started = handle.lifecycle().terminal_started(); - #[cfg(any(target_os = "macos", windows))] - let started = handle.terminal_started(); - started - }; - if clean_generation.is_some_and(|generation| { - crate::clean_capture::owner(&app, &project_file_path) != Some(generation) - }) { - return; - } - let follow_stop = automatic_stop.as_ref().is_some_and(|participant| { - participant.cohort.flight.lock().unwrap().explicit_seen - }) || matches!( - crate::clean_capture::phase(&app), - Some( - crate::clean_capture::Phase::Stopping - | crate::clean_capture::Phase::Restoring - ) - ); - if terminal_started && !follow_stop { - return; - } - let failure = if follow_stop { - res.err().map(|error| error.to_string()) - } else { - match classify_actor_done_result(res, true) { - ActorDoneDisposition::UnexpectedStop { error } - | ActorDoneDisposition::Failed { error } => Some(error), - ActorDoneDisposition::UserInitiatedStop => None, - } - }; - if let Some(completion) = control_studio_recording( - &app, - &state_mtx, - Some(&project_file_path), - StudioTerminalAction::Stop, - failure, - automatic_stop, - ) - .await - { - completion.present_automatic(); - } - return; - } - if let Some(generation) = clean_generation - && (crate::clean_capture::owner(&app, &project_file_path) != Some(generation) - || matches!( - crate::clean_capture::phase(&app), - Some( - crate::clean_capture::Phase::Stopping - | crate::clean_capture::Phase::Restoring - ) - )) - { - return; - } - info!("recording wait actor done: {:?}", &res); - let recording_still_active = matches!( - state_mtx.read().await.recording_state, - RecordingState::Active(_) - ); - classify_actor_done_result(res, recording_still_active) - }; - match disposition { - ActorDoneDisposition::UserInitiatedStop => { - let _ = finish_upload_tx.send(()); - let _ = RecordingEvent::Stopped.emit(&app); - } - ActorDoneDisposition::UnexpectedStop { error } - | ActorDoneDisposition::Failed { error } => { - let mut state = state_mtx.write().await; - if let Some(generation) = clean_generation - && (crate::clean_capture::owner(&app, &project_file_path) - != Some(generation) - || state.current_recording().is_none_or(|recording| { - recording.recording_dir() != &project_file_path - }) - || matches!( - crate::clean_capture::phase(&app), - Some( - crate::clean_capture::Phase::Stopping - | crate::clean_capture::Phase::Restoring - ) - )) - { - return; - } - - let _ = RecordingEvent::Failed { - error: error.clone(), - } - .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); - } - - if crate::clean_capture::phase(&app).is_none() { - dialog.blocking_show(); - } - - handle_recording_end(app, Err(error), &mut state, project_file_path) - .await - .ok(); - } - } - } - }); - - drop(_input_operation); - if clean_generation - .is_some_and(|generation| crate::clean_capture::stop_requested(&app, generation)) - { - #[cfg(target_os = "linux")] - if inputs.mode == RecordingMode::Instant { - if let Some(attempt) = linux_instant::current(&app) { - attempt.cancel(); - } - return Err("Instant startup cancelled".into()); - } - if inputs.mode == RecordingMode::Studio { - if let Some(generation) = clean_generation - && let Some(identity) = - studio_stop_registry(&app).active_identity(&project_file_path, generation) - { - Box::pin(stop_clean_studio_recording( - app.clone(), - identity.handle, - generation, - project_file_path.clone(), - )) - .await?; - } - } else { - Box::pin(stop_recording(app.clone(), state_mtx.clone())).await?; - } - return Ok(RecordingAction::Started); - } - - if matches!(inputs.mode, RecordingMode::Studio) { - spawn_current_desktop_background_snapshot( - project_file_path.clone(), - inputs.capture_target.clone(), - ); - } - - let _ = RecordingEvent::Started.emit(&app); - let _ = RecordingStarted.emit(&app); - let _ = watcher_started_tx.send(()); - - emit_recording_started_telemetry(&app, &state_mtx).await; - - if let Some(mut health_rx) = health_rx { - let accumulator_mode = { - let state = state_mtx.read().await; - state - .current_recording() - .map(|r| (r.common().health.clone(), r.inputs().mode)) - }; - - spawn_actor({ - let app = app.clone(); - async move { - let mut is_degraded = false; - while let Some(event) = health_rx.recv().await { - if let Some((health, mode)) = accumulator_mode.as_ref() - && let Some((reason_text, critical)) = health.record_event(&event) - { - use crate::recording_telemetry::{CriticalEvent, mode_label}; - use crate::telemetry::{AnalyticsEvent, async_capture_event}; - match critical { - CriticalEvent::MuxerCrashed { - seconds_into_recording, - .. - } => { - async_capture_event( - &app, - AnalyticsEvent::RecordingMuxerCrashed { - mode: mode_label(*mode), - reason: reason_text, - seconds_into_recording, - }, - ); - } - CriticalEvent::AudioDegraded { - seconds_into_recording, - .. - } => { - async_capture_event( - &app, - AnalyticsEvent::RecordingAudioDegraded { - mode: mode_label(*mode), - reason: reason_text, - seconds_into_recording, - }, - ); - } - } - } - - if let Some((_, mode)) = accumulator_mode.as_ref() { - use crate::recording_telemetry::mode_label; - use crate::telemetry::{AnalyticsEvent, async_capture_event}; - let mode_str = mode_label(*mode); - match &event { - cap_recording::PipelineHealthEvent::DiskSpaceLow { - bytes_remaining, - .. - } => async_capture_event( - &app, - AnalyticsEvent::RecordingDiskSpaceLow { - mode: mode_str, - bytes_remaining: *bytes_remaining, - }, - ), - cap_recording::PipelineHealthEvent::DiskSpaceExhausted { - bytes_remaining, - } => async_capture_event( - &app, - AnalyticsEvent::RecordingDiskSpaceExhausted { - mode: mode_str, - bytes_remaining: *bytes_remaining, - }, - ), - cap_recording::PipelineHealthEvent::DeviceLost { subsystem } => { - async_capture_event( - &app, - AnalyticsEvent::RecordingDeviceLost { - mode: mode_str, - subsystem: subsystem.clone(), - }, - ) - } - cap_recording::PipelineHealthEvent::EncoderRebuilt { - backend, - attempt, - } => async_capture_event( - &app, - AnalyticsEvent::RecordingEncoderRebuilt { - mode: mode_str, - backend: backend.clone(), - attempt: *attempt, - }, - ), - cap_recording::PipelineHealthEvent::SourceAudioReset { - source, - starvation_ms, - } => async_capture_event( - &app, - AnalyticsEvent::RecordingSourceAudioReset { - mode: mode_str, - source: source.clone(), - starvation_ms: *starvation_ms, - }, - ), - cap_recording::PipelineHealthEvent::CaptureTargetLost { target } => { - async_capture_event( - &app, - AnalyticsEvent::RecordingCaptureTargetLost { - mode: mode_str, - target: target.clone(), - }, - ) - } - cap_recording::PipelineHealthEvent::RecoveryFragmentCorrupt { - .. - } => {} - _ => {} - } - } - - let reason = match &event { - cap_recording::PipelineHealthEvent::FrameDropRateHigh { - source, - rate_pct, - } => Some(format!("High frame drop rate on {source}: {rate_pct:.0}%")), - cap_recording::PipelineHealthEvent::AudioGapDetected { gap_ms } => { - Some(format!("Audio gap detected: {gap_ms}ms")) - } - cap_recording::PipelineHealthEvent::SourceRestarting => { - Some("Capture source restarting".to_string()) - } - cap_recording::PipelineHealthEvent::AudioDegradedToVideoOnly { reason } => { - Some(format!("Audio lost: {reason}")) - } - cap_recording::PipelineHealthEvent::Stalled { source, waited_ms } => { - Some(format!("Pipeline stalled on {source} ({waited_ms}ms)")) - } - cap_recording::PipelineHealthEvent::MuxerCrashed { reason } => { - Some(format!("Muxer crashed: {reason}")) - } - cap_recording::PipelineHealthEvent::DiskSpaceLow { - bytes_remaining, - .. - } => Some(format!( - "Low disk space: {:.2} GB remaining", - *bytes_remaining as f64 / 1_073_741_824.0 - )), - cap_recording::PipelineHealthEvent::DiskSpaceExhausted { - bytes_remaining, - } => Some(format!( - "Disk full: {:.2} GB remaining", - *bytes_remaining as f64 / 1_073_741_824.0 - )), - cap_recording::PipelineHealthEvent::DeviceLost { subsystem } => { - Some(format!("Graphics device lost: {subsystem}")) - } - cap_recording::PipelineHealthEvent::EncoderRebuilt { backend, attempt } => { - Some(format!("Encoder rebuilt: {backend} (attempt {attempt})")) - } - cap_recording::PipelineHealthEvent::SourceAudioReset { - source, - starvation_ms, - } => Some(format!("Audio source reset: {source} ({starvation_ms}ms)")), - cap_recording::PipelineHealthEvent::RecoveryFragmentCorrupt { - path, - reason, - } => Some(format!( - "Corrupt recovery fragment skipped: {path} ({reason})" - )), - cap_recording::PipelineHealthEvent::CaptureTargetLost { target } => { - Some(format!("Capture target lost: {target}")) - } - cap_recording::PipelineHealthEvent::MicrophoneDigitalSilence => Some( - "Microphone is recording silence. Another app, like a call, may be using it." - .to_string(), - ), - cap_recording::PipelineHealthEvent::SourceRestarted => None, - }; - - if let Some(reason) = reason { - if !is_degraded { - is_degraded = true; - RecordingEvent::Degraded { reason }.emit(&app).ok(); - } - } else if matches!(event, cap_recording::PipelineHealthEvent::SourceRestarted) - && is_degraded - { - is_degraded = false; - RecordingEvent::Recovered.emit(&app).ok(); - } - } - } - }); - } - - Ok(RecordingAction::Started) -} - -async fn countdown_tick( - cancel_reason: &impl Fn() -> Option<&'static str>, -) -> Result<(), &'static str> { - let tick = tokio::time::sleep(Duration::from_secs(1)); - tokio::pin!(tick); - loop { - tokio::select! { - _ = &mut tick => break, - _ = tokio::time::sleep(Duration::from_millis(50)) => {} - } - if let Some(reason) = cancel_reason() { - return Err(reason); - } - } - cancel_reason().map_or(Ok(()), Err) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn get_recording_pause_state( - app: AppHandle, - state: MutableState<'_, App>, -) -> Result, String> { - let query: std::pin::Pin> + Send>> = - if crate::clean_capture::phase(&app).is_some() { - Box::pin(async move { crate::clean_capture::is_paused(&app).await }) - } else { - let state = state.read().await; - match state.current_recording() { - Some(InProgressRecording::Studio { handle, .. }) => { - let handle = handle.clone(); - Box::pin( - async move { handle.is_paused().await.map_err(|error| error.to_string()) }, - ) - } - Some(InProgressRecording::Instant { handle, .. }) => { - let query = handle.is_paused(); - Box::pin(async move { query.await.map_err(|error| error.to_string()) }) - } - None => return Ok(None), - } - }; - tokio::time::timeout(Duration::from_secs(2), query) - .await - .map_err(|_| "Timed out confirming recording pause state".to_string())? - .map(Some) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn pause_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { - if crate::clean_capture::phase(&app).is_some() { - return crate::clean_capture::control(&app, false).await; - } - let mut state = state.write().await; - - if let Some(recording) = state.current_recording_mut() { - recording.pause().await.map_err(|e| e.to_string())?; - RecordingEvent::Paused.emit(&app).ok(); - } - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn resume_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { - let requested = app.state::(); - let _input_operation = requested.try_resume_guard()?; - if crate::clean_capture::phase(&app).is_some() { - requested.ensure_ready_for_resume()?; - return crate::clean_capture::control(&app, true).await; - } - let mut state = state.write().await; - requested.ensure_ready_for_resume()?; - - if let Some(recording) = state.current_recording_mut() { - recording.resume().await.map_err(|e| e.to_string())?; - RecordingEvent::Resumed.emit(&app).ok(); - } - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(state))] -pub async fn set_mic_recording_muted( - state: MutableState<'_, App>, - muted: bool, -) -> Result<(), String> { - let state = state.read().await; - - let Some(recording) = state.current_recording() else { - return Err("No recording in progress".to_string()); - }; - - let mic_feed = match recording { - InProgressRecording::Instant { mic_feed, .. } => mic_feed.as_ref(), - // Studio records the mic as an editable track; muting would silently - // bake zeros into it. The bar only offers mute for instant mode — - // enforce the same contract here so no future caller can corrupt a - // studio track. - InProgressRecording::Studio { .. } => { - return Err("Mic mute is only available for instant recordings".to_string()); - } - }; - - let Some(mic_feed) = mic_feed else { - return Err("Recording has no microphone".to_string()); - }; - - mic_feed.set_recording_muted(muted); - info!(muted, "Recording microphone mute set"); - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn toggle_pause_recording( - app: AppHandle, - state: MutableState<'_, App>, -) -> Result<(), String> { - if crate::clean_capture::phase(&app).is_some() { - if crate::clean_capture::is_paused(&app).await? { - let requested = app.state::(); - let _input_operation = requested.try_resume_guard()?; - return crate::clean_capture::control(&app, true).await; - } - return crate::clean_capture::control(&app, false).await; - } - let state = state.read().await; - - if let Some(recording) = state.current_recording() { - if recording.is_paused().await.map_err(|e| e.to_string())? { - let requested = app.state::(); - let _input_operation = requested.try_resume_guard()?; - recording.resume().await.map_err(|e| e.to_string())?; - RecordingEvent::Resumed.emit(&app).ok(); - } else { - recording.pause().await.map_err(|e| e.to_string())?; - RecordingEvent::Paused.emit(&app).ok(); - } - } - - Ok(()) -} - -async fn handle_spawn_failure( - app: &AppHandle, - state_mtx: &MutableState<'_, App>, - recording_dir: &Path, - message: String, -) -> Result<(), String> { - #[cfg(target_os = "linux")] - if linux_instant::current(app).is_some_and(|attempt| attempt.owns_directory(recording_dir)) { - notify_recording_start_failed(app, &message); - return Err(message); - } - error!( - recording_dir = %recording_dir.display(), - error = %message, - "Recording actor spawn failed" - ); - - let _ = RecordingEvent::Failed { - error: message.clone(), - } - .emit(app); - - // DeviceNotFound errors are surfaced to the user via the frontend toast; skip the - // blocking native dialog so the overlay stays responsive and the error isn't repeated. - let is_device_not_found = - message.contains("no longer available") || message.contains("DeviceNotFound"); - - if !is_device_not_found && crate::clean_capture::phase(app).is_none() { - 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 mut state = state_mtx.write().await; - let _ = handle_recording_end( - app.clone(), - Err(message), - &mut state, - recording_dir.to_path_buf(), - ) - .await; - - Ok(()) -} - -fn panic_message(panic: Box) -> String { - if let Some(msg) = panic.downcast_ref::<&str>() { - msg.to_string() - } else if let Some(msg) = panic.downcast_ref::() { - msg.clone() - } else { - "unknown panic".to_string() - } -} - -async fn lock_selected_microphone( - mic_feed: &kameo::actor::ActorRef, - selected_label: Option, - selected_settings: Option, -) -> anyhow::Result>> { - let Some(label) = selected_label else { - return Ok(None); - }; - - permissions::check_microphone_access().map_err(anyhow::Error::msg)?; - - let existing_lock = match mic_feed.ask(microphone::Lock).await { - Ok(lock) if lock.device_name() == label => Some(lock), - Ok(lock) => { - drop(lock); - tokio::time::sleep(Duration::from_millis(50)).await; - None - } - Err(kameo::error::SendError::HandlerError(microphone::LockFeedError::NoInput)) => None, - Err(err) => { - return Err(anyhow!( - "Failed to lock selected microphone '{label}': {err}" - )); - } - }; - - let lock = if let Some(lock) = existing_lock { - lock - } else { - initialize_selected_microphone(mic_feed, &label, selected_settings).await?; - lock_initialized_microphone(mic_feed, &label).await? - }; - - validate_microphone_receiving(&lock, &label).await?; - Ok(Some(Arc::new(lock))) -} - -fn mic_actor_not_running(err: &anyhow::Error) -> bool { - err.chain().any(|cause| { - if let Some(source) = cause.downcast_ref::() { - matches!(source, MicrophoneSourceError::ActorNotRunning) - } else { - false - } - }) -} - -fn mic_feed_locked(err: &anyhow::Error) -> bool { - err.chain().any(|cause| { - cause - .downcast_ref::() - .is_some() - || cause - .downcast_ref::() - .is_some_and(|err| matches!(err, microphone::LockFeedError::Locked(_))) - || cause - .downcast_ref::() - .is_some_and(|err| matches!(err, microphone::SetInputError::Locked(_))) - }) || err.to_string().contains("FeedLocked") -} - -#[derive(Debug, PartialEq, Eq)] -enum ActorDoneDisposition { - UserInitiatedStop, - UnexpectedStop { error: String }, - Failed { error: String }, -} - -fn classify_actor_done_result( - result: Result<(), E>, - recording_still_active: bool, -) -> ActorDoneDisposition -where - E: ToString, -{ - match result { - Ok(()) if recording_still_active => ActorDoneDisposition::UnexpectedStop { - error: "Recording stopped unexpectedly before it was ended.".to_string(), - }, - Ok(()) => ActorDoneDisposition::UserInitiatedStop, - Err(error) => ActorDoneDisposition::Failed { - error: error.to_string(), - }, - } -} - -async fn cancel_discarded_recording( - app: &AppHandle, - recording: InProgressRecording, -) -> Option { - match recording { - InProgressRecording::Instant { - handle, - segment_upload, - video_upload_info, - .. - } => { - let video_id = video_upload_info.id; - #[cfg(target_os = "linux")] - { - if let Some(attempt) = linux_instant::current(app) { - attempt.cancel(); - } - if let Err(error) = handle.cancel().await { - warn!(%error, "Instant cancellation failed"); - } - if handle.lifecycle().wait_for_quiescence().await - != instant_recording::InstantQuiescence::Joined - { - return None; - } - if let Some(attempt) = linux_instant::current(app) - && !attempt.upload_cleanup().await - { - return None; - } - let _ = await_instant_upload(segment_upload).await; - } - #[cfg(not(target_os = "linux"))] - { - segment_upload.session.mark_cancelled().ok(); - if let Err(err) = handle.cancel().await { - warn!("Failed to cancel instant recording while discarding: {err:#}"); - } - match segment_upload.handle.await { - Ok(Ok(())) => {} - Ok(Err(err)) => warn!("Instant upload ended while discarding recording: {err}"), - Err(err) if err.is_cancelled() => {} - Err(err) => { - warn!("Failed to join instant upload while discarding recording: {err}") - } - } - } - crate::upload::emit_upload_complete(app, &video_id); - Some(video_id) - } - InProgressRecording::Studio { handle, .. } => { - if let Err(err) = handle.cancel().await { - warn!("Failed to cancel studio recording while discarding: {err:#}"); - } - - None - } - } -} - -async fn remove_recording_dir(recording_dir: &Path) -> Result<(), String> { - match tokio::fs::remove_dir_all(recording_dir).await { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(err) => Err(format!("Failed to delete recording files: {err}")), - } -} - -async fn delete_remote_instant_video(app: &AppHandle, video_id: &str) -> Result<(), String> { - let response = app - .authed_api_request( - format!("/api/desktop/video/delete?videoId={video_id}"), - |client, url| client.delete(url), - ) - .await - .map_err(|err| format!("Failed to delete instant recording: {err}"))?; - - let status = response.status(); - if status.is_success() || status == reqwest::StatusCode::NOT_FOUND { - return Ok(()); - } - - let body = response - .text() - .await - .unwrap_or_else(|err| format!("Failed to read response body: {err}")); - - Err(format!( - "Failed to delete instant recording {video_id}: {status}: {body}" - )) -} - -async fn discard_recording(app: &AppHandle, recording: InProgressRecording) -> Result<(), String> { - let recording_dir = recording.recording_dir().clone(); - let video_id = cancel_discarded_recording(app, recording).await; - if let Some(video_id) = video_id { - delete_remote_instant_video(app, &video_id).await?; - } - remove_recording_dir(&recording_dir).await -} - -#[cfg(target_os = "linux")] -async fn after_studio_join( - stop: impl std::future::Future, - finish: impl FnOnce(Result) -> F, -) -> Result -where - F: std::future::Future>, -{ - let report = stop.await; - if report.quiescence != studio_recording::StudioQuiescence::Joined { - return Err(format!( - "Studio cleanup is unconfirmed; recording and Stop control retained: {}", - report - .result - .err() - .unwrap_or_else(|| "terminal acknowledgement missing".into()) - )); - } - if !report.accepted_intent { - return Err("Another Studio terminal action owns cleanup".into()); - } - finish(report.result).await -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Clone, Copy, PartialEq, Eq)] -enum StudioTerminalAction { - Stop, - Discard, - Restart, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Clone, Copy, PartialEq, Eq)] -enum StudioStopOrigin { - Automatic, - Explicit, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Clone)] -struct StudioStopIdentity { - handle: studio_recording::ActorHandle, - directory: PathBuf, - generation: Option, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -impl StudioStopIdentity { - fn matches( - &self, - handle: &studio_recording::ActorHandle, - directory: &Path, - generation: Option, - ) -> bool { - #[cfg(target_os = "linux")] - let same = self.handle.lifecycle().same_attempt(&handle.lifecycle()); - #[cfg(any(target_os = "macos", windows))] - let same = self.handle.same_attempt(handle); - same && self.directory == directory && self.generation == generation - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Default)] -struct StudioStopFlight { - participants: usize, - explicit: usize, - explicit_seen: bool, - presentation_claimed: bool, - automatic_error: Option, - cleanup_completed: bool, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -struct StudioStopCohort { - identity: StudioStopIdentity, - notice: crate::clean_capture::StopNoticeTicket, - flight: std::sync::Mutex, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Clone, Default)] -struct StudioStopRegistry(Arc>); - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Default)] -struct StudioStopRegistryState { - cohorts: Vec>, - active: Option<(StudioStopIdentity, crate::clean_capture::StopNoticeOwner)>, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -#[derive(Clone)] -struct StudioStopError { - kind: crate::clean_capture::StopNoticeKind, - message: String, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -type StudioStopPresenter = - Arc; - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -impl StudioStopRegistry { - fn retire(&self, owner: &crate::clean_capture::StopNoticeOwner) { - let mut entries = self.0.lock().unwrap(); - if entries - .active - .as_ref() - .is_some_and(|(_, active)| active.same_attempt(owner)) - { - entries.active = None; - } - } - - fn active_identity(&self, directory: &Path, generation: u32) -> Option { - self.0 - .lock() - .unwrap() - .active - .as_ref() - .and_then(|(identity, _)| { - (identity.directory == directory && identity.generation == Some(generation)) - .then(|| identity.clone()) - }) - } - - fn retire_identity( - &self, - handle: &studio_recording::ActorHandle, - directory: &Path, - generation: Option, - ) -> Option { - let mut entries = self.0.lock().unwrap(); - if entries - .active - .as_ref() - .is_some_and(|(identity, _)| identity.matches(handle, directory, generation)) - { - entries.active.take().map(|(_, owner)| owner) - } else { - None - } - } - - fn enroll( - &self, - identity: StudioStopIdentity, - origin: StudioStopOrigin, - present: StudioStopPresenter, - new_owner: impl FnOnce(&StudioStopIdentity) -> crate::clean_capture::StopNoticeOwner, - new_ticket: impl FnOnce( - crate::clean_capture::StopNoticeOwner, - ) -> crate::clean_capture::StopNoticeTicket, - ) -> StudioStopParticipant { - let mut entries = self.0.lock().unwrap(); - let cohort = match entries.cohorts.iter().find(|entry| { - entry - .identity - .matches(&identity.handle, &identity.directory, identity.generation) - }) { - Some(entry) => entry.clone(), - None => { - let owner = match &entries.active { - Some((active, owner)) - if active.matches( - &identity.handle, - &identity.directory, - identity.generation, - ) => - { - owner.clone() - } - _ => { - let owner = new_owner(&identity); - entries.active = Some((identity.clone(), owner.clone())); - owner - } - }; - let entry = Arc::new(StudioStopCohort { - identity, - notice: new_ticket(owner), - flight: std::sync::Mutex::new(StudioStopFlight::default()), - }); - entries.cohorts.push(entry.clone()); - entry - } - }; - { - let mut flight = cohort.flight.lock().unwrap(); - flight.participants += 1; - if origin == StudioStopOrigin::Explicit { - flight.explicit += 1; - flight.explicit_seen = true; - } - } - StudioStopParticipant { - registry: self.clone(), - cohort, - origin, - present, - } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -fn studio_stop_registry(app: &AppHandle) -> StudioStopRegistry { - if app.try_state::().is_none() { - app.manage(StudioStopRegistry::default()); - } - app.state::().inner().clone() -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -fn enroll_studio_stop( - app: &AppHandle, - _state: &Arc>, - handle: &studio_recording::ActorHandle, - directory: &Path, - generation: Option, - origin: StudioStopOrigin, -) -> StudioStopParticipant { - let retained_app = app.clone(); - let registry = studio_stop_registry(app); - registry.enroll( - StudioStopIdentity { - handle: handle.clone(), - directory: directory.to_owned(), - generation, - }, - origin, - Arc::new(move |ticket, error| { - crate::clean_capture::retain_stop_notice( - &retained_app, - ticket, - error.kind, - error.message, - ); - }), - |identity| { - crate::clean_capture::reserve_stop_notice_owner( - app, - identity.directory.clone(), - identity.generation, - ) - }, - |owner| crate::clean_capture::reserve_stop_notice_ticket(app, owner), - ) -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -struct StudioStopParticipant { - registry: StudioStopRegistry, - cohort: Arc, - origin: StudioStopOrigin, - present: StudioStopPresenter, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -impl StudioStopParticipant { - fn claim_presentation(&self) { - let mut flight = self.cohort.flight.lock().unwrap(); - flight.presentation_claimed = true; - flight.automatic_error = None; - } - - fn hand_off_error(&self) { - if self.origin == StudioStopOrigin::Explicit { - self.claim_presentation(); - } - } - - fn defer_error(&self, error: StudioStopError) { - let mut flight = self.cohort.flight.lock().unwrap(); - if !flight.presentation_claimed && flight.automatic_error.is_none() { - flight.automatic_error = Some(error.clone()); - } - drop(flight); - (self.present)(&self.cohort.notice, error); - } - - fn stale_completion(self) -> StudioTerminalCompletion { - let outcome = stale_studio_completion( - Some(&self.cohort), - self.origin == StudioStopOrigin::Automatic, - ); - StudioTerminalCompletion { - outcome, - participant: Some(self), - } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -impl Drop for StudioStopParticipant { - fn drop(&mut self) { - let error = { - let mut entries = self.registry.0.lock().unwrap(); - let mut flight = self.cohort.flight.lock().unwrap(); - flight.participants -= 1; - if self.origin == StudioStopOrigin::Explicit { - flight.explicit -= 1; - } - let error = if flight.explicit == 0 && !flight.presentation_claimed { - let error = flight.automatic_error.take(); - if error.is_some() { - flight.presentation_claimed = true; - } - error - } else { - None - }; - if flight.participants == 0 { - entries - .cohorts - .retain(|entry| !Arc::ptr_eq(entry, &self.cohort)); - } - error - }; - if let Some(error) = error { - (self.present)(&self.cohort.notice, error); - } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -enum StudioControlFailure { - Unconfirmed(String), - TaskFailed(String), - RejectedConfirmed(String), - Other(String), -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -enum StudioTerminalOutcome { - AppliedCompletion(Result<(), String>), - SupersededConfirmedStop, - ControlFailure(StudioControlFailure), -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -fn stale_studio_completion( - cohort: Option<&Arc>, - automatic: bool, -) -> StudioTerminalOutcome { - if cohort.is_some_and(|cohort| { - cohort.flight.lock().unwrap().cleanup_completed - || automatic && cohort.notice.owner.is_confirmed() - }) { - StudioTerminalOutcome::SupersededConfirmedStop - } else { - StudioTerminalOutcome::ControlFailure(StudioControlFailure::Other( - "Studio terminal completion is stale".into(), - )) - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -fn complete_studio_cleanup( - app: &AppHandle, - cohort: Option<&Arc>, - state: &App, - result: Result<(), String>, -) -> StudioTerminalOutcome { - if let Some(cohort) = cohort { - if let Some(InProgressRecording::Studio { handle, common, .. }) = state.current_recording() - && cohort - .identity - .matches(handle, &common.recording_dir, cohort.identity.generation) - { - return StudioTerminalOutcome::ControlFailure(StudioControlFailure::Other( - result - .err() - .unwrap_or_else(|| "Studio cleanup did not retire the recording".into()), - )); - } - cohort.flight.lock().unwrap().cleanup_completed = true; - studio_stop_registry(app).retire(&cohort.notice.owner); - crate::clean_capture::confirm_stop_notice(app, &cohort.notice.owner); - } - StudioTerminalOutcome::AppliedCompletion(result) -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -struct StudioTerminalCompletion { - outcome: StudioTerminalOutcome, - participant: Option, -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -impl StudioTerminalCompletion { - #[cfg(any(target_os = "macos", windows))] - fn error(&self) -> Option<&String> { - match &self.outcome { - StudioTerminalOutcome::ControlFailure(StudioControlFailure::RejectedConfirmed(_)) - if self.participant.as_ref().is_some_and(|participant| { - participant.origin == StudioStopOrigin::Automatic - }) => - { - None - } - StudioTerminalOutcome::AppliedCompletion(Err(error)) - | StudioTerminalOutcome::ControlFailure(StudioControlFailure::Unconfirmed(error)) - | StudioTerminalOutcome::ControlFailure(StudioControlFailure::TaskFailed(error)) - | StudioTerminalOutcome::ControlFailure(StudioControlFailure::Other(error)) - | StudioTerminalOutcome::ControlFailure(StudioControlFailure::RejectedConfirmed( - error, - )) => Some(error), - _ => None, - } - } - - fn from_report( - result: Result, - accepted: bool, - confirmed: bool, - participant: Option, - ) -> Self { - let outcome = match result { - Ok(outcome) => outcome, - Err(error) => StudioTerminalOutcome::ControlFailure(if !confirmed { - StudioControlFailure::Unconfirmed(error) - } else if !accepted && confirmed { - StudioControlFailure::RejectedConfirmed(error) - } else { - StudioControlFailure::Other(error) - }), - }; - Self { - outcome, - participant: None, - } - .with_participant(participant) - } - - fn with_participant(mut self, participant: Option) -> Self { - let error = match &self.outcome { - StudioTerminalOutcome::AppliedCompletion(Err(message)) => Some(StudioStopError { - kind: crate::clean_capture::StopNoticeKind::ConfirmedFailure, - message: message.clone(), - }), - StudioTerminalOutcome::ControlFailure(StudioControlFailure::Unconfirmed(message)) => { - Some(StudioStopError { - kind: crate::clean_capture::StopNoticeKind::Unconfirmed, - message: message.clone(), - }) - } - StudioTerminalOutcome::ControlFailure( - StudioControlFailure::TaskFailed(message) | StudioControlFailure::Other(message), - ) => Some(StudioStopError { - kind: crate::clean_capture::StopNoticeKind::ControlFailure, - message: message.clone(), - }), - StudioTerminalOutcome::ControlFailure(StudioControlFailure::RejectedConfirmed( - message, - )) if participant - .as_ref() - .is_some_and(|participant| participant.origin == StudioStopOrigin::Explicit) => - { - Some(StudioStopError { - kind: crate::clean_capture::StopNoticeKind::ControlFailure, - message: message.clone(), - }) - } - _ => None, - }; - if let (Some(error), Some(participant)) = (error, &participant) { - participant.defer_error(error); - } - self.participant = participant; - self - } - - fn into_result(self) -> Result<(), String> { - let Self { - outcome, - participant, - } = self; - let completion = Self { - outcome, - participant: None, - } - .with_participant(participant); - match completion.outcome { - StudioTerminalOutcome::AppliedCompletion(result) => { - if result.is_err() - && let Some(participant) = &completion.participant - { - participant.hand_off_error(); - } - result - } - StudioTerminalOutcome::SupersededConfirmedStop => Ok(()), - StudioTerminalOutcome::ControlFailure(StudioControlFailure::Unconfirmed(error)) - | StudioTerminalOutcome::ControlFailure(StudioControlFailure::TaskFailed(error)) => { - if let Some(participant) = &completion.participant { - participant.hand_off_error(); - } - Err(error) - } - StudioTerminalOutcome::ControlFailure(StudioControlFailure::Other(error)) - | StudioTerminalOutcome::ControlFailure(StudioControlFailure::RejectedConfirmed( - error, - )) => Err(error), - } - } - - fn present_automatic(self) { - let participant = self.participant; - drop( - Self { - outcome: self.outcome, - participant: None, - } - .with_participant(participant), - ); - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -async fn run_owned_studio_stop( - work: impl std::future::Future> + Send + 'static, - participant: Option, -) -> Option { - let failed_task_presenter = participant - .as_ref() - .map(|participant| (participant.cohort.clone(), participant.present.clone())); - let owned = async move { - let completion = match AssertUnwindSafe(work).catch_unwind().await { - Ok(completion) => completion, - Err(panic) => Some(StudioTerminalCompletion { - outcome: StudioTerminalOutcome::ControlFailure(StudioControlFailure::TaskFailed( - format!( - "Studio Stop task failed; cleanup may be incomplete: {}", - panic_message(panic) - ), - )), - participant: None, - }), - }; - completion.map(|completion| completion.with_participant(participant)) - }; - match tauri::async_runtime::spawn(owned).await { - Ok(completion) => completion, - Err(error) => { - let error = format!("Studio Stop task failed; cleanup may be incomplete: {error}"); - if let Some((cohort, present)) = failed_task_presenter { - present( - &cohort.notice, - StudioStopError { - kind: crate::clean_capture::StopNoticeKind::ControlFailure, - message: error.clone(), - }, - ); - } - Some(StudioTerminalCompletion { - outcome: StudioTerminalOutcome::ControlFailure(StudioControlFailure::TaskFailed( - error, - )), - participant: None, - }) - } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -fn stopped_studio_error(error: &str, directory: &Path, action: StudioTerminalAction) -> String { - if action != StudioTerminalAction::Stop { - return error.to_owned(); - } - let lower = error.to_ascii_lowercase(); - let disk_full = lower.contains("disk full:") - || lower.contains("no space left on device") - || !cfg!(windows) && lower.contains("(os error 28)") - || cfg!(windows) && (lower.contains("(os error 112)") || lower.contains("(os error 39)")); - if disk_full { - format!( - "Recording stopped because your disk is full. Your recording files have been kept at {}. Free up space before recording again.", - directory.display() - ) - } else { - error.to_owned() - } -} - -#[cfg(target_os = "linux")] -async fn control_studio_recording( - app: &AppHandle, - state: &Arc>, - expected_directory: Option<&Path>, - action: StudioTerminalAction, - failure: Option, - automatic: Option, -) -> Option { - let (handle, directory, target_name, capture_target, generation, participant) = { - let current_state = state.read().await; - let Some(InProgressRecording::Studio { handle, common, .. }) = - current_state.current_recording() - else { - return automatic.map(StudioStopParticipant::stale_completion); - }; - if expected_directory.is_some_and(|expected| expected != common.recording_dir) { - return Some(match automatic { - Some(participant) => participant.stale_completion(), - None => StudioTerminalCompletion { - outcome: StudioTerminalOutcome::ControlFailure(StudioControlFailure::Other( - "Studio terminal operation belongs to an older recording".into(), - )), - participant: None, - }, - }); - } - let generation = crate::clean_capture::owner(app, &common.recording_dir); - let participant = if action == StudioTerminalAction::Stop { - match automatic { - Some(participant) => { - if !participant.cohort.identity.matches( - handle, - &common.recording_dir, - generation, - ) || participant.origin == StudioStopOrigin::Explicit - && !studio_stop_retry_is_current(app, &common.recording_dir, generation) - { - return Some(participant.stale_completion()); - } - Some(participant) - } - None => Some(enroll_studio_stop( - app, - state, - handle, - &common.recording_dir, - generation, - StudioStopOrigin::Explicit, - )), - } - } else { - None - }; - if action == StudioTerminalAction::Stop { - common.camera_snapshot.get_or_init(|| { - StudioCameraSnapshot::capture(app, ¤t_state, &common.inputs.capture_target) - }); - } - ( - handle.clone(), - common.recording_dir.clone(), - common.target_name.clone(), - common.inputs.capture_target.clone(), - generation, - participant, - ) - }; - - let automatic = participant - .as_ref() - .is_some_and(|participant| participant.origin == StudioStopOrigin::Automatic); - let cohort = participant - .as_ref() - .map(|participant| participant.cohort.clone()); - let app = app.clone(); - let state = state.clone(); - #[cfg(any(target_os = "macos", windows))] - let expected_directory = expected_directory.map(Path::to_owned); - let work = async move { - let app = &app; - let state = &state; - #[cfg(any(target_os = "macos", windows))] - let expected_directory = expected_directory.as_deref(); - let discard = action != StudioTerminalAction::Stop; - let intent = if discard { - studio_recording::StudioStopIntent::Discard - } else { - studio_recording::StudioStopIntent::Preserve - }; - - let stopping = handle.clone(); - let report = stopping.stop_with_intent(intent).await; - let accepted = report.accepted_intent; - let confirmed = report.quiescence == studio_recording::StudioQuiescence::Joined; - let result = after_studio_join( - async move { report }, - |result| async move { - let outcome = match failure { - Some(error) => Err(error), - None => result, - }; - if discard && let Err(error) = &outcome { - return Err(error.clone()); - } - let finalization_project = if !discard && outcome.is_ok() { - Some(crate::FinalizationProject::admit(directory.clone()).await) - } else { - None - }; - let mut state = state.write().await; - let current = match state.current_recording() { - Some(InProgressRecording::Studio { - handle: current, - common, - .. - }) => { - common.recording_dir == directory - && current.lifecycle().same_attempt(&handle.lifecycle()) - && crate::clean_capture::owner(app, &directory) == generation - } - _ => false, - }; - if !current { - return Ok(stale_studio_completion(cohort.as_ref(), automatic)); - } - if discard && let Err(error) = remove_recording_dir(&directory).await { - return Err(error); - } - let error = outcome.as_ref().err().cloned(); - let completed = if discard { - Err("Recording discarded after confirmed capture shutdown".into()) - } else { - outcome.map(|recording| CompletedRecording::Studio { - recording, - target_name, - capture_target, - }) - }; - let display_error = error.as_deref().map(|error| { - tracing::error!(error, directory = %directory.display(), "Studio stopped with a recording failure"); - stopped_studio_error(error, &directory, action) - }); - if let Some(error) = &display_error { - let _ = RecordingEvent::Failed { - error: error.clone(), - } - .emit(app); - } - let cleanup = handle_recording_end_inner( - app.clone(), - completed, - &mut state, - directory, - action == StudioTerminalAction::Restart, - finalization_project, - ) - .await; - let result = match display_error { - Some(error) => Err(error), - None => cleanup, - }; - let outcome = complete_studio_cleanup(app, cohort.as_ref(), &state, result); - drop(state); - Ok(outcome) - }, - ) - .await; - Some(StudioTerminalCompletion::from_report( - result, accepted, confirmed, None, - )) - }; - if action == StudioTerminalAction::Stop { - run_owned_studio_stop(work, participant).await - } else { - work.await - } -} - -#[cfg(any(target_os = "macos", windows))] -async fn after_studio_capture_stop( - stop: impl std::future::Future, - finish: impl FnOnce(Result) -> F, -) -> Result -where - F: std::future::Future>, -{ - let report = stop.await; - if !report.stop_acknowledged { - return Err(format!( - "Studio cleanup is unconfirmed; recording and Stop control retained: {}", - report - .result - .err() - .unwrap_or_else(|| "terminal acknowledgement missing".into()) - )); - } - if !report.accepted_intent { - return Err("Another Studio terminal action owns cleanup".into()); - } - finish(report.result).await -} - -#[cfg(any(target_os = "macos", windows))] -async fn control_studio_recording( - app: &AppHandle, - state: &Arc>, - expected_directory: Option<&Path>, - action: StudioTerminalAction, - failure: Option, - automatic: Option, -) -> Option { - let (handle, directory, target_name, capture_target, generation, participant) = { - let current_state = state.read().await; - let Some(InProgressRecording::Studio { handle, common, .. }) = - current_state.current_recording() - else { - return automatic.map(StudioStopParticipant::stale_completion); - }; - if expected_directory.is_some_and(|expected| expected != common.recording_dir) { - return Some(match automatic { - Some(participant) => participant.stale_completion(), - None => StudioTerminalCompletion { - outcome: StudioTerminalOutcome::ControlFailure(StudioControlFailure::Other( - "Studio terminal operation belongs to an older recording".into(), - )), - participant: None, - }, - }); - } - let generation = crate::clean_capture::owner(app, &common.recording_dir); - let participant = if action == StudioTerminalAction::Stop { - match automatic { - Some(participant) => { - if !participant.cohort.identity.matches( - handle, - &common.recording_dir, - generation, - ) || participant.origin == StudioStopOrigin::Explicit - && !studio_stop_retry_is_current(app, &common.recording_dir, generation) - { - return Some(participant.stale_completion()); - } - Some(participant) - } - None => Some(enroll_studio_stop( - app, - state, - handle, - &common.recording_dir, - generation, - StudioStopOrigin::Explicit, - )), - } - } else { - None - }; - if action == StudioTerminalAction::Stop { - common.camera_snapshot.get_or_init(|| { - StudioCameraSnapshot::capture(app, ¤t_state, &common.inputs.capture_target) - }); - } - ( - handle.clone(), - common.recording_dir.clone(), - common.target_name.clone(), - common.inputs.capture_target.clone(), - generation, - participant, - ) - }; - let automatic = participant - .as_ref() - .is_some_and(|participant| participant.origin == StudioStopOrigin::Automatic); - let cohort = participant - .as_ref() - .map(|participant| participant.cohort.clone()); - let app = app.clone(); - let state = state.clone(); - #[cfg(any(target_os = "macos", windows))] - let expected_directory = expected_directory.map(Path::to_owned); - let work = async move { - let app = &app; - let state = &state; - #[cfg(any(target_os = "macos", windows))] - let expected_directory = expected_directory.as_deref(); - let discard = action != StudioTerminalAction::Stop; - let intent = if discard { - studio_recording::StudioStopIntent::Discard - } else { - studio_recording::StudioStopIntent::Preserve - }; - - let stopping = handle.clone(); - let finishing = handle.clone(); - let report = stopping.stop_with_intent(intent).await; - let accepted = report.accepted_intent; - let confirmed = report.stop_acknowledged; - let result = after_studio_capture_stop( - async move { report }, - |result| async move { - let outcome = match (failure, result) { - (Some(failure), Err(error)) => Err(format!("{failure}; {error}")), - (Some(error), _) => Err(error), - (None, result) => result, - }; - let finalization_project = if !discard && outcome.is_ok() { - Some(crate::FinalizationProject::admit(directory.clone()).await) - } else { - None - }; - let mut state = state.write().await; - let current = match state.current_recording() { - Some(InProgressRecording::Studio { - handle: current, - common, - .. - }) => { - common.recording_dir == directory - && current.same_attempt(&finishing) - && crate::clean_capture::owner(app, &directory) == generation - } - _ => false, - }; - if !current { - return Ok(stale_studio_completion(cohort.as_ref(), automatic)); - } - let mut error = outcome.as_ref().err().cloned(); - if discard && error.is_none() { - error = remove_recording_dir(&directory).await.err(); - } - #[cfg(target_os = "macos")] - if action == StudioTerminalAction::Restart && error.is_none() { - drop(state.clear_current_recording()); - if let Some(owner) = studio_stop_registry(app).retire_identity(&finishing, &directory, generation) { - crate::clean_capture::confirm_stop_notice(app, &owner); - } - CurrentRecordingChanged.emit(app).ok(); - return Ok(StudioTerminalOutcome::AppliedCompletion(Ok(()))); - } - let completed = if let Some(error) = &error { - Err(error.clone()) - } else if discard { - Err("Recording discarded after Studio stop acknowledgement".into()) - } else { - outcome.map(|recording| CompletedRecording::Studio { - recording, - target_name, - capture_target, - }) - }; - let display_error = error.as_deref().map(|error| { - tracing::error!(error, directory = %directory.display(), "Studio stopped with a recording failure"); - stopped_studio_error(error, &directory, action) - }); - if let Some(error) = &display_error { - let _ = RecordingEvent::Failed { - error: error.clone(), - } - .emit(app); - } - let cleanup = handle_recording_end_inner( - app.clone(), - completed, - &mut state, - directory, - action == StudioTerminalAction::Restart && error.is_none(), - finalization_project, - ) - .await; - let result = match display_error { - Some(error) => Err(error), - None => cleanup, - }; - let outcome = complete_studio_cleanup(app, cohort.as_ref(), &state, result); - drop(state); - Ok(outcome) - }, - ) - .await; - let completion = StudioTerminalCompletion::from_report(result, accepted, confirmed, None); - if action != StudioTerminalAction::Stop - && !(automatic - && matches!( - &completion.outcome, - StudioTerminalOutcome::ControlFailure(StudioControlFailure::RejectedConfirmed( - _ - )) - )) - && let Some(error) = completion.error() - { - let state = state.read().await; - if let Some(InProgressRecording::Studio { - handle: current, - common, - .. - }) = state.current_recording() - && current.same_attempt(&handle) - && expected_directory.is_none_or(|expected| expected == common.recording_dir) - { - let _ = RecordingEvent::Failed { - error: error.clone(), - } - .emit(app); - } - } - Some(completion) - }; - if action == StudioTerminalAction::Stop { - run_owned_studio_stop(work, participant).await - } else { - work.await - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -fn studio_stop_retry_is_current( - app: &AppHandle, - directory: &Path, - generation: Option, -) -> bool { - let Some(generation) = generation else { - return false; - }; - let snapshot = crate::clean_capture::get_clean_capture_state(app.clone()); - snapshot.generation == generation - && matches!(snapshot.phase, Some(crate::clean_capture::Phase::Stopping)) - && matches!(snapshot.mode, Some(RecordingMode::Studio)) - && crate::clean_capture::owner(app, directory) == Some(generation) -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -pub(crate) fn queue_clean_studio_stop(app: &AppHandle, generation: u32, directory: PathBuf) { - let Some(identity) = studio_stop_registry(app).active_identity(&directory, generation) else { - return; - }; - let app = app.clone(); - drop(tauri::async_runtime::spawn(async move { - if let Err(error) = - stop_clean_studio_recording(app, identity.handle, generation, directory).await - { - tracing::error!(%error, "Clean capture Stop did not complete"); - } - })); -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -pub(crate) async fn stop_clean_studio_recording( - app: AppHandle, - handle: studio_recording::ActorHandle, - generation: u32, - directory: PathBuf, -) -> Result<(), String> { - let state = app.state::>>().inner().clone(); - let participant = { - let current = state.read().await; - let Some(InProgressRecording::Studio { - handle: current_handle, - common, - .. - }) = current.current_recording() - else { - return Ok(()); - }; - let identity = StudioStopIdentity { - handle, - directory, - generation: Some(generation), - }; - if !identity.matches( - current_handle, - &common.recording_dir, - crate::clean_capture::owner(&app, &common.recording_dir), - ) || !crate::clean_capture::queue_owned_studio_stop( - &app, - generation, - &common.recording_dir, - ) { - return Ok(()); - } - enroll_studio_stop( - &app, - &state, - current_handle, - &common.recording_dir, - Some(generation), - StudioStopOrigin::Explicit, - ) - }; - control_studio_recording( - &app, - &state, - None, - StudioTerminalAction::Stop, - None, - Some(participant), - ) - .await - .map_or(Ok(()), StudioTerminalCompletion::into_result) -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows))] -async fn queue_studio_stop( - app: &AppHandle, - state: &Arc>, -) -> (bool, Option) { - let current = state.read().await; - let generation = match current.current_recording() { - Some(InProgressRecording::Studio { common, .. }) => { - crate::clean_capture::owner(app, &common.recording_dir) - } - _ => None, - }; - let deferred = crate::clean_capture::queue_stop(app); - let retry = if deferred { - match current.current_recording() { - Some(InProgressRecording::Studio { handle, common, .. }) - if studio_stop_retry_is_current(app, &common.recording_dir, generation) => - { - Some(enroll_studio_stop( - app, - state, - handle, - &common.recording_dir, - generation, - StudioStopOrigin::Explicit, - )) - } - _ => None, - } - } else { - None - }; - (deferred, retry) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { - if cancel_recording_storage_prompt(&app, &state).await { - return Ok(()); - } - { - let current = state.read().await; - if let Some(InProgressRecording::Studio { common, .. }) = current.current_recording() { - common.camera_snapshot.get_or_init(|| { - StudioCameraSnapshot::capture(&app, ¤t, &common.inputs.capture_target) - }); - } - } - #[cfg(target_os = "linux")] - if let Some(attempt) = linux_instant::current(&app) { - return linux_instant::control(app, attempt, false).await; - } - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - let (deferred, retry) = queue_studio_stop(&app, &state).await; - #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - let deferred = crate::clean_capture::queue_stop(&app); - if deferred { - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - if let Some(retry) = retry { - return control_studio_recording( - &app, - &state, - None, - StudioTerminalAction::Stop, - None, - Some(retry), - ) - .await - .map_or(Ok(()), StudioTerminalCompletion::into_result); - } - return Ok(()); - } - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - if let Some(result) = - control_studio_recording(&app, &state, None, StudioTerminalAction::Stop, None, None).await - { - return result.into_result(); - } - let mut state = state.write().await; - let recording_pending = matches!(&state.recording_state, RecordingState::Pending { .. }); - let Some(current_recording) = state.clear_current_recording() else { - if recording_pending { - debug!("Stop recording requested before recording actor was ready"); - return Err("Recording is still starting".to_string()); - } - debug!("Stop recording requested without active recording"); - return Ok(()); - }; - - let recording_dir = current_recording.recording_dir().clone(); - let recording_outcome = match current_recording.stop().await { - Ok(completed) => Ok(completed), - Err((e, ctx)) => { - error!("Recording stop failed: {e:#}"); - if let Some(ctx) = ctx { - #[cfg(not(target_os = "linux"))] - { - ctx.segment_upload.session.cancel(); - let _ = ctx.segment_upload.handle.await; - } - #[cfg(target_os = "linux")] - if let Some(attempt) = linux_instant::current(&app) { - attempt.cancel(); - } - crate::upload::emit_upload_complete(&app, &ctx.video_upload_info.id); - } - Err(e.to_string()) - } - }; - - handle_recording_end(app, recording_outcome, &mut state, recording_dir).await?; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn restart_recording( - app: AppHandle, - state: MutableState<'_, App>, -) -> Result { - #[cfg(target_os = "linux")] - if let Some(attempt) = linux_instant::current(&app) { - let inputs = state - .read() - .await - .current_recording() - .ok_or("No recording in progress")? - .inputs() - .clone(); - let restore_generation = - crate::clean_capture::phase(&app).map(|_| crate::clean_capture::generation(&app)); - linux_instant::control(app.clone(), attempt, true).await?; - if let Some(generation) = restore_generation { - crate::clean_capture::wait_restored(&app, generation).await?; - } - return Box::pin(start_recording(app, state, inputs)).await; - } - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - { - let current = { - let state = state.read().await; - match state.current_recording() { - Some(InProgressRecording::Studio { common, .. }) => Some(( - common.inputs.clone(), - common.recording_dir.clone(), - crate::clean_capture::owner(&app, &common.recording_dir), - )), - _ => None, - } - }; - if let Some((inputs, directory, generation)) = current { - return complete_studio_restart(async move { - let state = app.state::>(); - let target = EditorRecordingTarget::get(&app); - restart_with_editor_target( - &target, - async { - control_studio_recording( - &app, - &state, - Some(&directory), - StudioTerminalAction::Restart, - None, - None, - ) - .await - .ok_or("Studio recording changed before restart")? - .into_result()?; - Ok(()) - }, - async { - #[cfg(target_os = "linux")] - if let Some(generation) = generation { - crate::clean_capture::wait_restored(&app, generation).await?; - } - #[cfg(any(target_os = "macos", windows))] - let _ = generation; - Ok(()) - }, - || Box::pin(start_recording(app.clone(), app.state(), inputs)), - |expected, cleanup_completed| { - let app = &app; - let state = &state; - let target = ⌖ - async move { - let state = state.read().await; - if let Some(editor_path) = take_failed_restart_editor_target( - target, - expected.as_deref(), - cleanup_completed - && matches!(state.recording_state, RecordingState::None), - ) { - crate::editor_recording::finish(app); - crate::editor_recording::reveal_editor(app, &editor_path); - } - } - }, - ) - .await - }) - .await; - } - } - if crate::clean_capture::phase(&app) == Some(crate::clean_capture::Phase::Recording) { - crate::clean_capture::control(&app, false).await?; - } - - let (recording, clean_generation) = { - let mut state = state.write().await; - let recording = state - .current_recording() - .ok_or("No recording in progress")?; - let generation = crate::clean_capture::begin_restart(&app, recording.recording_dir())?; - (state.clear_current_recording().unwrap(), generation) - }; - - let _ = CurrentRecordingChanged.emit(&app); - - let inputs = recording.inputs().clone(); - let recording_dir = recording.recording_dir().clone(); - - let upload_session = match &recording { - InProgressRecording::Instant { segment_upload, .. } => { - #[cfg(not(target_os = "linux"))] - let session = segment_upload.session.clone(); - #[cfg(target_os = "linux")] - let session = segment_upload.lock().await.session.clone(); - Some(session) - } - _ => None, - }; - let video_id = cancel_discarded_recording(&app, recording).await; - if let (Some(video_id), Some(session)) = (video_id, upload_session) { - let cleanup_app = app.clone(); - let cleanup_directory = recording_dir.clone(); - if let Err(error) = crate::upload::lifecycle::supervise(app.clone(), session, async move { - delete_remote_instant_video(&cleanup_app, &video_id) - .await - .map_err(AuthedApiError::from)?; - remove_recording_dir(&cleanup_directory) - .await - .map_err(AuthedApiError::from) - }) - .await - { - warn!(%error, "Restart retained the cancelled recording"); - } - } else if let Err(error) = remove_recording_dir(&recording_dir).await { - warn!(%error, "Failed to delete recording files while restarting"); - } - - if let Some(generation) = clean_generation { - let result = async { - crate::clean_capture::hide(&app, generation).await?; - let requested = app - .state::() - .ready_snapshot()?; - crate::clean_capture::prepare(&app, &inputs, Some(generation)).await?; - state - .write() - .await - .set_pending_recording(inputs.mode, inputs.capture_target.clone())?; - start_recording_prepared( - app.clone(), - state.clone(), - inputs, - requested, - Some(generation), - ) - .await - } - .await; - if !matches!(&result, Ok(RecordingAction::Started)) { - let mut app_state = state.write().await; - if crate::clean_capture::is_current(&app, generation) { - app_state.clear_pending_recording(); - drop(app_state); - crate::clean_capture::release(&app, generation, false); - } - } - return result; - } - start_recording(app.clone(), state, inputs).await -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows, test))] -async fn complete_studio_restart( - restart: impl std::future::Future> + Send + 'static, -) -> Result { - tokio::spawn(restart) - .await - .map_err(|error| format!("Recording restart task failed: {error}"))? -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows, test))] -async fn restart_with_editor_target( - target: &EditorRecordingTarget, - cleanup: C, - restore: R, - start: impl FnOnce() -> S, - on_failure: impl FnOnce(Option, bool) -> F, -) -> Result -where - C: std::future::Future>, - R: std::future::Future>, - S: std::future::Future>, - F: std::future::Future, -{ - let expected = target.0.lock().unwrap().clone(); - let cleanup_result = cleanup.await; - let cleanup_completed = cleanup_result.is_ok(); - let result = match cleanup_result { - Ok(()) => match restore.await { - Ok(()) => { - let unchanged = *target.0.lock().unwrap() == expected; - if unchanged { - start().await - } else { - Err("Recording editor target changed before restart".into()) - } - } - Err(error) => Err(error), - }, - Err(error) => Err(error), - }; - if !matches!(&result, Ok(RecordingAction::Started)) { - on_failure(expected, cleanup_completed).await; - } - result -} - -#[cfg(any(target_os = "linux", target_os = "macos", windows, test))] -fn take_failed_restart_editor_target( - target: &EditorRecordingTarget, - expected: Option<&Path>, - recording_cleared: bool, -) -> Option { - if !recording_cleared { - return None; - } - let mut current = target.0.lock().unwrap(); - if current.as_deref() == expected { - current.take() - } else { - None - } -} - -fn take_editor_target_after_recording( - target: &EditorRecordingTarget, - preserve: bool, -) -> Option { - if preserve { - None - } else { - target.0.lock().unwrap().take() - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(app, state))] -pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { - if cancel_recording_storage_prompt(&app, &state).await { - return Ok(()); - } - #[cfg(target_os = "linux")] - if let Some(attempt) = linux_instant::current(&app) { - return linux_instant::control(app, attempt, true).await; - } - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - if let Some(result) = control_studio_recording( - &app, - &state, - None, - StudioTerminalAction::Discard, - None, - None, - ) - .await - { - return result.into_result(); - } - if crate::clean_capture::phase(&app) == Some(crate::clean_capture::Phase::Recording) { - crate::clean_capture::control(&app, false).await?; - } - - if matches!( - crate::clean_capture::phase(&app), - Some( - crate::clean_capture::Phase::Starting - | crate::clean_capture::Phase::AwaitingShortcut - | crate::clean_capture::Phase::Pausing - | crate::clean_capture::Phase::Resuming - | crate::clean_capture::Phase::ResumeFailed - | crate::clean_capture::Phase::Restarting - ) - ) { - return Err("Recording is changing state. Use Ctrl+Shift+F9 to stop.".into()); - } - let recording_data = { - let mut app_state = state.write().await; - app_state.clear_current_recording() - }; - - if let Some(recording) = recording_data { - CurrentRecordingChanged.emit(&app).ok(); - RecordingStopped {}.emit(&app).ok(); - - if let Some(window) = CapWindowId::RecordingControls.get(&app) { - let _ = window.hide(); - } - - let clean_generation = crate::clean_capture::owner(&app, recording.recording_dir()); - if let Some(generation) = clean_generation { - crate::clean_capture::set_phase( - &app, - generation, - crate::clean_capture::Phase::Stopping, - ); - } - let delete_result = discard_recording(&app, recording).await; - if let Some(generation) = clean_generation { - crate::clean_capture::release(&app, generation, false); - } - - let settings = GeneralSettingsStore::get(&app) - .ok() - .flatten() - .unwrap_or_default(); - - match settings.post_deletion_behaviour { - PostDeletionBehaviour::DoNothing => {} - PostDeletionBehaviour::ReopenRecordingWindow => { - let _ = ShowCapWindow::Main { - init_target_mode: None, - } - .show(&app) - .await; - } - } - - delete_result?; - } - - Ok(()) -} - -#[tauri::command(async)] -#[specta::specta] -#[tracing::instrument(name = "take_screenshot", skip(app))] -pub async fn take_screenshot( - app: AppHandle, - target: ScreenCaptureTarget, -) -> Result { - use crate::NewScreenshotAdded; - use crate::notifications; - use crate::{PendingScreenshot, PendingScreenshots}; - use cap_recording::screenshot::capture_screenshot; - use image::ImageEncoder; - use std::time::Instant; - - let general_settings = GeneralSettingsStore::get(&app).ok().flatten(); - let general_settings = general_settings.as_ref(); - - let project_name = format_project_name( - general_settings - .and_then(|s| s.default_project_name_template.clone()) - .as_deref(), - target.title().as_deref().unwrap_or("Unknown"), - target.kind_str(), - RecordingMode::Screenshot, - None, - ); - - let mut hid_any = false; - for (label, window) in app.webview_windows() { - if let Ok(id) = CapWindowId::from_str(&label) - && matches!( - id, - CapWindowId::TargetSelectOverlay { .. } - | CapWindowId::WindowCaptureOccluder { .. } - | CapWindowId::CaptureArea - | CapWindowId::ModeSelect - | CapWindowId::RecordingsOverlay - ) - { - hide_overlay(&window); - hid_any = true; - } - } - - if hid_any { - tokio::time::sleep(std::time::Duration::from_millis(150)).await; - } - - let automation_target = target.clone(); - - let image = capture_screenshot(target) - .await - .map_err(|e| format!("Failed to capture screenshot: {e}"))?; - - AppSounds::Notification.play(); - - let image_width = image.width(); - let image_height = image.height(); - let channels: u32 = match &image { - image::DynamicImage::ImageRgba8(_) => 4, - _ => 3, - }; - let color_type = if channels == 4 { - image::ColorType::Rgba8 - } else { - image::ColorType::Rgb8 - }; - let image_data = image.into_bytes(); - - let filename = project_name.replace(":", "."); - let filename = format!("{}.cap", sanitize_filename::sanitize(&filename)); - - let screenshots_base_dir = app.path().app_data_dir().unwrap().join("screenshots"); - - let project_file_path = screenshots_base_dir.join(&cap_utils::ensure_unique_filename( - &filename, - &screenshots_base_dir, - )?); - - ensure_dir(&project_file_path) - .map_err(|e| format!("Failed to create screenshots directory: {e}"))?; - - let image_filename = "original.png"; - let image_path = project_file_path.join(image_filename); - let cap_dir_key = project_file_path.to_string_lossy().to_string(); - - let pending_screenshots = app.state::(); - pending_screenshots.insert( - cap_dir_key.clone(), - PendingScreenshot { - data: image_data.clone(), - width: image_width, - height: image_height, - channels, - created_at: Instant::now(), - }, - ); - - let relative_path = relative_path::RelativePathBuf::from(image_filename); - - let video_meta = cap_project::VideoMeta { - path: relative_path, - fps: 0, - start_time: Some(0.0), - device_id: None, - }; - - let segment = cap_project::SingleSegment { - display: video_meta, - camera: None, - audio: None, - cursor: None, - }; - - let meta = cap_project::RecordingMeta { - platform: Some(Platform::default()), - project_path: project_file_path.clone(), - pretty_name: project_name, - sharing: None, - inner: cap_project::RecordingMetaInner::Studio(Box::new( - cap_project::StudioRecordingMeta::SingleSegment { segment }, - )), - upload: None, - }; - - meta.save_for_project() - .map_err(|e| format!("Failed to save recording meta: {e}"))?; - - let mut screenshot_config = cap_project::ProjectConfiguration::default(); - screenshot_config.background.source = cap_project::BackgroundSource::Color { - value: [255, 255, 255], - alpha: 0, - }; - screenshot_config.background.shadow = 0.0; - screenshot_config - .write(&project_file_path) - .map_err(|e| format!("Failed to save project config: {e}"))?; - - let is_large_capture = (image_width as u64).saturating_mul(image_height as u64) > 8_000_000; - let compression = if is_large_capture { - image::codecs::png::CompressionType::Fast - } else { - image::codecs::png::CompressionType::Default - }; - let image_path_for_emit = image_path.clone(); - let image_path_for_write = image_path.clone(); - let app_handle = app.clone(); - let pending_state = PendingScreenshots(pending_screenshots.0.clone()); - - tauri::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}"))?; - let encoder = image::codecs::png::PngEncoder::new_with_quality( - std::io::BufWriter::new(file), - compression, - image::codecs::png::FilterType::Adaptive, - ); - - ImageEncoder::write_image( - encoder, - &image_data, - image_width, - image_height, - color_type.into(), - ) - .map_err(|e| format!("Failed to encode PNG: {e}")) - }) - .await; - - pending_state.remove(&cap_dir_key); - - match encode_result { - Ok(Ok(())) => { - let _ = NewScreenshotAdded { - path: image_path_for_emit.clone(), - } - .emit(&app_handle); - - crate::automation::run_screenshot_automations( - app_handle.clone(), - image_path_for_emit.clone(), - &automation_target, - ); - - notifications::send_notification( - &app_handle, - notifications::NotificationType::ScreenshotSaved, - ); - } - Ok(Err(e)) => { - error!("Failed to encode PNG: {e}"); - notifications::send_notification( - &app_handle, - notifications::NotificationType::ScreenshotSaveFailed, - ); - } - Err(e) => { - error!("Failed to join screenshot encoding task: {e}"); - notifications::send_notification( - &app_handle, - notifications::NotificationType::ScreenshotSaveFailed, - ); - } - } - }); - - Ok(image_path) -} - -async fn handle_recording_end( - handle: AppHandle, - recording: Result, - app: &mut App, - recording_dir: PathBuf, -) -> Result<(), String> { - handle_recording_end_inner(handle, recording, app, recording_dir, false, None).await -} - -async fn handle_recording_end_inner( - handle: AppHandle, - recording: Result, - app: &mut App, - recording_dir: PathBuf, - preserve_editor_target: bool, - finalization_project: Option, String>>, -) -> Result<(), String> { - #[cfg(target_os = "linux")] - if let Some(InProgressRecording::Studio { - handle: studio, - common, - .. - }) = app.current_recording() - && common.recording_dir == recording_dir - && studio.lifecycle().quiescence() != studio_recording::StudioQuiescence::Joined - { - return Err("Studio capture cleanup is unconfirmed; active recording retained".into()); - } - #[cfg(any(target_os = "macos", windows))] - if let Some(InProgressRecording::Studio { - handle: studio, - common, - .. - }) = app.current_recording() - && common.recording_dir == recording_dir - && !studio.stop_acknowledged() - { - return Err("Studio stop is unconfirmed; active recording retained".into()); - } - #[cfg(target_os = "linux")] - if linux_instant::current(&handle) - .is_some_and(|attempt| attempt.owns_directory(&recording_dir) && !attempt.capture_joined()) - { - return Err("Instant capture cleanup is unconfirmed; recording state retained".into()); - } - #[cfg(target_os = "linux")] - let strict_attempt = - linux_instant::current(&handle).filter(|attempt| attempt.owns_directory(&recording_dir)); - #[cfg(target_os = "linux")] - let mut recording = match &strict_attempt { - Some(attempt) => attempt.checked(recording), - None => recording, - }; - let clean_generation = crate::clean_capture::owner(&handle, &recording_dir); - if crate::clean_capture::phase(&handle).is_some() && clean_generation.is_none() { - return Ok(()); - } - let camera_snapshot = match &recording { - Ok(CompletedRecording::Studio { - recording, - capture_target, - .. - }) => { - let mut snapshot = app - .current_recording() - .and_then(|current| current.common().camera_snapshot.get()) - .cloned() - .unwrap_or_else(|| StudioCameraSnapshot::capture(&handle, app, capture_target)); - if recording.meta.camera_path().is_none() { - snapshot.placement = None; - } - Some(snapshot) - } - _ => None, - }; - let cleared = app.clear_recording_state(); - #[cfg(any(target_os = "linux", target_os = "macos", windows))] - if let Some(InProgressRecording::Studio { - handle: studio, - common, - .. - }) = &cleared - && let Some(owner) = studio_stop_registry(&handle).retire_identity( - studio, - &common.recording_dir, - clean_generation, - ) - { - crate::clean_capture::confirm_stop_notice(&handle, &owner); - } - - #[cfg(not(target_os = "linux"))] - let mut cleared = cleared; - - if let Some(in_progress) = cleared.as_ref() { - let mode = in_progress.inputs().mode; - let (snapshot, duration_secs, mic_feed) = { - match in_progress { - InProgressRecording::Instant { - common, mic_feed, .. - } - | InProgressRecording::Studio { - common, mic_feed, .. - } => ( - common.health.snapshot(), - common.health.seconds_since_start() as u64, - mic_feed.clone(), - ), - } - }; - let drop_rate_pct = 0.0_f64; - let dropped_mic_messages = match mic_feed { - Some(feed) => feed.dropped_message_count().await, - None => 0, - }; - #[cfg(target_os = "linux")] - if let Some(attempt) = &strict_attempt { - recording = attempt.checked(recording); - } - let (status, error_class) = match &recording { - Ok(_) => ("stopped", None), - Err(e) => ("failed", Some(classify_error_message(e.as_str()))), - }; - crate::telemetry::async_capture_event( - &handle, - crate::telemetry::AnalyticsEvent::RecordingCompleted { - mode: crate::recording_telemetry::mode_label(mode), - status, - duration_secs, - segment_count: 1, - track_failure_count: 0, - error_class, - video_frames_captured: 0, - video_frames_dropped: 0, - drop_rate_pct, - capture_stalls_count: snapshot.capture_stalls_count, - capture_stalls_max_ms: snapshot.capture_stalls_max_ms, - mixer_stalls_count: snapshot.mixer_stalls_count, - mixer_stalls_max_ms: snapshot.mixer_stalls_max_ms, - audio_gaps_count: snapshot.audio_gaps_count, - audio_gaps_total_ms: snapshot.audio_gaps_total_ms, - frame_drop_rate_high_count: snapshot.frame_drop_rate_high_count, - source_restarts_count: snapshot.source_restarts_count, - muxer_crash_count: snapshot.muxer_crash_count, - audio_degraded_count: snapshot.audio_degraded_count, - dropped_mic_messages, - }, - ); - } - - app.disconnected_inputs.clear(); - app.camera_in_use = false; - - if recording.is_err() - && let Some(InProgressRecording::Instant { - segment_upload, - video_upload_info, - .. - }) = cleared.as_ref() - { - info!("Ending upload after recording failure"); - #[cfg(not(target_os = "linux"))] - segment_upload.session.cancel(); - #[cfg(target_os = "linux")] - { - let _ = segment_upload; - if let Some(attempt) = linux_instant::current(&handle) { - attempt.upload().deny(); - } - } - crate::upload::emit_upload_complete(&handle, &video_upload_info.id); - } - - #[cfg(not(target_os = "linux"))] - if recording.is_err() - && let Some(InProgressRecording::Instant { segment_upload, .. }) = cleared.take() - { - let session = segment_upload.session.clone(); - let _ = crate::upload::lifecycle::supervise(handle.clone(), session, async move { - segment_upload - .handle - .await - .map_err(|error| error.to_string())? - }) - .await; - } - drop(cleared); - - if app.was_camera_only_recording { - app.was_camera_only_recording = false; - } - - #[cfg(target_os = "linux")] - if let Some(attempt) = &strict_attempt { - recording = attempt.checked(recording); - } - let res = match recording { - // we delay reporting errors here so that everything else happens first - Ok(recording) => Some( - handle_recording_finish(&handle, recording, finalization_project, camera_snapshot) - .await, - ), - Err(error) => { - if let Ok(mut project_meta) = - RecordingMeta::load_for_project(&recording_dir).map_err(|err| { - error!("Error loading recording meta while finishing recording: {err}") - }) - { - match &mut project_meta.inner { - RecordingMetaInner::Studio(meta) => { - if let StudioRecordingMeta::MultipleSegments { inner } = &mut **meta { - inner.status = Some(StudioRecordingStatus::Failed { error }); - } - } - RecordingMetaInner::Instant(meta) => { - *meta = InstantRecordingMeta::Failed { error }; - } - } - project_meta - .save_for_project() - .map_err(|err| { - error!("Error saving recording meta while finishing recording: {err}") - }) - .ok(); - } - - None - } - }; - - let _ = RecordingStopped.emit(&handle); - - let _ = app.recording_logging_handle.reload(None); - - if let Some(window) = CapWindowId::RecordingControls.get(&handle) { - let _ = window.hide(); - } - - crate::target_select_overlay::close_target_select_overlay_windows(&handle); - - if let Some(camera) = CapWindowId::Camera.get(&handle) { - let _ = camera.hide(); - } - - app.camera_preview.pause(); - app.applied_mic_input.invalidate(); - let _ = app.mic_feed.ask(microphone::RemoveInput).await; - let _ = app.camera_feed.ask(camera::RemoveInput).await; - - let main_window = CapWindowId::Main.get(&handle); - - // When the finish path handed the foreground to an editor window, leave - // the main window alone: un-minimizing it here (Windows `Close` behaviour - // minimizes; macOS `Minimise` miniaturizes) would restore it on top of the - // editor that just opened. - let mut editor_took_foreground = matches!(&res, Some(Ok(true))); - - if let Some(window) = main_window { - if !editor_took_foreground && clean_generation.is_none() { - window.unminimize().ok(); - } - let requested = handle.state::().snapshot(); - if clean_generation.is_none() - && !requested.microphone.pending - && requested.microphone.error.is_none() - && requested.microphone.value == app.selected_mic_label - && let Err(err) = app.ensure_selected_mic_ready().await - { - warn!("Failed to restore microphone preview after recording: {err}"); - } - } else { - app.selected_mic_label = None; - app.selected_camera_id = None; - } - - // Fallback for in-editor recordings that did NOT reach - // `apply_post_studio_editor_behaviour` (failed/cancelled recordings, or - // non-studio modes). On the studio success path `handle_recording_finish` - // — awaited above into `res` — already consumed the target and emitted - // `EditorRecordingAdded`, so this `take()` returns `None` and is a no-op. - // Using `take()` (not `current()`) here is deliberate: it restores the - // editor window AND clears any stale target so it can't leak into the next - // recording session. - if let Some(editor_path) = take_editor_target_after_recording( - &EditorRecordingTarget::get(&handle), - preserve_editor_target, - ) { - crate::editor_recording::finish(&handle); - if crate::editor_recording::reveal_editor(&handle, &editor_path) { - editor_took_foreground = true; - } - } - - CurrentRecordingChanged.emit(&handle).ok(); - if let Some(generation) = clean_generation { - crate::clean_capture::release_after_recording(&handle, generation, editor_took_foreground); - } - - if let Some(res) = res { - let _editor_took_foreground: bool = res?; - } - - Ok(()) -} - -fn compute_studio_duration_secs(recording_dir: &std::path::Path) -> f64 { - let Ok(meta) = RecordingMeta::load_for_project(recording_dir) else { - return 0.0; - }; - let Some(studio_meta) = meta.studio_meta() else { - return 0.0; - }; - ProjectRecordingsMeta::new(&recording_dir.to_path_buf(), studio_meta) - .map(|r| r.duration()) - .unwrap_or(0.0) -} - -/// Returns `true` when an editor window took the foreground (in-editor -/// re-record, or the post-recording behaviour opened the editor). Callers use -/// this to keep the main window suppressed so it can't cover the editor. -async fn apply_post_studio_editor_behaviour( - app: &AppHandle, - recording_dir: PathBuf, - duration_secs: f64, -) -> bool { - if let Some(editor_path) = EditorRecordingTarget::take(app) { - crate::editor_recording::finish(app); - crate::editor_recording::reveal_editor(app, &editor_path); - - let _ = EditorRecordingAdded { - editor_path, - recording_path: recording_dir, - } - .emit(app); - - return true; - } - - let default = GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|v| v.post_studio_recording_behaviour) - .unwrap_or(PostStudioRecordingBehaviour::OpenEditor); - - match crate::automation::studio_recording_editor_behaviour( - app, - &recording_dir, - duration_secs, - default, - ) { - Some(PostStudioRecordingBehaviour::OpenEditor) => { - let _ = ShowCapWindow::Editor { - project_path: recording_dir, - } - .show(app) - .await; - - true - } - Some(PostStudioRecordingBehaviour::ShowOverlay) => { - let _ = ShowCapWindow::RecordingsOverlay.show(app).await; - - let app = AppHandle::clone(app); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(1000)).await; - let _ = NewStudioRecordingAdded { - path: recording_dir, - } - .emit(&app); - }); - - false - } - None => { - let _ = NewStudioRecordingAdded { - path: recording_dir, - } - .emit(app); - - false - } - } -} - -// runs when a recording successfully finishes; Ok(true) means an editor -// window took the foreground and the main window must stay suppressed -async fn handle_recording_finish( - app: &AppHandle, - completed_recording: CompletedRecording, - finalization_project: Option, String>>, - camera_snapshot: Option, -) -> Result { - let recording_dir = completed_recording.project_path().clone(); - - let screenshots_dir = recording_dir.join("screenshots"); - std::fs::create_dir_all(&screenshots_dir).ok(); - - let (meta_inner, sharing) = match completed_recording { - CompletedRecording::Studio { - recording, - capture_target, - .. - } => { - let meta_inner = RecordingMetaInner::Studio(Box::new(recording.meta.clone())); - - if let Ok(mut meta) = RecordingMeta::load_for_project(&recording_dir).map_err(|err| { - error!("Failed to load recording meta while saving finished recording: {err}") - }) { - meta.inner = meta_inner.clone(); - meta.sharing = None; - meta.save_for_project() - .map_err(|e| format!("Failed to save recording meta: {e}"))?; - } - - let needs_remux = needs_fragment_remux(&recording_dir, &recording.meta); - - if needs_remux { - info!( - "Recording has fragments queued for finalization - opening editor immediately" - ); - - let project = finalization_project.ok_or_else(|| { - crate::recoverable_finalization_error( - &recording_dir, - "Recording directory was not admitted for finalization.".into(), - ) - })??; - let finalizing_state = app.state::(); - let finalization = finalizing_state.start_finalizing(project.clone())?; - - let duration = compute_studio_duration_secs(&recording_dir); - let editor_took_foreground = - apply_post_studio_editor_behaviour(app, recording_dir.clone(), duration).await; - - AppSounds::StopRecording.play(); - - let app = app.clone(); - let recording_dir_for_finalize = recording_dir.clone(); - let default_preset = PresetsStore::get_default_preset(&app) - .ok() - .flatten() - .map(|p| p.config); - - tokio::spawn(async move { - let result = finalize_studio_recording( - &app, - project, - recording, - default_preset, - Some(capture_target), - finalization.preparing(), - camera_snapshot, - ) - .await; - - match &result { - Ok(()) => { - let duration = - compute_studio_duration_secs(&recording_dir_for_finalize); - crate::automation::run_studio_recording_automations( - app.clone(), - recording_dir_for_finalize.clone(), - duration, - ); - } - Err(e) => error!("Failed to finalize recording: {e}"), - } - - finalization.finish(result); - }); - - return Ok(editor_took_foreground); - } - - let updated_studio_meta = recording.meta.clone(); - - let display_output_path = match &updated_studio_meta { - StudioRecordingMeta::SingleSegment { segment } => { - segment.display.path.to_path(&recording_dir) - } - StudioRecordingMeta::MultipleSegments { inner, .. } => { - inner.segments[0].display.path.to_path(&recording_dir) - } - }; - - let display_screenshot = screenshots_dir.join("display.jpg"); - tokio::spawn(create_screenshot( - display_output_path, - display_screenshot.clone(), - None, - )); - - let recordings = ProjectRecordingsMeta::new(&recording_dir, &updated_studio_meta)?; - - let config = project_config_from_recording( - app, - &cap_recording::studio_recording::CompletedRecording { - project_path: recording.project_path, - meta: updated_studio_meta.clone(), - cursor_data: recording.cursor_data, - clean_stopped: None, - }, - &recordings, - PresetsStore::get_default_preset(app)?.map(|p| p.config), - Some(&capture_target), - stored_current_desktop_background_path(&recording_dir), - camera_snapshot.as_ref(), - ); - - config.write(&recording_dir).map_err(|e| e.to_string())?; - - ( - RecordingMetaInner::Studio(Box::new(updated_studio_meta)), - None, - ) - } - CompletedRecording::Instant { - #[cfg(target_os = "linux")] - finalized, - recording, - segment_upload, - video_upload_info, - .. - } => { - #[cfg(target_os = "linux")] - if finalized { - if let Some(attempt) = linux_instant::current(app) { - attempt.checked(Ok(()))?; - } - return Ok(false); - } - if !recording.health.is_uploadable() - && let cap_recording::RecordingHealth::Damaged { ref reason } = recording.health - { - error!( - reason, - "Instant recording is damaged and cannot be uploaded" - ); - RecordingEvent::Failed { - error: format!("Recording output is damaged: {reason}"), - } - .emit(app) - .ok(); - return Ok(false); - } - - AppSounds::StopRecording.play(); - use tauri_plugin_clipboard_manager::ClipboardExt; - let _ = app.clipboard().write_text(video_upload_info.link.clone()); - let _ = open_external_link( - app.clone(), - recording_stopped_share_url(&video_upload_info.link), - ); - - let app = app.clone(); - let is_camera_only = - matches!(recording.display_source, ScreenCaptureTarget::CameraOnly); - - let display_screenshot = screenshots_dir.join("display.jpg"); - let screenshot_task = if is_camera_only { - let output_mp4 = recording_dir.join("content/output.mp4"); - tokio::spawn({ - let display_screenshot = display_screenshot.clone(); - async move { create_screenshot(output_mp4, display_screenshot, None).await } - }) - } else { - let segments_dir = recording_dir.join("content/display"); - tokio::spawn({ - let display_screenshot = display_screenshot.clone(); - async move { - let screenshot_source: Result = - create_screenshot_source_from_segments(&segments_dir).await; - match screenshot_source { - Ok(temp_path) => { - let result = - create_screenshot(temp_path.clone(), display_screenshot, None) - .await; - let _ = tokio::fs::remove_file(&temp_path).await; - result - } - Err(e) => Err(format!("Failed to create screenshot source: {e}")), - } - } - }) - }; - - #[cfg(not(target_os = "linux"))] - let session = segment_upload.session.clone(); - #[cfg(target_os = "linux")] - let session = segment_upload.lock().await.session.clone(); - session - .persist_local_complete( - recording.meta.clone(), - SharingMeta { - id: video_upload_info.id.clone(), - link: video_upload_info.link.clone(), - content_hash: None, - }, - ) - .map_err(|error| error.to_string())?; - session - .mark_ready(false) - .map_err(|error| error.to_string())?; - let job_session = session.clone(); - crate::upload::lifecycle::supervise(app.clone(), session, { - let video = video_upload_info.clone(); - let recording_dir = recording_dir.clone(); - async move { - let uploaded = await_instant_upload(segment_upload).await; - let screenshot = screenshot_task.await; - uploaded.map_err(AuthedApiError::from)??; - screenshot - .map_err(|error| error.to_string())? - .map_err(AuthedApiError::from)?; - job_session.check()?; - let bytes = compress_image(display_screenshot).await?; - crate::upload::singlepart_uploader( - app.clone(), - crate::api::PresignedS3PutRequest { - video_id: video.id.clone(), - subpath: "screenshot/screen-capture.jpg".into(), - method: PresignedS3PutRequestMethod::Put, - meta: None, - }, - bytes.len() as u64, - stream::once( - async move { Ok::<_, std::io::Error>(bytes::Bytes::from(bytes)) }, - ), - ) - .await?; - job_session.complete_locally(&app).await?; - crate::automation::run_upload_completed_automations( - app, - recording_dir, - Some(video.link), - Some(video.id), - ); - Ok(()) - } - }) - .await - .map_err(|error| error.to_string())?; - - ( - RecordingMetaInner::Instant(recording.meta), - Some(SharingMeta { - link: video_upload_info.link, - id: video_upload_info.id, - content_hash: None, - }), - ) - } - }; - - let instant_share = sharing.as_ref().map(|s| (s.link.clone(), s.id.clone())); - - if let RecordingMetaInner::Instant(_) = &meta_inner { - let (link, id) = match instant_share { - Some((link, id)) => (Some(link), Some(id)), - None => (None, None), - }; - crate::automation::run_instant_recording_automations( - app.clone(), - recording_dir.clone(), - link, - id, - ); - } - - let mut editor_took_foreground = false; - if let RecordingMetaInner::Studio(_) = meta_inner { - let duration = compute_studio_duration_secs(&recording_dir); - crate::automation::run_studio_recording_automations( - app.clone(), - recording_dir.clone(), - duration, - ); - editor_took_foreground = - apply_post_studio_editor_behaviour(app, recording_dir, duration).await; - AppSounds::StopRecording.play(); - } - - Ok(editor_took_foreground) -} - -async fn finalize_studio_recording( - app: &AppHandle, - project: Arc, - recording: cap_recording::studio_recording::CompletedRecording, - default_preset: Option, - capture_target: Option, - preparing: crate::preparing_finalization::FinalizationPreparing, - camera_snapshot: Option, -) -> Result<(), String> { - info!("Starting background finalization for recording"); - project.validate_async().await?; - preparing.set_presentation( - preparing_presentation_snapshot(default_preset.as_ref(), capture_target.as_ref()).map( - |mut config| { - apply_studio_sound_default(app, &mut config); - if let Some(snapshot) = &camera_snapshot { - snapshot.apply(&mut config); - } - config - }, - ), - ); - let recording_dir = project.work_path().to_path_buf(); - let screenshots_dir = recording_dir.join("screenshots"); - let display_path = project.display_path().to_path_buf(); - let recording_dir_for_remux = recording_dir.clone(); - let app_for_remux = app.clone(); - let (remux_result, recording) = tokio::task::spawn_blocking(move || { - let result = remux_fragmented_recording_with_preparing( - &recording_dir_for_remux, - &display_path, - "recording_stop", - Some(&app_for_remux), - Some((&preparing, &recording)), - ); - (result, recording) - }) - .await - .map_err(|e| format!("Recording finalization task panicked: {e}"))?; - - if let Err(e) = remux_result { - error!("Failed to finalize fragmented recording: {e}"); - return Err(e); - } - - let updated_meta = RecordingMeta::load_for_project(&recording_dir) - .map_err(|e| format!("Failed to reload recording meta: {e}"))?; - let updated_studio_meta = updated_meta - .studio_meta() - .ok_or_else(|| "Expected studio meta after remux".to_string())? - .clone(); - - let display_output_path = match &updated_studio_meta { - StudioRecordingMeta::SingleSegment { segment } => { - segment.display.path.to_path(&recording_dir) - } - StudioRecordingMeta::MultipleSegments { inner, .. } => { - inner.segments[0].display.path.to_path(&recording_dir) - } - }; - - let display_screenshot = screenshots_dir.join("display.jpg"); - tokio::spawn(create_screenshot( - display_output_path, - display_screenshot, - None, - )); - - let recordings = ProjectRecordingsMeta::new(&recording_dir, &updated_studio_meta) - .map_err(|e| format!("Failed to create project recordings meta: {e}"))?; - - let config = project_config_from_recording( - app, - &cap_recording::studio_recording::CompletedRecording { - project_path: recording.project_path, - meta: updated_studio_meta, - cursor_data: recording.cursor_data, - clean_stopped: None, - }, - &recordings, - default_preset, - capture_target.as_ref(), - stored_current_desktop_background_path(&recording_dir), - camera_snapshot.as_ref(), - ); - - config - .write(&recording_dir) - .map_err(|e| format!("Failed to write project config: {e}"))?; - - project.validate_async().await?; - info!("Background finalization completed for recording"); - - Ok(()) -} - -pub const DEFAULT_AUTO_ZOOM_AMOUNT: f64 = 2.0; - -fn generate_zoom_segments_from_clicks_impl( - mut clicks: Vec, - _moves: Vec, - max_duration: f64, - zoom_amount: f64, -) -> Vec { - const MS_PER_SECOND: f64 = 1000.0; - const START_MIN_MS: f64 = 1.0; - const CLICK_PRE_PADDING_MS: f64 = 300.0; - const CLICK_POST_PADDING_MS: f64 = 2500.0; - const CLICK_END_CLAMP_PADDING_MS: f64 = 800.0; - const TRAILING_CLICK_IGNORE_MS: f64 = 1000.0; - const MERGE_GAP_MS: f64 = 2500.0; - - if max_duration <= 0.0 { - return Vec::new(); - } - - let duration_ms = max_duration * MS_PER_SECOND; - let click_cutoff_ms = duration_ms - TRAILING_CLICK_IGNORE_MS; - let end_limit_ms = duration_ms - CLICK_END_CLAMP_PADDING_MS; - if click_cutoff_ms <= 0.0 || end_limit_ms <= START_MIN_MS { - return Vec::new(); - } - - clicks.sort_by(|a, b| { - a.time_ms - .partial_cmp(&b.time_ms) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - let mut intervals: Vec<(f64, f64)> = Vec::new(); - for click in clicks { - let time_ms = click.time_ms.floor(); - if time_ms >= click_cutoff_ms { - continue; - } - - let start = (time_ms - CLICK_PRE_PADDING_MS).max(START_MIN_MS); - let end = (time_ms + CLICK_POST_PADDING_MS).min(end_limit_ms); - - if end > start { - intervals.push((start, end)); - } - } - - if intervals.is_empty() { - return Vec::new(); - } - - intervals.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); - - let mut merged: Vec<(f64, f64)> = Vec::new(); - for interval in intervals { - if let Some(last) = merged.last_mut() - && interval.0 <= last.1 + MERGE_GAP_MS - { - last.1 = last.1.max(interval.1); - continue; - } - merged.push(interval); - } - - merged - .into_iter() - .map(|(start, end)| ZoomSegment { - start: start.round() / MS_PER_SECOND, - end: end.round() / MS_PER_SECOND, - amount: zoom_amount, - mode: ZoomMode::Auto, - glide_direction: GlideDirection::None, - glide_speed: 0.5, - instant_animation: false, - edge_snap_ratio: 0.25, - }) - .collect() -} - -/// Generates zoom segments based on mouse click events during recording. -/// Used during the recording completion process. -pub fn generate_zoom_segments_from_clicks( - recording: &studio_recording::CompletedRecording, - recordings: &ProjectRecordingsMeta, - zoom_amount: f64, -) -> Vec { - // Build a temporary RecordingMeta so we can use the common implementation - let recording_meta = RecordingMeta { - platform: None, - project_path: recording.project_path.clone(), - pretty_name: String::new(), - sharing: None, - inner: RecordingMetaInner::Studio(Box::new(recording.meta.clone())), - upload: None, - }; - - generate_zoom_segments_for_project(&recording_meta, recordings, zoom_amount) -} - -/// Generates zoom segments from clicks for an existing project. -/// Used in the editor context where we have RecordingMeta. -pub fn generate_zoom_segments_for_project( - recording_meta: &RecordingMeta, - recordings: &ProjectRecordingsMeta, - zoom_amount: f64, -) -> Vec { - let RecordingMetaInner::Studio(studio_meta) = &recording_meta.inner else { - return Vec::new(); - }; - - let mut all_clicks = Vec::new(); - let mut all_moves = Vec::new(); - - match &**studio_meta { - StudioRecordingMeta::SingleSegment { segment } => { - if let Some(cursor_path) = &segment.cursor { - let mut events = CursorEvents::load_from_file(&recording_meta.path(cursor_path)) - .unwrap_or_default(); - let pointer_ids = studio_meta.pointer_cursor_ids(); - let pointer_ids_ref = (!pointer_ids.is_empty()).then_some(&pointer_ids); - events.stabilize_short_lived_cursor_shapes( - pointer_ids_ref, - SHORT_CURSOR_SHAPE_DEBOUNCE_MS, - ); - all_clicks = events.clicks; - all_moves = events.moves; - } - } - StudioRecordingMeta::MultipleSegments { inner, .. } => { - for segment in inner.segments.iter() { - let events = segment.cursor_events(recording_meta); - all_clicks.extend(events.clicks); - all_moves.extend(events.moves); - } - } - } - - generate_zoom_segments_from_clicks_impl( - all_clicks, - all_moves, - recordings.duration(), - zoom_amount, - ) -} - -fn apply_studio_sound_default(app: &AppHandle, config: &mut ProjectConfiguration) { - let audio_enhancement = app - .store("store") - .ok() - .and_then(|store| store.get("audio_enhancement")); - if audio_enhancement - .as_ref() - .and_then(|value| { - value - .get("enabledByDefault") - .and_then(serde_json::Value::as_bool) - }) - .unwrap_or(true) - { - config.audio.improve = true; - config.audio.isolation = audio_enhancement - .and_then(|value| value.get("isolation").cloned()) - .and_then(|value| serde_json::from_value(value).ok()) - .unwrap_or_default(); - } -} - -fn project_config_from_recording( - app: &AppHandle, - completed_recording: &studio_recording::CompletedRecording, - recordings: &ProjectRecordingsMeta, - default_config: Option, - capture_target: Option<&ScreenCaptureTarget>, - stored_desktop_background_path: Option, - camera_snapshot: Option<&StudioCameraSnapshot>, -) -> ProjectConfiguration { - let settings = GeneralSettingsStore::get(app) - .unwrap_or(None) - .unwrap_or_default(); - - let using_default_config = default_config.is_none(); - let mut config = default_config.unwrap_or_default(); - apply_studio_sound_default(app, &mut config); - if using_default_config { - let library = app - .store("store") - .ok() - .and_then(|store| store.get("animated_gradients")) - .and_then(|value| serde_json::from_value(value).ok()); - apply_animated_gradient_default( - &mut config, - library.as_ref(), - using_default_config, - capture_target, - ); - } - config.cursor.size = cap_project::CursorConfiguration::default().size; - apply_recording_presentation_defaults( - app, - &mut config, - capture_target, - using_default_config, - stored_desktop_background_path, - ); - - if let Some(snapshot) = camera_snapshot { - snapshot.apply(&mut config); - } - - let timeline_segments = recordings - .segments - .iter() - .enumerate() - .map(|(i, segment)| TimelineSegment { - recording_clip: i as u32, - start: 0.0, - end: segment.duration(), - timescale: 1.0, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }) - .collect::>(); - - let zoom_segments = if settings.auto_zoom_on_clicks { - generate_zoom_segments_from_clicks( - completed_recording, - recordings, - settings - .default_zoom_amount - .unwrap_or(DEFAULT_AUTO_ZOOM_AMOUNT), - ) - } else { - Vec::new() - }; - - if should_enable_notch_overlay( - capture_target, - settings.macbook_notch_overlay.unwrap_or(false), - completed_recording.meta.display_notch().is_some(), - ) { - config.background.notch = Some(cap_project::NotchConfiguration { - enabled: true, - ..Default::default() - }); - } - - config.timeline = Some(recording_timeline(timeline_segments, zoom_segments)); - - config -} - -pub(crate) fn recording_timeline( - segments: Vec, - zoom_segments: Vec, -) -> TimelineConfiguration { - TimelineConfiguration { - segments, - transitions: Vec::new(), - zoom_segments, - scene_segments: Vec::new(), - style_segments: Vec::new(), - image_segments: Vec::new(), - mask_segments: Vec::new(), - text_segments: Vec::new(), - caption_segments: Vec::new(), - keyboard_segments: Vec::new(), - audio_segments: Vec::new(), - camera3d_segments: Vec::new(), - } -} - -#[derive(Clone)] -struct StudioCameraSnapshot { - state: crate::camera::CameraPreviewState, - placement: Option, -} - -impl StudioCameraSnapshot { - fn capture(app: &AppHandle, state: &App, target: &ScreenCaptureTarget) -> Self { - Self { - state: state.camera_preview.get_state().unwrap_or_default(), - placement: state - .camera_in_use - .then(|| studio_camera_placement(app, target)) - .flatten(), - } - } - - fn apply(&self, config: &mut ProjectConfiguration) { - apply_recording_camera_preview_state(config, &self.state); - if let Some(placement) = self.placement { - placement.apply(&mut config.camera); - } - } -} - -fn studio_camera_placement( - app: &AppHandle, - target: &ScreenCaptureTarget, -) -> Option { - if matches!(target, ScreenCaptureTarget::CameraOnly) { - return None; - } - let window = CapWindowId::Camera.get(app)?; - let scale = window.scale_factor().ok()?; - if !scale.is_finite() || scale <= 0.0 { - return None; - } - let position = window.inner_position().ok()?; - let size = window.inner_size().ok()?; - let toolbar = 56.0 * scale; - #[cfg(target_os = "macos")] - let units = scale; - #[cfg(not(target_os = "macos"))] - let units = 1.0; - cap_recording::camera_placement::recording_camera_placement( - target, - [ - f64::from(position.x) / units, - (f64::from(position.y) + toolbar) / units, - f64::from(size.width) / units, - (f64::from(size.height) - toolbar) / units, - ], - ) -} - -fn apply_recording_camera_preview_state( - config: &mut ProjectConfiguration, - camera_preview_state: &crate::camera::CameraPreviewState, -) { - match camera_preview_state.shape { - CameraPreviewShape::Round => { - config.camera.shape = CameraShape::Square; - config.camera.rounding = 100.0; - config.camera.rounding_type = CornerStyle::Rounded; - } - CameraPreviewShape::Square => { - config.camera.shape = CameraShape::Square; - config.camera.rounding = 25.0; - } - CameraPreviewShape::Full => { - config.camera.shape = CameraShape::Source; - config.camera.rounding = 25.0; - } - } - - config.camera.background_blur = cap_project::BackgroundBlurConfig { - mode: camera_preview_state.background_blur, - }; -} - -fn should_enable_notch_overlay( - capture_target: Option<&ScreenCaptureTarget>, - setting_enabled: bool, - has_recorded_notch: bool, -) -> bool { - setting_enabled - && has_recorded_notch - && matches!( - capture_target, - Some(ScreenCaptureTarget::Display { .. } | ScreenCaptureTarget::Area { .. }) - ) -} - -fn apply_recording_presentation_defaults( - app: &AppHandle, - config: &mut ProjectConfiguration, - capture_target: Option<&ScreenCaptureTarget>, - using_default_config: bool, - stored_desktop_background_path: Option, -) { - let default_wallpaper_path = if using_default_config { - stored_desktop_background_path.or_else(|| { - app.path() - .resolve("assets/backgrounds/cities/sf.jpg", BaseDirectory::Resource) - .ok() - .map(|path| path.to_string_lossy().into_owned()) - }) - } else { - None - }; - - apply_screen_recording_presentation_defaults( - config, - capture_target, - using_default_config, - default_wallpaper_path, - ); -} - -fn apply_animated_gradient_default( - config: &mut ProjectConfiguration, - library: Option<&cap_project::AnimatedGradientLibrary>, - using_default_config: bool, - capture_target: Option<&ScreenCaptureTarget>, -) { - if using_default_config - && !matches!(capture_target, Some(ScreenCaptureTarget::CameraOnly)) - && let Some(library) = library - && library.selected - && let Some(gradient) = &library.last_used - { - config.background.source = cap_project::BackgroundSource::AnimatedGradient { - config: gradient.normalized(), - }; - } -} - -const DEFAULT_SCREEN_RECORDING_BACKGROUND_ROUNDING_PERCENT: f64 = 7.5; - -fn apply_screen_recording_presentation_defaults( - config: &mut ProjectConfiguration, - capture_target: Option<&ScreenCaptureTarget>, - using_default_config: bool, - default_wallpaper_path: Option, -) { - use cap_project::{BackgroundSource, ScreenMovementSpring}; - - if matches!(capture_target, Some(ScreenCaptureTarget::CameraOnly)) { - return; - } - - let has_default_background = matches!( - &config.background.source, - BackgroundSource::Color { value, alpha } if *value == [255, 255, 255] && *alpha == 255 - ); - - if using_default_config && has_default_background { - if let Some(path) = default_wallpaper_path { - config.background.source = BackgroundSource::Wallpaper { path: Some(path) }; - } - } - - if config.background.padding <= f64::EPSILON { - config.background.padding = 10.0; - } - - if matches!( - capture_target, - Some(ScreenCaptureTarget::Window { .. } | ScreenCaptureTarget::Display { .. }) - ) && config.background.rounding <= f64::EPSILON - { - config.background.rounding = DEFAULT_SCREEN_RECORDING_BACKGROUND_ROUNDING_PERCENT; - } - - if (config.screen_movement_spring.stiffness - 120.0).abs() < f32::EPSILON - && (config.screen_movement_spring.damping - 14.0).abs() < f32::EPSILON - && (config.screen_movement_spring.mass - 1.0).abs() < f32::EPSILON - { - config.screen_movement_spring = ScreenMovementSpring::default(); - } -} - -pub fn default_project_config() -> ProjectConfiguration { - let mut config = ProjectConfiguration::default(); - - apply_screen_recording_presentation_defaults(&mut config, None, false, None); - - if config.background.rounding <= f64::EPSILON { - config.background.rounding = DEFAULT_SCREEN_RECORDING_BACKGROUND_ROUNDING_PERCENT; - } - - config -} - -#[tauri::command] -#[specta::specta] -pub fn get_default_project_config() -> ProjectConfiguration { - default_project_config() -} - -pub fn needs_fragment_remux(recording_dir: &Path, meta: &StudioRecordingMeta) -> bool { - let StudioRecordingMeta::MultipleSegments { inner, .. } = meta else { - return false; - }; - - for segment in &inner.segments { - let display_path = segment.display.path.to_path(recording_dir); - if display_path.is_dir() { - return true; - } - } - - false -} - -pub const FRAGMENTED_EXPORT_FFMPEG_MARKER: &str = ".force-ffmpeg-export"; - -fn fragmented_export_ffmpeg_marker_path(recording_dir: &Path) -> PathBuf { - recording_dir.join(FRAGMENTED_EXPORT_FFMPEG_MARKER) -} - -fn mark_fragmented_recording_for_ffmpeg_export(recording_dir: &Path) -> Result<(), String> { - std::fs::write( - fragmented_export_ffmpeg_marker_path(recording_dir), - b"fragmented-remux", - ) - .map_err(|e| format!("Failed to mark recording for FFmpeg export: {e}")) -} - -pub(crate) fn remux_fragmented_recording( - project: &crate::FinalizationProject, -) -> Result<(), String> { - remux_fragmented_recording_with_trigger( - project.work_path(), - project.display_path(), - "manual_remux", - None, - ) -} - -pub fn remux_fragmented_recording_with_trigger( - recording_dir: &Path, - display_path: &Path, - trigger: &'static str, - app: Option<&AppHandle>, -) -> Result<(), String> { - remux_fragmented_recording_with_preparing(recording_dir, display_path, trigger, app, None) -} - -pub(crate) fn remux_fragmented_recording_with_preparing( - recording_dir: &Path, - display_path: &Path, - trigger: &'static str, - app: Option<&AppHandle>, - preparing: Option<( - &crate::preparing_finalization::FinalizationPreparing, - &studio_recording::CompletedRecording, - )>, -) -> Result<(), String> { - crate::recovery::ensure_finalization_storage(recording_dir, display_path)?; - let incomplete_recording = RecoveryManager::inspect_recording(recording_dir); - - if let Some(recording) = incomplete_recording { - let normal_stop = trigger == "recording_stop"; - let validation_start = std::time::Instant::now(); - let outcome = if normal_stop { - match preparing.and_then(|(publisher, completed)| publisher.claim(completed)) { - Some(job) => RecoveryManager::finalize_with_preparing(&recording, job), - None => RecoveryManager::finalize(&recording), - } - } else { - RecoveryManager::recover(&recording) - }; - let validation_took_ms = validation_start.elapsed().as_millis() as u64; - - match outcome { - Ok(_) => { - if let Err(error) = mark_fragmented_recording_for_ffmpeg_export(recording_dir) { - warn!(project_path = %recording_dir.display(), error = %error, "Failed to write fragmented recording export marker"); - } - if normal_stop { - info!("Successfully finalized fragmented recording"); - } else { - info!("Successfully recovered fragmented recording"); - } - - if let Some(app_handle) = app - && !normal_stop - { - let recovered_duration_secs = RecordingMeta::load_for_project(recording_dir) - .ok() - .and_then(|meta| match meta.inner { - RecordingMetaInner::Studio(studio) => match *studio { - StudioRecordingMeta::MultipleSegments { inner } => Some( - inner - .segments - .iter() - .filter_map(|seg| seg.display.start_time) - .fold(0.0_f64, |acc, v| acc.max(v)), - ), - StudioRecordingMeta::SingleSegment { .. } => None, - }, - _ => None, - }) - .map(|s| s as u64) - .unwrap_or_default(); - - let segments_recovered = RecordingMeta::load_for_project(recording_dir) - .ok() - .and_then(|meta| match meta.inner { - RecordingMetaInner::Studio(studio) => match *studio { - StudioRecordingMeta::MultipleSegments { inner } => { - Some(inner.segments.len() as u32) - } - StudioRecordingMeta::SingleSegment { .. } => Some(1), - }, - _ => None, - }) - .unwrap_or(0); - - crate::telemetry::async_capture_event( - app_handle, - crate::telemetry::AnalyticsEvent::RecordingRecovered { - trigger, - recovered_duration_secs, - segments_recovered, - validation_took_ms, - }, - ); - } - Ok(()) - } - Err(e) => { - let storage_full = crate::recovery::is_storage_full_recovery_error(&e); - let reason = format!("{e}"); - if let Some(app_handle) = app { - crate::telemetry::async_capture_event( - app_handle, - crate::telemetry::AnalyticsEvent::RecordingRecoveryFailed { - trigger, - reason: reason.clone(), - }, - ); - } - if storage_full { - return Err(crate::recovery::finalization_storage_error(display_path)); - } - let action = if normal_stop { "finalize" } else { "recover" }; - Err(format!("Failed to {action} recording: {reason}")) - } - } - } else { - Err("Could not find fragments to remux".to_string()) - } -} - -fn classify_error_message(error: &str) -> String { - let lowered = error.to_ascii_lowercase(); - let class = if lowered.contains("permission") { - "permission" - } else if lowered.contains("disk") || lowered.contains("no space") { - "disk" - } else if lowered.contains("camera") { - "camera" - } else if lowered.contains("microphone") || lowered.contains("mic") { - "microphone" - } else if lowered.contains("display") || lowered.contains("screen") { - "display" - } else if lowered.contains("muxer") { - "muxer" - } else if lowered.contains("timeout") { - "timeout" - } else if lowered.contains("cancel") { - "cancelled" - } else { - "other" - }; - class.to_string() -} - -async fn emit_recording_started_telemetry(app: &AppHandle, state_mtx: &MutableState<'_, App>) { - use crate::recording_telemetry::{mode_label, target_kind_label}; - use crate::telemetry::{AnalyticsEvent, async_capture_event}; - - let (mode, recording_mode, target_kind, has_camera, has_mic, has_system_audio) = { - let state = state_mtx.read().await; - let Some(recording) = state.current_recording() else { - return; - }; - let inputs = recording.inputs(); - let target_kind = target_kind_label(recording.capture_target()); - let has_camera = match recording { - InProgressRecording::Instant { camera_feed, .. } - | InProgressRecording::Studio { camera_feed, .. } => camera_feed.is_some(), - }; - ( - mode_label(inputs.mode), - inputs.mode, - target_kind, - has_camera, - state.selected_mic_label.is_some(), - inputs.capture_system_audio, - ) - }; - - let general = GeneralSettingsStore::get(app).ok().flatten(); - let defaults = desktop_recording_defaults(general.as_ref()); - let fragmented = defaults.crash_recovery_recording; - let custom_cursor_capture = defaults.custom_cursor_capture; - // Studio applies the camera fps clamp via `apply_to_studio_builder`; Instant records screen at a - // fixed fps, so report the value each mode actually uses rather than the raw studio cap. - let target_fps = match recording_mode { - RecordingMode::Studio => defaults.studio_max_fps(has_camera, None), - RecordingMode::Instant | RecordingMode::Screenshot => { - cap_recording::DEFAULT_INSTANT_MODE_FPS - } - }; - - async_capture_event( - app, - AnalyticsEvent::RecordingStarted { - mode, - target_kind, - has_camera, - has_mic, - has_system_audio, - target_fps, - target_width: 0, - target_height: 0, - fragmented, - custom_cursor_capture, - }, - ); -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::TimeZone as _; - use tempfile::tempdir; - - #[test] - fn project_name_random_placeholder_is_filename_safe_and_sized() { - let name = format_project_name( - Some("{random}-{random:16}-{random:0}"), - "Example Window", - "Window", - RecordingMode::Studio, - None, - ); - let parts = name.split('-').collect::>(); - - assert_eq!(parts[0].len(), 10); - assert_eq!(parts[1].len(), 16); - assert!( - parts[..2] - .iter() - .all(|part| part.chars().all(|character| character.is_ascii_hexdigit())) - ); - assert_eq!(parts[2], "{random:0}"); - } - - #[test] - fn project_name_preserves_random_placeholder_in_window_title() { - let timestamp = chrono::Local - .with_ymd_and_hms(2026, 8, 25, 9, 15, 0) - .single() - .unwrap(); - - let name = format_project_name( - None, - "Implement {random} placeholder", - "Window", - RecordingMode::Studio, - Some(timestamp), - ); - - assert!(name.contains("Implement {random} placeholder")); - } - - #[test] - fn requested_microphone_absence_is_an_error_not_an_empty_track() { - let error = selected_microphone_for_start(Some("Requested mic".into()), &[]) - .unwrap_err() - .to_string(); - assert!(error.contains("Requested mic")); - assert!(error.contains("no longer available")); - assert!(error.contains("Reconnect")); - } - - #[test] - fn requested_microphone_requires_exact_name_without_substring_fallback() { - for names in [ - vec!["Requested mic alternate".into()], - vec!["mic".into()], - vec!["requested mic".into()], - ] { - assert!(selected_microphone_for_start(Some("Requested mic".into()), &names).is_err()); - } - assert_eq!( - selected_microphone_for_start( - Some("Requested mic".into()), - &["Requested mic alternate".into(), "Requested mic".into()], - ) - .unwrap(), - Some("Requested mic".into()) - ); - } - - #[test] - fn intentional_no_microphone_is_preserved() { - assert_eq!(selected_microphone_for_start(None, &[]).unwrap(), None); - assert_eq!( - selected_microphone_for_start(None, &["Another mic".into()]).unwrap(), - None - ); - } - - #[test] - fn requested_camera_absence_is_an_error_for_screen_capture_too() { - let id = camera::DeviceOrModelID::DeviceID("requested-camera".into()); - let error = validate_selected_camera_for_start(Some(&id), |_| false) - .unwrap_err() - .to_string(); - assert!(error.contains("requested-camera")); - assert!(error.contains("no longer available")); - assert!(error.contains("Reconnect")); - assert!(validate_selected_camera_for_start(Some(&id), |_| true).is_ok()); - } - - #[test] - fn intentional_no_camera_does_not_probe_or_choose_another_device() { - assert!( - validate_selected_camera_for_start(None, |_| { panic!("No camera was requested") }) - .is_ok() - ); - } - - #[test] - fn animated_gradient_default_is_remembered_without_overwriting_explicit_presets() { - let library = cap_project::AnimatedGradientLibrary { - selected: true, - last_used: Some(cap_project::AnimatedGradientConfig::from_seed(42)), - ..Default::default() - }; - let mut project = ProjectConfiguration::default(); - apply_animated_gradient_default(&mut project, Some(&library), false, None); - assert!(matches!( - project.background.source, - cap_project::BackgroundSource::Color { .. } - )); - apply_animated_gradient_default(&mut project, Some(&library), true, None); - apply_screen_recording_presentation_defaults( - &mut project, - None, - true, - Some("wallpaper.jpg".into()), - ); - let cap_project::BackgroundSource::AnimatedGradient { config } = project.background.source - else { - panic!("Expected remembered gradient"); - }; - assert_eq!(Some(config), library.last_used); - assert_eq!(project.background.padding, 10.0); - } - - #[test] - fn deselected_or_missing_animated_gradient_keeps_recording_defaults() { - let mut project = ProjectConfiguration::default(); - let library = cap_project::AnimatedGradientLibrary { - last_used: Some(cap_project::AnimatedGradientConfig::default()), - ..Default::default() - }; - apply_animated_gradient_default(&mut project, Some(&library), true, None); - apply_animated_gradient_default(&mut project, None, true, None); - assert!(matches!( - project.background.source, - cap_project::BackgroundSource::Color { .. } - )); - } - - #[test] - fn animated_gradient_default_preserves_camera_only_presentation() { - let library = cap_project::AnimatedGradientLibrary { - selected: true, - last_used: Some(cap_project::AnimatedGradientConfig::default()), - ..Default::default() - }; - let mut project = ProjectConfiguration::default(); - let original = serde_json::to_value(&project.background).unwrap(); - apply_animated_gradient_default( - &mut project, - Some(&library), - true, - Some(&ScreenCaptureTarget::CameraOnly), - ); - assert_eq!(serde_json::to_value(project.background).unwrap(), original); - } - - #[test] - fn recording_start_preflight_requires_authentication_for_instant_recordings() { - assert_eq!( - recording_start_mode_error(RecordingMode::Instant, false), - Some("Please sign in to use instant recording") - ); - assert_eq!( - recording_start_mode_error(RecordingMode::Instant, true), - None - ); - } - - #[test] - fn recording_start_preflight_preserves_studio_and_rejects_screenshot_modes() { - assert_eq!( - recording_start_mode_error(RecordingMode::Studio, false), - None - ); - assert_eq!( - recording_start_mode_error(RecordingMode::Screenshot, true), - Some("Use take_screenshot for screenshots") - ); - } - - fn click_event_with_state(time_ms: f64, down: bool) -> CursorClickEvent { - CursorClickEvent { - active_modifiers: vec![], - cursor_num: 0, - cursor_id: "default".to_string(), - time_ms, - down, - } - } - - fn click_event(time_ms: f64) -> CursorClickEvent { - click_event_with_state(time_ms, true) - } - - fn click_up_event(time_ms: f64) -> CursorClickEvent { - click_event_with_state(time_ms, false) - } - - fn move_event(time_ms: f64, x: f64, y: f64) -> CursorMoveEvent { - CursorMoveEvent { - active_modifiers: vec![], - cursor_id: "default".to_string(), - time_ms, - x, - y, - } - } - - #[test] - fn mic_feed_locked_detects_feed_lock_errors() { - assert!(mic_feed_locked(&anyhow::Error::new( - microphone::FeedLockedError - ))); - assert!(mic_feed_locked(&anyhow::Error::new( - microphone::LockFeedError::Locked(microphone::FeedLockedError) - ))); - assert!(mic_feed_locked(&anyhow::Error::new( - microphone::SetInputError::Locked(microphone::FeedLockedError) - ))); - } - - #[test] - fn mic_feed_locked_ignores_unrelated_errors() { - assert!(!mic_feed_locked(&anyhow!("different failure"))); - } - - #[test] - fn skips_trailing_stop_click() { - let segments = generate_zoom_segments_from_clicks_impl( - vec![click_event(11_900.0)], - vec![], - 12.0, - DEFAULT_AUTO_ZOOM_AMOUNT, - ); - - assert!( - segments.is_empty(), - "expected trailing stop click to be ignored" - ); - } - - #[test] - fn merges_clicks_with_three_second_gap() { - let clicks = vec![click_event(1_200.0), click_event(4_200.0)]; - let moves = vec![ - move_event(1_500.0, 0.10, 0.12), - move_event(1_720.0, 0.42, 0.45), - move_event(1_940.0, 0.74, 0.78), - ]; - - let segments = - generate_zoom_segments_from_clicks_impl(clicks, moves, 20.0, DEFAULT_AUTO_ZOOM_AMOUNT); - - assert!( - !segments.is_empty(), - "expected activity to produce zoom segments" - ); - let first = &segments[0]; - assert_eq!(segments.len(), 1); - assert_eq!(first.start, 0.9); - assert_eq!(first.end, 6.7); - } - - #[test] - fn separates_click_groups_across_long_idle_gap() { - let clicks = vec![ - click_event(2_271.0), - click_event(9_137.0), - click_event(9_915.0), - click_event(19_404.0), - ]; - let moves = vec![ - move_event(562.0, 0.48, 0.50), - move_event(2_271.0, 0.05, 0.08), - move_event(9_137.0, 0.94, 0.06), - move_event(9_915.0, 0.94, 0.07), - move_event(19_364.0, 0.44, 0.95), - ]; - - let segments = generate_zoom_segments_from_clicks_impl( - clicks, - moves, - 19.436_667, - DEFAULT_AUTO_ZOOM_AMOUNT, - ); - - assert_eq!(segments.len(), 2); - assert_eq!(segments[0].start, 1.971); - assert_eq!(segments[0].end, 4.771); - assert_eq!(segments[1].start, 8.837); - assert_eq!(segments[1].end, 12.415); - } - - #[test] - fn extends_segment_until_after_mouse_up() { - let clicks = vec![click_event(1_000.0), click_up_event(2_500.0)]; - - let segments = - generate_zoom_segments_from_clicks_impl(clicks, vec![], 10.0, DEFAULT_AUTO_ZOOM_AMOUNT); - - assert_eq!(segments.len(), 1); - assert_eq!(segments[0].start, 0.7); - assert_eq!(segments[0].end, 5.0); - } - - #[test] - fn clamps_zoom_end_before_recording_end() { - let clicks = vec![click_event(8_999.0), click_event(9_000.0)]; - - let segments = - generate_zoom_segments_from_clicks_impl(clicks, vec![], 10.0, DEFAULT_AUTO_ZOOM_AMOUNT); - - assert_eq!(segments.len(), 1); - assert_eq!(segments[0].start, 8.699); - assert_eq!(segments[0].end, 9.2); - } - - #[test] - fn does_not_zoom_without_clicks() { - let jitter_moves = (0..30) - .map(|i| { - let t = 1_000.0 + (i as f64) * 30.0; - let delta = (i as f64) * 0.0004; - move_event(t, 0.5 + delta, 0.5) - }) - .collect::>(); - - let segments = generate_zoom_segments_from_clicks_impl( - Vec::new(), - jitter_moves, - 15.0, - DEFAULT_AUTO_ZOOM_AMOUNT, - ); - - assert!( - segments.is_empty(), - "small jitter should not generate segments" - ); - } - - #[test] - fn marks_fragmented_recordings_for_ffmpeg_export() { - let dir = tempdir().unwrap(); - - assert!(!fragmented_export_ffmpeg_marker_path(dir.path()).exists()); - - mark_fragmented_recording_for_ffmpeg_export(dir.path()).unwrap(); - - assert!(fragmented_export_ffmpeg_marker_path(dir.path()).exists()); - } - - #[test] - fn skips_desktop_background_paths_that_can_trigger_macos_prompts() { - let home = Path::new("/Users/test"); - - assert!(desktop_background_source_requires_user_prompt_for_home( - Path::new("/Users/test/Downloads/wallpaper.jpg"), - home - )); - assert!(desktop_background_source_requires_user_prompt_for_home( - Path::new("/Users/test/Library/CloudStorage/iCloud Drive/wallpaper.jpg"), - home - )); - assert!(!desktop_background_source_requires_user_prompt_for_home( - Path::new("/Users/test/Pictures/wallpaper.jpg"), - home - )); - assert!(!desktop_background_source_requires_user_prompt_for_home( - Path::new("/System/Library/Desktop Pictures/wallpaper.jpg"), - home - )); - } - - #[test] - fn importing_a_desktop_background_preserves_referenced_snapshots() { - let dir = tempdir().unwrap(); - let project = dir.path().join("project.cap"); - let source = dir.path().join("wallpaper.jpg"); - image::RgbImage::from_pixel(32, 16, image::Rgb([40, 120, 200])) - .save(&source) - .unwrap(); - let previous = project.join("assets/current-desktop-background-1.jpg"); - let previous_pending = project.join("assets/current-desktop-background-1.pending.jpg"); - assert!(matches!( - write_desktop_background_source_to(&source, &previous, &previous_pending).unwrap(), - CurrentDesktopBackgroundWrite::Stored - )); - let saved_style_path = previous.clone(); - let history_path = previous.clone(); - let previous_bytes = std::fs::read(&previous).unwrap(); - let sibling_pending = project.join("assets/current-desktop-background-2.pending.jpg"); - std::fs::write(&sibling_pending, b"another import in progress").unwrap(); - - image::RgbImage::from_pixel(48, 24, image::Rgb([180, 60, 30])) - .save(&source) - .unwrap(); - let imported = import_current_desktop_background_from_source(&project, &source).unwrap(); - - assert_ne!(Path::new(&imported), previous); - assert_eq!(image::image_dimensions(&imported).unwrap(), (48, 24)); - assert_eq!(std::fs::read(saved_style_path).unwrap(), previous_bytes); - assert_eq!(image::image_dimensions(history_path).unwrap(), (32, 16)); - assert_eq!( - std::fs::read(sibling_pending).unwrap(), - b"another import in progress" - ); - } - - #[test] - fn notch_overlay_requires_recorded_geometry_on_screen_target() { - let display = ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }; - let area = ScreenCaptureTarget::Area { - screen: "1".parse().unwrap(), - bounds: scap_targets::bounds::LogicalBounds::new( - scap_targets::bounds::LogicalPosition::new(0.0, 0.0), - scap_targets::bounds::LogicalSize::new(100.0, 100.0), - ), - }; - - assert!(should_enable_notch_overlay(Some(&display), true, true)); - assert!(should_enable_notch_overlay(Some(&area), true, true)); - assert!(!should_enable_notch_overlay(Some(&display), true, false)); - assert!(!should_enable_notch_overlay(Some(&area), true, false)); - assert!(!should_enable_notch_overlay(Some(&display), false, true)); - assert!(!should_enable_notch_overlay( - Some(&ScreenCaptureTarget::CameraOnly), - true, - true - )); - } - - #[test] - fn skips_screen_presentation_defaults_for_camera_only_recordings() { - let mut config = ProjectConfiguration::default(); - - apply_screen_recording_presentation_defaults( - &mut config, - Some(&ScreenCaptureTarget::CameraOnly), - true, - Some("wallpaper.jpg".to_string()), - ); - - assert_eq!(config.background.padding, 0.0); - assert!(matches!( - config.background.source, - cap_project::BackgroundSource::Color { - value: [255, 255, 255], - alpha: 255, - } - )); - } - - #[test] - fn applies_screen_presentation_defaults_for_screen_recordings() { - let mut config = ProjectConfiguration::default(); - let capture_target = ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }; - - apply_screen_recording_presentation_defaults( - &mut config, - Some(&capture_target), - true, - Some("wallpaper.jpg".to_string()), - ); - - assert_eq!(config.background.padding, 10.0); - assert_eq!(config.background.rounding, 7.5); - assert!(matches!( - config.background.source, - cap_project::BackgroundSource::Wallpaper { path: Some(path) } if path == "wallpaper.jpg" - )); - } - - #[test] - fn screen_presentation_defaults_apply_window_rounding_without_default_border() { - let mut config = ProjectConfiguration::default(); - let capture_target = ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }; - - apply_screen_recording_presentation_defaults( - &mut config, - Some(&capture_target), - true, - Some("wallpaper.jpg".to_string()), - ); - - assert_eq!(config.background.rounding, 7.5); - assert!(config.background.border.is_none()); - } - - #[test] - fn default_project_config_matches_screen_recording_presentation() { - let config = default_project_config(); - let spring = cap_project::ScreenMovementSpring::default(); - - assert_eq!(config.background.padding, 10.0); - assert_eq!( - config.background.rounding, - DEFAULT_SCREEN_RECORDING_BACKGROUND_ROUNDING_PERCENT - ); - assert_eq!(config.screen_movement_spring.stiffness, spring.stiffness); - assert_eq!(config.screen_movement_spring.damping, spring.damping); - assert_eq!(config.screen_movement_spring.mass, spring.mass); - } - - #[test] - fn classifies_unsolicited_pipeline_completion_as_unexpected_stop() { - let disposition = classify_actor_done_result(Ok::<(), anyhow::Error>(()), true); - - assert_eq!( - disposition, - ActorDoneDisposition::UnexpectedStop { - error: "Recording stopped unexpectedly before it was ended.".to_string() - } - ); - } - - #[test] - fn classifies_pipeline_completion_after_user_stop_as_expected() { - let disposition = classify_actor_done_result(Ok::<(), anyhow::Error>(()), false); - - assert_eq!(disposition, ActorDoneDisposition::UserInitiatedStop); - } - - #[test] - fn classifies_pipeline_failure_as_failure() { - let disposition = classify_actor_done_result(Err(anyhow!("feed lost")), true); - - assert_eq!( - disposition, - ActorDoneDisposition::Failed { - error: "feed lost".to_string() - } - ); - } -} - -#[cfg(target_os = "linux")] -pub(crate) mod linux_instant { - use super::*; - use crate::upload::strict_instant::{Control, Permission}; - use std::sync::{ - Mutex, - atomic::{AtomicBool, Ordering}, - }; - - #[derive(Clone)] - pub struct Attempt(Arc); - - struct AttemptInner { - lifecycle: Mutex>, - directory: Mutex>, - cancelled: AtomicBool, - cancel_changed: tokio::sync::watch::Sender, - started: tokio::sync::watch::Sender, - operation: tokio::sync::Mutex<()>, - upload: Control, - permission: Mutex>, - uploading: AtomicBool, - ui_ready: AtomicBool, - cleanup_uncertain: AtomicBool, - upload_result: Mutex>>, - terminal: Mutex>, - } - - #[derive(Clone, Copy)] - enum ControlSuccess { - Stopped, - Discarded, - } - - #[derive(Clone)] - struct TerminalControl { - kind: ControlSuccess, - result: Result<(), String>, - } - - impl Attempt { - fn new() -> Self { - let (upload, permission) = Control::new(); - let (started, _) = tokio::sync::watch::channel(false); - Self(Arc::new(AttemptInner { - lifecycle: Mutex::new(None), - directory: Mutex::new(None), - cancelled: AtomicBool::new(false), - cancel_changed: tokio::sync::watch::channel(false).0, - started, - operation: tokio::sync::Mutex::new(()), - upload, - ui_ready: AtomicBool::new(false), - cleanup_uncertain: AtomicBool::new(false), - upload_result: Mutex::new(None), - terminal: Mutex::new(None), - permission: Mutex::new(Some(permission)), - uploading: AtomicBool::new(false), - })) - } - - pub(crate) fn checked(&self, result: Result) -> Result { - result.and_then(|value| { - if self.cancelled() { - Err("Instant attempt cancelled; local recording retained".into()) - } else { - Ok(value) - } - }) - } - - fn terminal_result(&self, discard: bool) -> Option> { - let mut terminal = self.0.terminal.lock().unwrap(); - let terminal = terminal.as_mut()?; - terminal.result = self.checked(terminal.result.clone()); - Some( - terminal - .result - .clone() - .and_then(|()| match (terminal.kind, discard) { - (ControlSuccess::Stopped, true) => { - Err("Instant recording was stopped, not discarded".into()) - } - _ => Ok(()), - }), - ) - } - - fn record_terminal(&self, discard: bool, result: Result<(), String>) -> Result<(), String> { - let result = self.checked(result); - let kind = if discard { - ControlSuccess::Discarded - } else { - ControlSuccess::Stopped - }; - let mut terminal = self.0.terminal.lock().unwrap(); - if terminal.is_none() { - *terminal = Some(TerminalControl { kind, result }); - } - drop(terminal); - self.terminal_result(discard).unwrap() - } - - pub fn control_in_progress(&self) -> bool { - self.0.operation.try_lock().is_err() - } - pub fn terminal_needs_cleanup(&self) -> bool { - if self.control_in_progress() { - false - } else { - self.cancel(); - true - } - } - - fn prepare_control(&self) { - if self.0.operation.try_lock().is_err() || !*self.0.started.borrow() { - self.cancel(); - } - } - - pub fn cancel(&self) { - self.0.cancelled.store(true, Ordering::Release); - self.0.cancel_changed.send_replace(true); - self.0.upload.deny(); - if let Some(lifecycle) = self.lifecycle() { - lifecycle.cancel(); - } - } - - pub fn cancelled(&self) -> bool { - self.0.cancelled.load(Ordering::Acquire) - } - pub(crate) async fn while_active( - &self, - operation: impl std::future::Future>, - ) -> Result { - self.checked(Ok(()))?; - let mut changed = self.0.cancel_changed.subscribe(); - tokio::select! { - biased; - _ = async { - while !*changed.borrow_and_update() { - if changed.changed().await.is_err() { break; } - } - } => Err("Instant preparation cancelled".into()), - result = operation => self.checked(result), - } - } - - pub fn upload(&self) -> Control { - self.0.upload.clone() - } - pub fn upload_started(&self) { - self.0.uploading.store(true, Ordering::Release); - } - fn lifecycle(&self) -> Option { - self.0.lifecycle.lock().unwrap().clone() - } - pub fn owns_directory(&self, directory: &Path) -> bool { - self.0.directory.lock().unwrap().as_deref() == Some(directory) - } - pub fn capture_joined(&self) -> bool { - self.lifecycle().is_none_or(|lifecycle| { - lifecycle.quiescence() == instant_recording::InstantQuiescence::Joined - }) - } - pub fn has_capture(&self) -> bool { - self.lifecycle().is_some() - } - pub fn ui_ready(&self) -> bool { - self.0.ui_ready.load(Ordering::Acquire) - } - pub fn set_directory(&self, directory: PathBuf) { - *self.0.directory.lock().unwrap() = Some(directory); - } - pub(crate) fn same(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } - - pub async fn attach( - &self, - directory: PathBuf, - lifecycle: instant_recording::InstantLifecycle, - ) -> Result<(), String> { - if let Some(previous) = self.lifecycle() - && previous.wait_for_quiescence().await - != instant_recording::InstantQuiescence::Joined - { - lifecycle.cancel(); - return Err("Previous Instant startup cleanup is unconfirmed".into()); - } - *self.0.directory.lock().unwrap() = Some(directory); - *self.0.lifecycle.lock().unwrap() = Some(lifecycle.clone()); - if self.cancelled() { - lifecycle.cancel(); - return Err("Instant startup cancelled".into()); - } - Ok(()) - } - - async fn capture_cleanup(&self) -> bool { - match self.lifecycle() { - Some(lifecycle) => { - lifecycle.wait_for_quiescence().await - == instant_recording::InstantQuiescence::Joined - } - None => true, - } - } - - pub(super) async fn upload_cleanup(&self) -> bool { - !self.0.uploading.load(Ordering::Acquire) || self.0.upload.joined().await - } - - async fn join_upload(&self, upload: InstantUploader) -> Result<(), String> { - if let Some(result) = self.0.upload_result.lock().unwrap().clone() { - return result; - } - let result = await_instant_upload(upload) - .await - .and_then(|result| result.map_err(|error| error.to_string())); - *self.0.upload_result.lock().unwrap() = Some(result.clone()); - result - } - - async fn wait_started(&self) -> Result<(), String> { - let mut started = self.0.started.subscribe(); - loop { - if *started.borrow_and_update() { - return Ok(()); - } - started - .changed() - .await - .map_err(|_| "Instant startup owner was lost")?; - } - } - - fn authorize(&self) -> Result<(), String> { - if self.cancelled() { - return Err("Instant attempt was cancelled".into()); - } - self.0 - .permission - .lock() - .unwrap() - .take() - .ok_or("Instant completion permission was consumed")? - .grant() - .map_err(|error| error.to_string()) - } - } - - struct Waiter(Option); - impl Drop for Waiter { - fn drop(&mut self) { - if let Some(attempt) = self.0.take() { - attempt.cancel(); - } - } - } - - pub fn current(app: &AppHandle) -> Option { - app.try_state::() - .and_then(|state| state.instant.lock().unwrap().clone()) - } - - fn is_current(app: &AppHandle, attempt: &Attempt) -> bool { - current(app).is_some_and(|value| value.same(attempt)) - } - - fn release(app: &AppHandle, attempt: &Attempt) { - let state = app.state::(); - let mut current = state.instant.lock().unwrap(); - if current.as_ref().is_some_and(|value| value.same(attempt)) { - drop(current.take()); - } - } - - pub fn blocks_cleanup(app: &AppHandle) -> bool { - current(app).is_some_and(|attempt| !attempt.capture_joined()) - } - - pub(crate) fn validate_screen_request( - inputs: &StartRecordingInputs, - requested: &crate::RequestedInputs, - ) -> Result<(), String> { - validate_screen_camera_support( - inputs, - requested.camera.value.is_some(), - std::env::var_os("DISPLAY").is_some() - && std::env::var_os("WAYLAND_DISPLAY").is_none() - && std::env::var("XDG_SESSION_TYPE") - .is_ok_and(|value| value.eq_ignore_ascii_case("x11")), - ) - } - - fn validate_screen_camera_support( - inputs: &StartRecordingInputs, - camera_requested: bool, - x11: bool, - ) -> Result<(), String> { - if inputs.mode != RecordingMode::Instant - || !camera_requested - || matches!(inputs.capture_target, ScreenCaptureTarget::CameraOnly) - { - return Ok(()); - } - if !x11 { - return Err( - "Instant camera composition requires an X11 desktop; Wayland is not supported." - .into(), - ); - } - Ok(()) - } - - pub(crate) fn capture_rect( - target: &ScreenCaptureTarget, - ) -> Result { - let (display, crop) = - cap_recording::target_to_display_and_crop(target).map_err(|error| error.to_string())?; - let position = display - .raw_handle() - .physical_position() - .ok_or("Display physical position unavailable")?; - if matches!(target, ScreenCaptureTarget::Window { .. }) { - let crop = crop.ok_or("Capture window bounds unavailable")?; - return physical_window_capture_rect( - position.x() + crop.position().x(), - position.y() + crop.position().y(), - crop.size().width(), - crop.size().height(), - ); - } - let size = display - .physical_size() - .ok_or("Display physical size unavailable")?; - physical_capture_rect( - (position.x(), position.y(), size.width(), size.height()), - crop.map(|crop| { - ( - crop.position().x(), - crop.position().y(), - crop.size().width(), - crop.size().height(), - ) - }), - ) - } - - fn physical_window_capture_rect( - x: f64, - y: f64, - width: f64, - height: f64, - ) -> Result { - let coordinates = f64::from(i32::MIN)..=f64::from(i32::MAX); - let dimensions = 2.0..=f64::from(i32::MAX); - if ![x, y, width, height] - .iter() - .all(|value| value.is_finite() && value.fract() == 0.0) - || !coordinates.contains(&x) - || !coordinates.contains(&y) - || !dimensions.contains(&width) - || !dimensions.contains(&height) - { - return Err("Capture window has invalid physical bounds".into()); - } - Ok(crate::linux_instant_camera::PhysicalRect { - x: x as i32, - y: y as i32, - width: width as u32, - height: height as u32, - }) - } - - fn physical_capture_rect( - display: (f64, f64, f64, f64), - crop: Option<(f64, f64, f64, f64)>, - ) -> Result { - let (x, y, width, height) = cap_recording::sources::screen_capture::x11_capture_rect( - display.0, display.1, display.2, display.3, crop, - ) - .map_err(|error| error.to_string())?; - Ok(crate::linux_instant_camera::PhysicalRect { - x, - y, - width, - height, - }) - } - - pub(crate) struct PreparedScreenCamera { - pub source: instant_recording::LinuxProcessedCameraSource, - pub presentation: instant_recording::LinuxCameraPresentation, - pub reference_size: (u32, u32), - } - - pub(crate) fn configure_screen_camera( - builder: instant_recording::ActorBuilder, - target: &ScreenCaptureTarget, - camera_requested: bool, - prepared: Option, - ) -> Result { - if let Some(prepared) = prepared { - if !camera_requested || matches!(target, ScreenCaptureTarget::CameraOnly) { - return Err("Unexpected Instant screen camera preparation".into()); - } - Ok(builder.with_linux_processed_camera( - prepared.source, - prepared.presentation, - prepared.reference_size, - )) - } else if camera_requested && !matches!(target, ScreenCaptureTarget::CameraOnly) { - Err( - "Requested Instant screen camera was not prepared; no raw fallback is allowed" - .into(), - ) - } else { - Ok(builder) - } - } - - pub(crate) async fn prepare_screen_camera( - app: &AppHandle, - attempt: &Attempt, - generation: u32, - inputs: &StartRecordingInputs, - requested: &crate::RequestedInputs, - camera: Option>, - ) -> Result, String> { - validate_screen_request(inputs, requested)?; - if camera.is_some() != requested.camera.value.is_some() { - return Err("Requested camera lock is missing or unexpected".into()); - } - let capture = capture_rect(&inputs.capture_target)?; - let prepared = if let Some(camera) = camera { - let presentation = attempt - .while_active(crate::linux_instant_camera::request_presentation( - app, generation, capture, - )) - .await?; - let state = app.state::>(); - let factory = state.read().await.camera_processing.clone(); - let source = attempt - .while_active(async { - factory - .subscribe(camera, &presentation) - .await - .map_err(|error| error.to_string()) - }) - .await?; - Some((presentation, source)) - } else { - None - }; - let (presentation, source) = match prepared { - Some((presentation, source)) => (Some(presentation), Some(source)), - None => (None, None), - }; - let seal = crate::clean_capture::seal_instant( - app, - crate::clean_capture::InstantSeal { - generation, - attempt: attempt.clone(), - requested: requested.clone(), - target: inputs.capture_target.clone(), - capture, - presentation, - }, - ) - .await?; - match (seal.presentation, source) { - (Some(presentation), Some(source)) => Ok(Some(PreparedScreenCamera { - source, - presentation: presentation.presentation, - reference_size: presentation.reference_size, - })), - (None, None) => Ok(None), - _ => Err("Instant prepared camera ownership was lost".into()), - } - } - - pub fn validate_inputs(inputs: &StartRecordingInputs) -> Result<(), String> { - if inputs.mode == RecordingMode::Instant - && matches!(inputs.capture_target, ScreenCaptureTarget::CameraOnly) - && inputs.capture_system_audio - { - Err("System audio is not supported for Linux Instant CameraOnly. Disable it before recording.".into()) - } else { - Ok(()) - } - } - - pub async fn start( - app: AppHandle, - state: MutableState<'_, App>, - inputs: StartRecordingInputs, - ) -> Result { - validate_inputs(&inputs)?; - let attempt = Attempt::new(); - { - let app_state = state.write().await; - if !matches!(app_state.recording_state, RecordingState::None) { - return Err("Recording already in progress".into()); - } - let state = app.state::(); - let mut slot = state.instant.lock().unwrap(); - if slot.is_some() { - return Err("Finish the previous Instant attempt before recording".into()); - } - *slot = Some(attempt.clone()); - } - let work_attempt = attempt.clone(); - let abandoned_attempt = attempt.clone(); - let abandoned_app = app.clone(); - owned_reply( - attempt, - async move { - let attempt = work_attempt; - let state = app.state::>(); - let result = - AssertUnwindSafe(start_recording_inner(app.clone(), state.clone(), inputs)) - .catch_unwind() - .await - .unwrap_or_else(|panic| { - Err(format!( - "Instant startup panicked: {}", - panic_message(panic) - )) - }); - let started = - matches!(result, Ok(RecordingAction::Started)) && !attempt.cancelled(); - if !started { - attempt.cancel(); - let joined = attempt.capture_cleanup().await; - let upload_joined = attempt.upload_cleanup().await; - if joined && upload_joined && is_current(&app, &attempt) { - let upload = { - let state = state.read().await; - match state.current_recording() { - Some(InProgressRecording::Instant { segment_upload, .. }) => { - Some(segment_upload.clone()) - } - _ => None, - } - }; - if let Some(upload) = upload { - let _ = attempt.join_upload(upload).await; - } - attempt.0.ui_ready.store(true, Ordering::Release); - let directory = attempt.0.directory.lock().unwrap().clone(); - if let Some(directory) = directory { - let mut state = state.write().await; - let _ = handle_recording_end( - app.clone(), - Err("Instant startup failed or was cancelled".into()), - &mut state, - directory, - ) - .await; - } else { - state.write().await.clear_pending_recording(); - } - let terminal = result.as_ref().err().cloned().unwrap_or_else(|| { - "Instant startup did not establish a recording".into() - }); - let _ = attempt.record_terminal(false, Err(terminal)); - release(&app, &attempt); - } - } - attempt.0.started.send_replace(true); - match result { - Ok(RecordingAction::Started) if !started => { - Err("Instant startup cancelled".into()) - } - result => result, - } - }, - async move { - let _ = execute(abandoned_app, abandoned_attempt, false).await; - }, - ) - .await - } - - pub fn control( - app: AppHandle, - attempt: Attempt, - discard: bool, - ) -> futures::future::BoxFuture<'static, Result<(), String>> { - Box::pin(async move { - attempt.prepare_control(); - let worker = attempt.clone(); - owned_reply( - attempt, - async move { - let result = execute(app.clone(), worker.clone(), discard).await; - let result = storage_preflight_control_result(result, worker.has_capture()); - if let Err(error) = &result { - let _ = RecordingEvent::Failed { - error: error.clone(), - } - .emit(&app); - } - result - }, - async {}, - ) - .await - }) - } - - async fn owned_reply(attempt: Attempt, work: F, abandoned: A) -> Result - where - T: Send + 'static, - F: std::future::Future> + Send + 'static, - A: std::future::Future + Send + 'static, - { - let mut waiter = Waiter(Some(attempt.clone())); - let (reply, result) = tokio::sync::oneshot::channel(); - drop(tauri::async_runtime::spawn(async move { - let result = match AssertUnwindSafe(work).catch_unwind().await { - Ok(result) => result, - Err(panic) => { - attempt.0.cleanup_uncertain.store(true, Ordering::Release); - attempt.cancel(); - attempt.0.started.send_replace(true); - let _ = attempt.upload_cleanup().await; - Err(format!( - "Instant owned work panicked; cleanup is unconfirmed: {}", - panic_message(panic) - )) - } - }; - if result.is_err() { - attempt.cancel(); - } - if reply.send(result).is_err() { - attempt.cancel(); - abandoned.await; - } - })); - let result = result - .await - .map_err(|_| "Instant cleanup owner was lost".to_string())?; - drop(waiter.0.take()); - result - } - - async fn run_control( - attempt: &Attempt, - discard: bool, - current: impl FnOnce() -> bool, - work: W, - ) -> Result<(), String> - where - F: std::future::Future>, - W: FnOnce() -> F, - { - attempt.wait_started().await?; - let _operation = attempt.0.operation.lock().await; - if let Some(result) = attempt.terminal_result(discard) { - return result; - } - if !current() { - return Err("Instant attempt was superseded without a terminal result".into()); - } - work().await - } - - async fn complete_local( - attempt: &Attempt, - outcome: Result, - acquire: L, - finish: W, - ) -> Result<(), String> - where - L: std::future::Future, - F: std::future::Future>, - W: FnOnce(G, Result) -> F, - { - let guard = acquire.await; - let outcome = attempt.checked(outcome); - let failure = outcome.as_ref().err().cloned(); - let cleanup = finish(guard, outcome).await; - attempt.checked(failure.map_or(cleanup, Err)) - } - - async fn execute(app: AppHandle, attempt: Attempt, discard: bool) -> Result<(), String> { - run_control( - &attempt, - discard, - || is_current(&app, &attempt), - || execute_current(app.clone(), attempt.clone(), discard), - ) - .await - } - - async fn execute_current( - app: AppHandle, - attempt: Attempt, - discard: bool, - ) -> Result<(), String> { - if attempt.0.cleanup_uncertain.load(Ordering::Acquire) { - attempt.cancel(); - return Err("Instant finalization cleanup is unconfirmed; local recording and Stop ownership retained".into()); - } - let state = app.state::>(); - let (handle, directory, segment_upload, video_upload_info, target_name) = { - let state = state.read().await; - match state.current_recording() { - Some(InProgressRecording::Instant { - handle, - common, - segment_upload, - video_upload_info, - .. - }) if attempt.owns_directory(&common.recording_dir) => ( - handle.clone(), - common.recording_dir.clone(), - segment_upload.clone(), - video_upload_info.clone(), - common.target_name.clone(), - ), - _ => return Err("Instant recording is unavailable; cleanup state retained".into()), - } - }; - if discard { - attempt.upload().deny(); - } - let stopped = if discard || attempt.cancelled() { - handle.cancel().await.map(|()| None) - } else { - handle.stop().await.map(Some) - }; - if stopped.is_err() { - attempt.cancel(); - } - if !attempt.capture_cleanup().await { - return Err("Instant capture cleanup is unconfirmed. Local recording retained; use Stop to retry.".into()); - } - let video_id = video_upload_info.id.clone(); - let share_info = video_upload_info.clone(); - let result = match stopped { - Ok(Some(recording)) if !attempt.cancelled() => { - let mut completed = CompletedRecording::Instant { - recording, - segment_upload: segment_upload.clone(), - video_upload_info, - target_name, - finalized: false, - }; - match finalize(&attempt, &mut completed).await { - Ok(()) => Ok(Some(completed)), - Err(error) => { - attempt.cancel(); - let _ = attempt.upload_cleanup().await; - Err(error) - } - } - } - result => { - attempt.upload().deny(); - if !attempt.upload_cleanup().await { - return Err( - "Instant upload reader cleanup is unconfirmed; local recording retained" - .into(), - ); - } - match result { - Ok(None) if discard && !attempt.cancelled() => Ok(None), - Ok(_) => { - Err("Instant recording was cancelled; local recording retained".into()) - } - Err(error) => Err(error.to_string()), - } - } - }; - if !matches!(result, Ok(Some(_))) { - if !attempt.upload_cleanup().await { - return Err( - "Instant upload cleanup is unconfirmed; local recording retained".into(), - ); - } - let _ = attempt.join_upload(segment_upload.clone()).await; - } - let successful_discard = matches!(result, Ok(None)); - let successful_upload = matches!(result, Ok(Some(_))); - let finish_app = app.clone(); - let finish_attempt = attempt.clone(); - let finish_directory = directory.clone(); - let mut result = complete_local( - &attempt, - result, - state.write(), - move |mut state, result| async move { - if !is_current(&finish_app, &finish_attempt) - || state - .current_recording() - .is_none_or(|recording| recording.recording_dir() != &finish_directory) - { - return Err("Instant completion no longer owns the recording".into()); - } - let outcome = match result { - Ok(Some(completed)) => Ok(completed), - Ok(None) => Err("Recording discarded".into()), - Err(error) => Err(error), - }; - finish_attempt.0.ui_ready.store(true, Ordering::Release); - handle_recording_end(finish_app, outcome, &mut state, finish_directory).await - }, - ) - .await; - if result.is_ok() && successful_upload { - let session = segment_upload.lock().await.session.clone(); - result = session.mark_ready(false).map_err(|error| error.to_string()); - if result.is_ok() { - let network_attempt = attempt.clone(); - let network_app = app.clone(); - let network_video = share_info.clone(); - let network_session = session.clone(); - result = crate::upload::lifecycle::supervise(app.clone(), session, async move { - let authorized = network_attempt.authorize(); - let uploaded = network_attempt.join_upload(segment_upload).await; - let joined = network_attempt.upload_cleanup().await; - authorized.map_err(AuthedApiError::from)?; - uploaded.map_err(AuthedApiError::from)?; - if !joined { - return Err("Instant upload cleanup is unconfirmed".into()); - } - let bytes = - compress_image(network_session.directory.join("screenshots/display.jpg")) - .await?; - crate::upload::strict_instant::upload_thumbnail( - &network_app, - &network_video.id, - bytes, - &network_attempt.upload(), - ) - .await?; - network_session.complete_locally(&network_app).await?; - crate::automation::run_upload_completed_automations( - network_app, - network_session.directory.clone(), - Some(network_video.link), - Some(network_video.id), - ); - Ok(()) - }) - .await - .map_err(|error| error.to_string()); - } - if result.is_ok() { - result = successful_effects(&app, &attempt, &directory, &share_info); - } - } - result = attempt.checked(result); - if result.is_ok() && successful_discard { - result = delete_remote_instant_video(&app, &video_id).await; - result = attempt.checked(result); - if result.is_ok() { - result = crate::upload::lifecycle::mark_cancelled(&directory) - .map_err(|error| error.to_string()); - } - if result.is_ok() { - result = remove_owned_directory(directory.clone(), attempt.clone(), false).await; - result = attempt.checked(result); - } - } - result = attempt.checked(result); - if let Err(error) = &result { - attempt.cancel(); - persist_terminal_failure(&directory, error).await; - } - let result = attempt.record_terminal(discard, result); - release(&app, &attempt); - attempt.terminal_result(discard).unwrap_or(result) - } - - async fn persist_terminal_failure(directory: &Path, error: &str) { - let directory = directory.to_path_buf(); - let failure = error.to_string(); - let result = tokio::task::spawn_blocking(move || -> Result<(), String> { - let mut meta = - RecordingMeta::load_for_project(&directory).map_err(|error| error.to_string())?; - meta.inner = - RecordingMetaInner::Instant(InstantRecordingMeta::Failed { error: failure }); - meta.save_for_project().map_err(|error| error.to_string()) - }) - .await; - if !matches!(&result, Ok(Ok(()))) { - error!( - ?result, - "Failed to persist Instant terminal error; local recording retained" - ); - } - } - - struct SuccessEffects { - recording_automation: R, - upload_automation: U, - open_link: O, - copy_link: C, - sound: S, - } - - fn dispatch_success_effects( - attempt: &Attempt, - share_link: &str, - effects: SuccessEffects, - ) -> Result<(), String> - where - R: FnOnce(), - U: FnOnce(), - O: FnOnce(String) -> Result<(), String>, - C: FnOnce(String) -> Result<(), String>, - S: FnOnce(), - { - attempt.checked(Ok(()))?; - (effects.recording_automation)(); - attempt.checked(Ok(()))?; - (effects.upload_automation)(); - attempt.checked(Ok(()))?; - (effects.open_link)(recording_stopped_share_url(share_link))?; - attempt.checked(Ok(()))?; - let _ = (effects.copy_link)(share_link.to_string()); - attempt.checked(Ok(()))?; - (effects.sound)(); - attempt.checked(Ok(())) - } - - fn successful_effects( - app: &AppHandle, - attempt: &Attempt, - directory: &Path, - video: &VideoUploadInfo, - ) -> Result<(), String> { - use tauri_plugin_clipboard_manager::ClipboardExt; - dispatch_success_effects( - attempt, - &video.link, - SuccessEffects { - recording_automation: || { - crate::automation::run_instant_recording_automations( - app.clone(), - directory.to_path_buf(), - Some(video.link.clone()), - Some(video.id.clone()), - ) - }, - upload_automation: || {}, - open_link: |link| open_external_link(app.clone(), link), - copy_link: |link| { - app.clipboard() - .write_text(link) - .map_err(|error| error.to_string()) - }, - sound: || AppSounds::StopRecording.play(), - }, - ) - } - - async fn remove_owned_directory( - directory: PathBuf, - attempt: Attempt, - uploaded: bool, - ) -> Result<(), String> { - if attempt.cancelled() { - return Err("Instant cleanup cancelled; local recording retained".into()); - } - let worker = attempt.clone(); - tokio::task::spawn_blocking(move || { - if worker.cancelled() { - return Err("Instant cleanup cancelled before deletion".to_string()); - } - if uploaded { - worker.upload().check().map_err(|error| error.to_string())?; - } - std::fs::remove_dir_all(directory).map_err(|error| error.to_string()) - }) - .await - .map_err(|error| error.to_string())??; - if attempt.cancelled() { - return Err("Instant cleanup cancelled during deletion".into()); - } - Ok(()) - } - - pub fn persist_upload_start( - directory: &Path, - video: &VideoUploadInfo, - segmented: bool, - ) -> Result<(), String> { - let mut meta = - RecordingMeta::load_for_project(directory).map_err(|error| error.to_string())?; - meta.upload = Some(if segmented { - cap_project::UploadMeta::SegmentUpload { - video_id: video.id.clone(), - pre_created_video: video.clone(), - recording_dir: directory.into(), - } - } else { - cap_project::UploadMeta::MultipartUpload { - video_id: video.id.clone(), - pre_created_video: video.clone(), - recording_dir: directory.into(), - file_path: directory.join("content/output.mp4"), - } - }); - meta.save_for_project().map_err(|error| error.to_string()) - } - - async fn finalize(attempt: &Attempt, completed: &mut CompletedRecording) -> Result<(), String> { - let CompletedRecording::Instant { - recording, - segment_upload: _, - video_upload_info, - finalized, - .. - } = completed - else { - unreachable!() - }; - if !recording.health.is_uploadable() { - return Err("Instant recording output is damaged".into()); - } - let directory = &recording.project_path; - let screenshots = directory.join("screenshots"); - std::fs::create_dir_all(&screenshots).map_err(|error| error.to_string())?; - let screenshot = screenshots.join("display.jpg"); - let source = if matches!(recording.display_source, ScreenCaptureTarget::CameraOnly) { - directory.join("content/output.mp4") - } else { - create_screenshot_source_from_segments(&directory.join("content/display")).await? - }; - create_screenshot(source, screenshot.clone(), None).await?; - let mut meta = - RecordingMeta::load_for_project(directory).map_err(|error| error.to_string())?; - attempt.checked(Ok(()))?; - meta.sharing = Some(SharingMeta { - link: video_upload_info.link.clone(), - id: video_upload_info.id.clone(), - content_hash: None, - }); - meta.inner = RecordingMetaInner::Instant(recording.meta.clone()); - meta.save_for_project().map_err(|error| error.to_string())?; - attempt.checked(Ok(()))?; - *finalized = true; - Ok(()) - } - - #[cfg(test)] - mod tests { - use super::*; - #[tokio::test] - async fn preflight_cancel_before_f9_unblocks_start_and_stop_only_after_owned_cleanup() { - for discard in [false, true] { - let state = Arc::new(crate::clean_capture::instant_preflight_fixture(7)); - let attempt = Attempt::new(); - let starter = attempt.clone(); - let waiting = state.clone(); - let (polled_tx, polled_rx) = tokio::sync::oneshot::channel(); - let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel(); - let (cleanup_tx, cleanup_rx) = tokio::sync::oneshot::channel(); - let startup = tokio::spawn(async move { - polled_tx.send(()).unwrap(); - let error = crate::clean_capture::await_instant_shortcut(&waiting, 7, &starter) - .await - .unwrap_err(); - cancelled_tx.send(()).unwrap(); - cleanup_rx.await.unwrap(); - assert!(starter.capture_cleanup().await); - assert!(starter.upload_cleanup().await); - let result = starter.record_terminal(false, Err(error)); - starter.0.started.send_replace(true); - result - }); - polled_rx.await.unwrap(); - attempt.prepare_control(); - let controller = attempt.clone(); - let stop = tokio::spawn(async move { - run_control( - &controller, - discard, - || true, - || async { panic!("cancelled preflight has no active actor to finalize") }, - ) - .await - }); - tokio::time::timeout(std::time::Duration::from_secs(2), cancelled_rx) - .await - .expect("Cancel must wake preflight without F9") - .unwrap(); - assert!(!stop.is_finished()); - assert!(!*attempt.0.started.borrow()); - assert!(!attempt.ui_ready()); - cleanup_tx.send(()).unwrap(); - let startup_error = startup.await.unwrap().unwrap_err(); - assert_eq!(stop.await.unwrap().unwrap_err(), startup_error); - assert!(attempt.upload().check().is_err()); - } - } - - #[tokio::test] - async fn preflight_cancelled_before_wait_poll_does_not_need_state_notification() { - let state = crate::clean_capture::instant_preflight_fixture(7); - let attempt = Attempt::new(); - attempt.cancel(); - assert!( - tokio::time::timeout( - std::time::Duration::from_secs(2), - crate::clean_capture::await_instant_shortcut(&state, 7, &attempt), - ) - .await - .expect("pre-cancelled wait must return") - .is_err() - ); - } - - #[tokio::test] - async fn preflight_command_waiter_drop_wakes_shortcut_wait_without_aborting_cleanup() { - let state = Arc::new(crate::clean_capture::instant_preflight_fixture(7)); - let attempt = Attempt::new(); - let waiter = Waiter(Some(attempt.clone())); - let owner = attempt.clone(); - let (polled_tx, polled_rx) = tokio::sync::oneshot::channel(); - let task = tokio::spawn(async move { - polled_tx.send(()).unwrap(); - crate::clean_capture::await_instant_shortcut(&state, 7, &owner).await - }); - polled_rx.await.unwrap(); - drop(waiter); - assert!( - tokio::time::timeout(std::time::Duration::from_secs(2), task) - .await - .expect("dropped command must wake preflight") - .unwrap() - .is_err() - ); - assert!(attempt.capture_cleanup().await); - assert!(attempt.upload_cleanup().await); - assert!(attempt.authorize().is_err()); - } - - #[tokio::test] - async fn preflight_cancel_wins_when_f9_and_cancel_are_ready_together() { - let state = Arc::new(crate::clean_capture::instant_preflight_fixture(7)); - let attempt = Attempt::new(); - let owner = attempt.clone(); - let waiting = state.clone(); - let (polled_tx, polled_rx) = tokio::sync::oneshot::channel(); - let task = tokio::spawn(async move { - polled_tx.send(()).unwrap(); - crate::clean_capture::await_instant_shortcut(&waiting, 7, &owner).await - }); - polled_rx.await.unwrap(); - crate::clean_capture::deliver_preflight_shortcut(&state); - attempt.cancel(); - assert!(task.await.unwrap().is_err()); - } - - #[tokio::test] - async fn preflight_actual_shortcut_wait_preserves_success_and_rejects_wrong_generation() { - let state = Arc::new(crate::clean_capture::instant_preflight_fixture(7)); - assert!( - crate::clean_capture::wait_for_shortcut(&state, 8) - .await - .is_err() - ); - let waiting = state.clone(); - let task = - tokio::spawn( - async move { crate::clean_capture::wait_for_shortcut(&waiting, 7).await }, - ); - crate::clean_capture::deliver_preflight_shortcut(&state); - task.await.unwrap().unwrap(); - } - - #[test] - fn instant_window_capture_reference_preserves_exact_size_and_origin() { - assert_eq!( - physical_window_capture_rect(-40.0, 31.0, 1281.0, 721.0).unwrap(), - crate::linux_instant_camera::PhysicalRect { - x: -40, - y: 31, - width: 1281, - height: 721, - } - ); - assert_eq!( - physical_window_capture_rect(-1921.0, -1081.0, 1919.0, 1079.0).unwrap(), - crate::linux_instant_camera::PhysicalRect { - x: -1921, - y: -1081, - width: 1919, - height: 1079, - } - ); - } - - #[test] - fn instant_window_capture_reference_rejects_invalid_geometry() { - for value in [ - f64::NAN, - f64::INFINITY, - 0.0, - 1.0, - 400.5, - f64::from(u32::MAX), - ] { - assert!(physical_window_capture_rect(0.0, 0.0, value, 720.0).is_err()); - assert!(physical_window_capture_rect(0.0, 0.0, 1280.0, value).is_err()); - } - for value in [ - f64::NAN, - f64::NEG_INFINITY, - -0.5, - f64::from(i32::MIN) - 1.0, - f64::from(i32::MAX) + 1.0, - ] { - assert!(physical_window_capture_rect(value, 0.0, 1280.0, 720.0).is_err()); - assert!(physical_window_capture_rect(0.0, value, 1280.0, 720.0).is_err()); - } - } - - #[test] - fn instant_physical_capture_reference_matches_negative_origin_fractional_crop_and_even_rounding() - { - assert_eq!( - physical_capture_rect( - (-1920.0, 0.0, 1920.0, 1080.0), - Some((10.25, 20.75, 301.25, 199.5)) - ) - .unwrap(), - crate::linux_instant_camera::PhysicalRect { - x: -1910, - y: 20, - width: 302, - height: 200 - }, - ); - assert_eq!( - physical_capture_rect((0.0, -1080.0, 1919.0, 1079.0), None).unwrap(), - crate::linux_instant_camera::PhysicalRect { - x: 0, - y: -1080, - width: 1918, - height: 1078 - }, - ); - } - - #[test] - fn instant_physical_capture_reference_clamps_and_rejects_invalid_geometry() { - assert_eq!( - physical_capture_rect( - (0.0, 0.0, 1920.0, 1080.0), - Some((-50.0, -10.0, 3000.0, 2000.0)) - ) - .unwrap(), - crate::linux_instant_camera::PhysicalRect { - x: 0, - y: 0, - width: 1920, - height: 1080 - }, - ); - assert!(physical_capture_rect((f64::NAN, 0.0, 1920.0, 1080.0), None).is_err()); - } - - struct PreparationLease(std::sync::Arc); - impl Drop for PreparationLease { - fn drop(&mut self) { - self.0.store(true, Ordering::SeqCst); - } - } - - #[tokio::test] - async fn instant_preparation_cancel_wakes_pending_first_frame_and_drops_source_reservation() - { - let attempt = Attempt::new(); - let dropped = Arc::new(AtomicBool::new(false)); - let source = PreparationLease(dropped.clone()); - let owner = attempt.clone(); - let (tx, rx) = tokio::sync::oneshot::channel(); - let task = tokio::spawn(async move { - owner - .while_active(async move { - let _source = source; - tx.send(()).unwrap(); - std::future::pending::>().await - }) - .await - }); - rx.await.unwrap(); - attempt.cancel(); - assert!(task.await.unwrap().is_err()); - assert!(dropped.load(Ordering::SeqCst)); - assert!(!attempt.has_capture()); - assert!(attempt.upload().check().is_err()); - } - - #[tokio::test] - async fn instant_preparation_cancel_before_poll_never_requests_or_hides_preview() { - let attempt = Attempt::new(); - attempt.cancel(); - let result: Result<(), String> = attempt - .while_active(async { - panic!("cancelled preparation must not contact the preview or hide windows") - }) - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn instant_preparation_rechecks_cancel_after_first_frame_arrives() { - let attempt = Attempt::new(); - let owner = attempt.clone(); - let result = attempt - .while_active(async move { - owner.cancel(); - Ok(7) - }) - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn instant_preparation_preserves_fresh_source_and_failure_identity() { - let attempt = Attempt::new(); - assert_eq!(attempt.while_active(async { Ok(7) }).await.unwrap(), 7); - let result: Result<(), String> = attempt - .while_active(async { Err("first mask unavailable".into()) }) - .await; - assert_eq!(result.unwrap_err(), "first mask unavailable"); - assert!(!attempt.cancelled()); - } - - #[tokio::test] - async fn instant_builder_rejects_unprepared_screen_camera_and_joins_unused_attempt() { - let target = ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }; - let builder = instant_recording::Actor::builder( - PathBuf::from("unused-activation-test"), - target.clone(), - ); - let lifecycle = builder.lifecycle(); - assert!(configure_screen_camera(builder, &target, true, None).is_err()); - assert_eq!( - lifecycle.wait_for_quiescence().await, - instant_recording::InstantQuiescence::Joined - ); - } - - #[tokio::test] - async fn instant_builder_preserves_camera_only_and_unselected_camera_paths() { - for (target, requested) in [ - (ScreenCaptureTarget::CameraOnly, true), - ( - ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }, - false, - ), - ] { - let builder = instant_recording::Actor::builder( - PathBuf::from("unused-activation-test"), - target.clone(), - ); - let lifecycle = builder.lifecycle(); - drop(configure_screen_camera(builder, &target, requested, None).unwrap()); - assert_eq!( - lifecycle.wait_for_quiescence().await, - instant_recording::InstantQuiescence::Joined - ); - } - } - - #[test] - fn instant_screen_camera_support_allows_x11_window_and_preserves_wayland_rejection() { - let mut inputs = StartRecordingInputs { - capture_target: ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }, - capture_system_audio: false, - mode: RecordingMode::Instant, - organization_id: None, - }; - assert!(validate_screen_camera_support(&inputs, true, true).is_ok()); - assert!(validate_screen_camera_support(&inputs, true, false).is_err()); - assert!(validate_screen_camera_support(&inputs, false, false).is_ok()); - inputs.capture_target = ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }; - assert!(validate_screen_camera_support(&inputs, true, true).is_ok()); - assert!(validate_screen_camera_support(&inputs, true, false).is_err()); - inputs.mode = RecordingMode::Studio; - assert!(validate_screen_camera_support(&inputs, true, false).is_ok()); - inputs.mode = RecordingMode::Instant; - inputs.capture_target = ScreenCaptureTarget::CameraOnly; - assert!(validate_screen_camera_support(&inputs, true, false).is_ok()); - } - - #[tokio::test] - async fn owned_waiter_drop_cancels_without_aborting_cleanup() { - let attempt = Attempt::new(); - let (release, released) = tokio::sync::oneshot::channel(); - let (finished, finished_rx) = tokio::sync::oneshot::channel(); - let cleaned = Arc::new(AtomicBool::new(false)); - let worker_cleaned = cleaned.clone(); - let mut waiter = Box::pin(owned_reply( - attempt.clone(), - async move { - released.await.unwrap(); - worker_cleaned.store(true, Ordering::Release); - Ok(()) - }, - async move { - finished.send(()).unwrap(); - }, - )); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut waiter) - .await - .is_err() - ); - drop(waiter); - assert!(attempt.cancelled()); - assert!(!cleaned.load(Ordering::Acquire)); - release.send(()).unwrap(); - tokio::time::timeout(Duration::from_secs(1), finished_rx) - .await - .unwrap() - .unwrap(); - assert!(cleaned.load(Ordering::Acquire)); - } - #[tokio::test] - async fn owned_work_failure_returns_error_without_success_or_permission() { - let attempt = Attempt::new(); - let result: Result<(), _> = owned_reply( - attempt.clone(), - async { Err("required output failed".into()) }, - async {}, - ) - .await; - assert!(result.is_err()); - assert!(!attempt.ui_ready()); - attempt.cancel(); - assert!(attempt.authorize().is_err()); - } - #[tokio::test] - async fn owned_work_panic_retains_unconfirmed_cleanup() { - let attempt = Attempt::new(); - let result: Result<(), _> = owned_reply( - attempt.clone(), - async { panic!("failed finalization owner") }, - async {}, - ) - .await; - assert!(result.unwrap_err().contains("unconfirmed")); - assert!(attempt.0.cleanup_uncertain.load(Ordering::Acquire)); - assert!(attempt.cancelled()); - assert!(!attempt.ui_ready()); - } - #[tokio::test] - async fn cancel_before_builder_poll_joins_unused_core_lifecycle() { - let attempt = Attempt::new(); - let builder = instant_recording::Actor::builder( - PathBuf::from("unused-synthetic.cap"), - ScreenCaptureTarget::CameraOnly, - ); - attempt - .attach(PathBuf::from("unused-synthetic.cap"), builder.lifecycle()) - .await - .unwrap(); - attempt.cancel(); - drop(builder); - assert!(attempt.capture_cleanup().await); - assert!(attempt.cancelled()); - assert!(attempt.authorize().is_err()); - } - #[tokio::test] - async fn stop_retry_signals_cancellation_before_waiting_for_operation_lock() { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let active = attempt.0.operation.lock().await; - attempt.prepare_control(); - assert!(attempt.cancelled()); - assert!(attempt.upload().check().is_err()); - drop(active); - } - #[test] - fn explicit_camera_only_system_audio_request_is_rejected_before_normalization() { - let mut inputs = StartRecordingInputs { - capture_target: ScreenCaptureTarget::CameraOnly, - capture_system_audio: true, - mode: RecordingMode::Instant, - organization_id: None, - }; - assert!(validate_inputs(&inputs).is_err()); - assert!(inputs.capture_system_audio); - inputs.capture_system_audio = false; - assert!(validate_inputs(&inputs).is_ok()); - inputs.capture_system_audio = true; - inputs.mode = RecordingMode::Studio; - assert!(validate_inputs(&inputs).is_ok()); - } - #[tokio::test] - async fn terminal_observer_does_not_revoke_an_owned_successful_stop() { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let active = attempt.0.operation.lock().await; - assert!(!attempt.terminal_needs_cleanup()); - assert!(!attempt.cancelled()); - drop(active); - assert!(attempt.terminal_needs_cleanup()); - assert!(attempt.cancelled()); - } - #[tokio::test] - async fn local_completion_rechecks_after_held_app_lock_before_success_or_delete() { - for delete_after_upload in [false, true] { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let state = tokio::sync::RwLock::new(Vec::new()); - let held = state.read().await; - let deleted = AtomicBool::new(false); - let shared = AtomicBool::new(false); - let future = run_control( - &attempt, - false, - || true, - || async { - let result = complete_local( - &attempt, - Ok(()), - state.write(), - |mut state, result| async move { - state.push(result.is_ok()); - Ok(()) - }, - ) - .await; - if result.is_ok() { - shared.store(true, Ordering::Release); - if delete_after_upload { - deleted.store(true, Ordering::Release); - } - } - attempt.record_terminal(false, result) - }, - ); - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - attempt.prepare_control(); - drop(held); - assert!(future.await.is_err()); - assert_eq!(*state.read().await, vec![false]); - assert!(!deleted.load(Ordering::Acquire)); - assert!(!shared.load(Ordering::Acquire)); - } - } - - #[tokio::test] - async fn local_completion_rechecks_after_owned_cleanup_await() { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let state = tokio::sync::RwLock::new(false); - let (release, released) = tokio::sync::oneshot::channel(); - let (entered, entering) = tokio::sync::oneshot::channel(); - let shared = AtomicBool::new(false); - let future = run_control( - &attempt, - false, - || true, - || async { - let result = complete_local( - &attempt, - Ok(()), - state.write(), - |mut state, result| async move { - assert!(result.is_ok()); - entered.send(()).unwrap(); - released.await.unwrap(); - *state = true; - Ok(()) - }, - ) - .await; - if result.is_ok() { - shared.store(true, Ordering::Release); - } - attempt.record_terminal(false, result) - }, - ); - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - entering.await.unwrap(); - attempt.prepare_control(); - release.send(()).unwrap(); - assert!(future.await.is_err()); - assert!(*state.read().await); - assert!(!shared.load(Ordering::Acquire)); - } - - #[tokio::test] - async fn queued_stop_and_discard_replay_first_joined_error() { - for queued_discard in [false, true] { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let current = AtomicBool::new(true); - let state = tokio::sync::RwLock::new(()); - let (release, released) = tokio::sync::oneshot::channel(); - let first = run_control( - &attempt, - false, - || current.load(Ordering::Acquire), - || async { - released.await.unwrap(); - let result = complete_local( - &attempt, - Err::<(), _>("required audio failed".into()), - state.write(), - |_, result| async move { - assert_eq!(result.unwrap_err(), "required audio failed"); - Err("later cleanup error".into()) - }, - ) - .await; - attempt.0.ui_ready.store(true, Ordering::Release); - let result = attempt.record_terminal(false, result); - current.store(false, Ordering::Release); - result - }, - ); - tokio::pin!(first); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut first) - .await - .is_err() - ); - attempt.prepare_control(); - let second = run_control( - &attempt, - queued_discard, - || current.load(Ordering::Acquire), - || async { panic!("queued control must not finalize or delete again") }, - ); - tokio::pin!(second); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut second) - .await - .is_err() - ); - release.send(()).unwrap(); - assert_eq!(first.await.unwrap_err(), "required audio failed"); - assert_eq!(second.await.unwrap_err(), "required audio failed"); - } - } - - #[tokio::test] - async fn successful_stop_is_idempotent_but_is_not_successful_discard() { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - run_control( - &attempt, - false, - || true, - || async { attempt.record_terminal(false, Ok(())) }, - ) - .await - .unwrap(); - run_control( - &attempt, - false, - || false, - || async { panic!("already finished") }, - ) - .await - .unwrap(); - let result = run_control( - &attempt, - true, - || false, - || async { panic!("must not delete") }, - ) - .await; - assert!(result.unwrap_err().contains("not discarded")); - run_control( - &attempt, - false, - || false, - || async { panic!("already finished") }, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn uncertain_cleanup_and_unrelated_generation_never_replay_ui_ready_success() { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - attempt.0.ui_ready.store(true, Ordering::Release); - for _ in 0..2 { - assert_eq!( - run_control( - &attempt, - false, - || true, - || async { Err("cleanup unconfirmed".into()) } - ) - .await - .unwrap_err(), - "cleanup unconfirmed" - ); - } - assert!(attempt.0.terminal.lock().unwrap().is_none()); - let result = run_control( - &attempt, - false, - || false, - || async { panic!("stale generation must not execute") }, - ) - .await; - assert!(result.unwrap_err().contains("superseded")); - } - - #[tokio::test] - async fn terminal_failure_persists_failed_metadata_and_keeps_local_media() { - let path = - std::env::temp_dir().join(format!("cap-instant-terminal-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&path).unwrap(); - let media = path.join("synthetic-bytes"); - std::fs::write(&media, b"preserve").unwrap(); - RecordingMeta { - platform: None, - project_path: path.clone(), - pretty_name: "Synthetic terminal failure".into(), - sharing: None, - inner: RecordingMetaInner::Instant(InstantRecordingMeta::InProgress { - recording: false, - }), - upload: Some(cap_project::UploadMeta::Complete), - } - .save_for_project() - .unwrap(); - persist_terminal_failure(&path, "late cancellation").await; - let meta = RecordingMeta::load_for_project(&path).unwrap(); - assert!( - matches!(meta.inner, RecordingMetaInner::Instant(InstantRecordingMeta::Failed { error }) if error == "late cancellation") - ); - assert_eq!(std::fs::read(&media).unwrap(), b"preserve"); - std::fs::remove_dir_all(path).unwrap(); - } - fn synthetic_success_effects( - attempt: &Attempt, - copies: &Mutex>, - cancel_before_copy: bool, - clipboard_error: bool, - ) -> Result<(), String> { - dispatch_success_effects( - attempt, - "https://example.invalid/s/fixture?existing=1", - SuccessEffects { - recording_automation: || {}, - upload_automation: || {}, - open_link: |_| { - if cancel_before_copy { - attempt.cancel(); - } - Ok(()) - }, - copy_link: |link| { - copies.lock().unwrap().push(link); - if clipboard_error { - Err("synthetic clipboard unavailable".into()) - } else { - Ok(()) - } - }, - sound: || {}, - }, - ) - } - - async fn finish_synthetic_control( - attempt: &Attempt, - state: &tokio::sync::RwLock<()>, - copies: &Mutex>, - outcome: Result, String>, - discard: bool, - ) -> Result<(), String> { - let successful_upload = matches!(outcome, Ok(Some(()))); - let mut result = - complete_local(attempt, outcome, state.write(), |_, _| async { Ok(()) }).await; - if result.is_ok() && successful_upload { - result = synthetic_success_effects(attempt, copies, false, false); - } - attempt.record_terminal(discard, result) - } - - #[tokio::test] - async fn normal_instant_success_copies_original_link_once_without_auto_open_and_not_on_replay() - { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let state = tokio::sync::RwLock::new(()); - let copies = Mutex::new(Vec::new()); - run_control( - &attempt, - false, - || true, - || finish_synthetic_control(&attempt, &state, &copies, Ok(Some(())), false), - ) - .await - .unwrap(); - run_control( - &attempt, - false, - || false, - || async { panic!("successful replay must not dispatch effects") }, - ) - .await - .unwrap(); - assert_eq!( - *copies.lock().unwrap(), - vec!["https://example.invalid/s/fixture?existing=1"] - ); - } - - #[tokio::test] - async fn failed_and_discarded_controls_never_copy_share_link() { - for (outcome, discard) in [ - (Err("required source failed".into()), false), - (Ok(None), true), - ] { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let state = tokio::sync::RwLock::new(()); - let copies = Mutex::new(Vec::new()); - let result = run_control( - &attempt, - discard, - || true, - || finish_synthetic_control(&attempt, &state, &copies, outcome, discard), - ) - .await; - assert_eq!(result.is_ok(), discard); - assert!(copies.lock().unwrap().is_empty()); - } - } - - #[tokio::test] - async fn cancelled_held_local_completion_and_queued_replay_never_copy_link() { - let attempt = Attempt::new(); - attempt.0.started.send_replace(true); - let state = tokio::sync::RwLock::new(()); - let held = state.read().await; - let copies = Mutex::new(Vec::new()); - let first = run_control( - &attempt, - false, - || true, - || finish_synthetic_control(&attempt, &state, &copies, Ok(Some(())), false), - ); - tokio::pin!(first); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut first) - .await - .is_err() - ); - attempt.prepare_control(); - let second = run_control( - &attempt, - false, - || false, - || async { panic!("cancelled replay must not dispatch effects") }, - ); - tokio::pin!(second); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut second) - .await - .is_err() - ); - drop(held); - assert!(first.await.is_err()); - assert!(second.await.is_err()); - assert!(copies.lock().unwrap().is_empty()); - } - - #[test] - fn clipboard_boundary_rechecks_revocation_and_preserves_nonfatal_clipboard_error_policy() { - let cancelled = Attempt::new(); - let cancelled_copies = Mutex::new(Vec::new()); - assert!(synthetic_success_effects(&cancelled, &cancelled_copies, true, false).is_err()); - assert!(cancelled_copies.lock().unwrap().is_empty()); - let healthy = Attempt::new(); - let attempted_copies = Mutex::new(Vec::new()); - synthetic_success_effects(&healthy, &attempted_copies, false, true).unwrap(); - assert_eq!(attempted_copies.lock().unwrap().len(), 1); - } - } -} - -#[cfg(test)] -mod editor_recording_restart_tests { - use super::*; - use std::sync::Mutex; - - fn editor_target(path: &str) -> EditorRecordingTarget { - EditorRecordingTarget(Arc::new(Mutex::new(Some(PathBuf::from(path))))) - } - - #[tokio::test] - async fn successful_restart_keeps_destination_through_terminal_cleanup() { - let target = editor_target("original.cap"); - let result = restart_with_editor_target( - &target, - async { - assert!(take_editor_target_after_recording(&target, true).is_none()); - Ok(()) - }, - async { Ok(()) }, - || async { - assert_eq!( - target.0.lock().unwrap().as_deref(), - Some(Path::new("original.cap")) - ); - Ok(RecordingAction::Started) - }, - |_, _| async { panic!("successful restart must retain the destination") }, - ) - .await; - assert!(matches!(result, Ok(RecordingAction::Started))); - assert_eq!( - take_editor_target_after_recording(&target, false), - Some(PathBuf::from("original.cap")) - ); - assert!(target.0.lock().unwrap().is_none()); - } - - #[tokio::test] - async fn failed_replacement_start_clears_only_its_editor_destination() { - let target = editor_target("original.cap"); - let restored = Mutex::new(None); - let result = restart_with_editor_target( - &target, - async { Ok(()) }, - async { Ok(()) }, - || async { Err("requested microphone unavailable".into()) }, - |expected, cleanup_completed| { - let target = ⌖ - let restored = &restored; - async move { - *restored.lock().unwrap() = take_failed_restart_editor_target( - target, - expected.as_deref(), - cleanup_completed, - ); - } - }, - ) - .await; - assert!(result.is_err()); - assert_eq!( - *restored.lock().unwrap(), - Some(PathBuf::from("original.cap")) - ); - assert!(target.0.lock().unwrap().is_none()); - } - - #[tokio::test] - async fn unconfirmed_old_stop_keeps_active_editor_ownership() { - let target = editor_target("original.cap"); - let result = restart_with_editor_target( - &target, - async { Err("Studio cleanup is unconfirmed".into()) }, - async { panic!("controls cannot restore before confirmed cleanup") }, - || async { panic!("replacement cannot start before confirmed cleanup") }, - |expected, cleanup_completed| { - let target = ⌖ - async move { - assert!( - take_failed_restart_editor_target( - target, - expected.as_deref(), - cleanup_completed - ) - .is_none() - ); - } - }, - ) - .await; - assert!(result.is_err()); - assert_eq!( - target.0.lock().unwrap().as_deref(), - Some(Path::new("original.cap")) - ); - } - - #[tokio::test] - async fn replaced_editor_destination_cancels_restart_without_consuming_new_target() { - let target = editor_target("original.cap"); - let result = restart_with_editor_target( - &target, - async { - *target.0.lock().unwrap() = Some(PathBuf::from("replacement.cap")); - Ok(()) - }, - async { Ok(()) }, - || async { panic!("a different editor now owns recording") }, - |expected, cleanup_completed| { - let target = ⌖ - async move { - assert!( - take_failed_restart_editor_target( - target, - expected.as_deref(), - cleanup_completed - ) - .is_none() - ); - } - }, - ) - .await; - assert!(result.is_err()); - assert_eq!( - target.0.lock().unwrap().as_deref(), - Some(Path::new("replacement.cap")) - ); - } - - #[tokio::test] - async fn losing_restart_cannot_clear_the_winners_target_after_recording_state_is_cleared() { - let target = editor_target("original.cap"); - let result = restart_with_editor_target( - &target, - async { Err("Studio terminal completion is stale".into()) }, - async { panic!("the losing restart cannot restore controls") }, - || async { panic!("the losing restart cannot start another recording") }, - |expected, cleanup_completed| { - let target = ⌖ - async move { - let recording_cleared = true; - assert!( - take_failed_restart_editor_target( - target, - expected.as_deref(), - cleanup_completed && recording_cleared, - ) - .is_none() - ); - } - }, - ) - .await; - assert!(result.is_err()); - assert_eq!( - target.0.lock().unwrap().as_deref(), - Some(Path::new("original.cap")) - ); - } - - #[tokio::test] - async fn restoration_failure_clears_the_owned_target_before_any_replacement_start() { - let target = editor_target("original.cap"); - let result = restart_with_editor_target( - &target, - async { Ok(()) }, - async { Err("Recording controls could not be restored".into()) }, - || async { panic!("replacement cannot start before controls are restored") }, - |expected, cleanup_completed| { - let target = ⌖ - async move { - assert_eq!( - take_failed_restart_editor_target( - target, - expected.as_deref(), - cleanup_completed, - ), - Some(PathBuf::from("original.cap")), - ); - } - }, - ) - .await; - assert!(result.is_err()); - assert!(target.0.lock().unwrap().is_none()); - } - - #[tokio::test] - async fn caller_cancellation_does_not_abandon_restart_after_old_capture_stops() { - let target = editor_target("original.cap"); - let retained = target.clone(); - let (entered, entry) = tokio::sync::oneshot::channel(); - let (release, released) = tokio::sync::oneshot::channel(); - let (finished, finish) = tokio::sync::oneshot::channel(); - let caller = tokio::spawn(async move { - complete_studio_restart(async move { - let result = restart_with_editor_target( - &target, - async { - assert!(take_editor_target_after_recording(&target, true).is_none()); - entered.send(()).unwrap(); - released.await.unwrap(); - Ok(()) - }, - async { Ok(()) }, - || async { Ok(RecordingAction::Started) }, - |_, _| async { - panic!("owned replacement must finish after caller cancellation") - }, - ) - .await; - finished.send(()).unwrap(); - result - }) - .await - }); - entry.await.unwrap(); - caller.abort(); - assert!(matches!(caller.await, Err(error) if error.is_cancelled())); - release.send(()).unwrap(); - tokio::time::timeout(Duration::from_secs(2), finish) - .await - .unwrap() - .unwrap(); - assert_eq!( - retained.0.lock().unwrap().as_deref(), - Some(Path::new("original.cap")) - ); - } -} - -#[cfg(all(test, target_os = "linux"))] -mod studio_joined_completion_tests { - use super::*; - use std::sync::atomic::{AtomicUsize, Ordering}; - - #[tokio::test] - async fn unconfirmed_stop_never_enters_local_completion() { - let effects = AtomicUsize::new(0); - let result = after_studio_join( - async { - studio_recording::StudioStopReport { - accepted_intent: true, - quiescence: studio_recording::StudioQuiescence::Unconfirmed, - result: Err("source stop unknown".into()), - } - }, - |_| async { - effects.fetch_add(1, Ordering::SeqCst); - Ok(()) - }, - ) - .await; - assert!(result.is_err()); - assert_eq!(effects.load(Ordering::SeqCst), 0); - } - - #[tokio::test] - async fn losing_preserve_cannot_finish_while_discard_local_cleanup_is_waiting() { - let effects = Arc::new(AtomicUsize::new(0)); - let (entered, entry) = tokio::sync::oneshot::channel(); - let (release, released) = tokio::sync::oneshot::channel(); - let owner = tokio::spawn({ - let effects = effects.clone(); - async move { - after_studio_join( - async { - studio_recording::StudioStopReport { - accepted_intent: true, - quiescence: studio_recording::StudioQuiescence::Joined, - result: Ok(studio_recording::CompletedRecording { - project_path: std::path::PathBuf::from("synthetic.cap"), - meta: cap_project::StudioRecordingMeta::MultipleSegments { - inner: cap_project::MultipleSegments { - segments: Vec::new(), - cursors: Default::default(), - status: Some(cap_project::StudioRecordingStatus::Complete), - }, - }, - cursor_data: Default::default(), - clean_stopped: None, - }), - } - }, - |_| async move { - entered.send(()).unwrap(); - released.await.unwrap(); - effects.fetch_add(1, Ordering::SeqCst); - Ok(()) - }, - ) - .await - } - }); - entry.await.unwrap(); - let losing = after_studio_join( - async { - studio_recording::StudioStopReport { - accepted_intent: false, - quiescence: studio_recording::StudioQuiescence::Joined, - result: Err("different terminal action owns attempt".into()), - } - }, - |_| async { - effects.fetch_add(100, Ordering::SeqCst); - Ok(()) - }, - ) - .await; - assert!(losing.is_err()); - assert!(!owner.is_finished()); - assert_eq!(effects.load(Ordering::SeqCst), 0); - release.send(()).unwrap(); - owner.await.unwrap().unwrap(); - assert_eq!(effects.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn joined_failure_waits_for_app_lock_without_turning_into_success() { - let app_lock = Arc::new(tokio::sync::RwLock::new(())); - let held = app_lock.clone().write_owned().await; - let effects = Arc::new(AtomicUsize::new(0)); - let (joined_tx, joined_rx) = tokio::sync::oneshot::channel(); - let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); - let task = tokio::spawn({ - let effects = effects.clone(); - async move { - after_studio_join( - async { - joined_rx.await.unwrap(); - studio_recording::StudioStopReport { - accepted_intent: true, - quiescence: studio_recording::StudioQuiescence::Joined, - result: Err("requested microphone failed".into()), - } - }, - |result| async move { - let _ = entered_tx.send(()); - let _state = app_lock.write().await; - effects.fetch_add(1, Ordering::SeqCst); - result.map(|_| ()) - }, - ) - .await - } - }); - assert_eq!(effects.load(Ordering::SeqCst), 0); - joined_tx.send(()).unwrap(); - entered_rx.await.unwrap(); - assert!(!task.is_finished()); - assert_eq!(effects.load(Ordering::SeqCst), 0); - drop(held); - assert!(task.await.unwrap().is_err()); - assert_eq!(effects.load(Ordering::SeqCst), 1); - } -} - -#[cfg(all(test, any(target_os = "linux", target_os = "macos", windows)))] -mod studio_failure_presentation_tests { - use super::*; - - #[test] - fn confirmed_disk_full_message_keeps_exact_recording_path() { - let directory = Path::new("/recordings/Unfinished capture.cap"); - for error in [ - "out-of-process media finalization failed: disk full: encoder exited with code 60", - "Could not save cursor events: No space left on device (os error 28)", - "Failed to write keyboard events file: No space left on device (os error 28)", - ] { - let message = stopped_studio_error(error, directory, StudioTerminalAction::Stop); - assert!(message.starts_with("Recording stopped because your disk is full.")); - assert!(message.contains(&directory.display().to_string())); - assert!(message.ends_with("Free up space before recording again.")); - assert!(!message.contains("playable")); - } - } - - #[test] - fn discard_and_restart_errors_do_not_claim_files_were_kept() { - let error = "partial directory removal failed: No space left on device (os error 28)"; - for action in [StudioTerminalAction::Discard, StudioTerminalAction::Restart] { - assert_eq!( - stopped_studio_error(error, Path::new("recording.cap"), action), - error - ); - } - } - - #[test] - fn unrelated_recording_failures_keep_their_details() { - for error in [ - "camera disconnected", - "Permission denied (os error 13)", - "could not open /recordings/disk full recording.cap", - ] { - assert_eq!( - stopped_studio_error( - error, - Path::new("recording.cap"), - StudioTerminalAction::Stop - ), - error - ); - } - } - - #[cfg(windows)] - #[test] - fn windows_storage_full_codes_are_recognized() { - for error in ["write failed (os error 112)", "write failed (os error 39)"] { - assert!( - stopped_studio_error( - error, - Path::new("recording.cap"), - StudioTerminalAction::Stop - ) - .contains("your disk is full") - ); - } - } -} - -#[cfg(all(test, any(target_os = "macos", windows)))] -mod studio_capture_control_tests { - use super::*; - - #[tokio::test] - async fn studio_unconfirmed_or_losing_stop_cannot_restore_or_delete() { - for (accepted_intent, stop_acknowledged) in [(true, false), (false, true), (false, false)] { - let effects = std::sync::atomic::AtomicUsize::new(0); - let result = after_studio_capture_stop( - async { - studio_recording::WindowsStudioStopReport { - accepted_intent, - stop_acknowledged, - result: Err("encoder join unconfirmed".into()), - } - }, - |_| async { - effects.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - Ok(()) - }, - ) - .await; - assert!(result.is_err()); - assert_eq!(effects.load(std::sync::atomic::Ordering::SeqCst), 0); - } - } - - #[tokio::test] - async fn acknowledged_media_failure_reaches_cleanup_with_its_error() { - let effects = std::sync::atomic::AtomicUsize::new(0); - let result = after_studio_capture_stop( - async { - studio_recording::WindowsStudioStopReport { - accepted_intent: true, - stop_acknowledged: true, - result: Err("encoder exited before completing media".into()), - } - }, - |result| async { - effects.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - result.map(|_| ()) - }, - ) - .await; - assert_eq!(effects.load(std::sync::atomic::Ordering::SeqCst), 1); - assert_eq!( - result.unwrap_err(), - "encoder exited before completing media" - ); - } - - #[tokio::test] - async fn pending_stop_does_not_enter_cleanup() { - let effects = std::sync::atomic::AtomicUsize::new(0); - let (send, receive) = tokio::sync::oneshot::channel(); - let stop = after_studio_capture_stop(async { receive.await.unwrap() }, |result| async { - effects.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - result.map(|_| ()) - }); - tokio::pin!(stop); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut stop) - .await - .is_err() - ); - assert_eq!(effects.load(std::sync::atomic::Ordering::SeqCst), 0); - send.send(studio_recording::WindowsStudioStopReport { - accepted_intent: true, - stop_acknowledged: true, - result: Err("media finalization failed".into()), - }) - .unwrap_or_else(|_| panic!("Stop receiver closed before acknowledgement")); - assert!(stop.await.is_err()); - assert_eq!(effects.load(std::sync::atomic::Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn disk_full_presentation_requires_confirmed_stop_ownership() { - for (accepted_intent, stop_acknowledged) in [(true, true), (true, false), (false, true)] { - let entered = std::sync::atomic::AtomicBool::new(false); - let result = after_studio_capture_stop( - async { - studio_recording::WindowsStudioStopReport { - accepted_intent, - stop_acknowledged, - result: Err("disk full: encoder exited with code 60".into()), - } - }, - |result| async { - entered.store(true, std::sync::atomic::Ordering::SeqCst); - result.map_err(|error| { - stopped_studio_error( - &error, - Path::new("retained.cap"), - StudioTerminalAction::Stop, - ) - }) - }, - ) - .await; - let confirmed = accepted_intent && stop_acknowledged; - assert_eq!(entered.load(std::sync::atomic::Ordering::SeqCst), confirmed); - assert_eq!( - result - .err() - .unwrap() - .starts_with("Recording stopped because your disk is full."), - confirmed - ); - } - } -} - -pub(crate) fn preparing_presentation_snapshot( - preset: Option<&ProjectConfiguration>, - capture_target: Option<&ScreenCaptureTarget>, -) -> Result { - let mut config = preset - .cloned() - .ok_or("Default presentation requires ordinary finalization")?; - if matches!(capture_target, None | Some(ScreenCaptureTarget::CameraOnly)) - || !matches!( - config.background.source, - cap_project::BackgroundSource::Color { .. } - | cap_project::BackgroundSource::Gradient { - animated: None | Some(false), - .. - } - ) - || config.background.notch.is_some() - { - return Err("Presentation requires ordinary finalization".into()); - } - config.cursor.size = cap_project::CursorConfiguration::default().size; - apply_screen_recording_presentation_defaults(&mut config, capture_target, false, None); - Ok(config) -} - -#[cfg(test)] -mod preparing_presentation_tests { - use super::*; - - #[test] - fn stopped_camera_snapshot_matches_preparing_and_final_presentation() { - let target = ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }; - for blur in [ - cap_project::BackgroundBlurMode::Off, - cap_project::BackgroundBlurMode::Remove, - ] { - let snapshot = StudioCameraSnapshot { - state: crate::camera::CameraPreviewState { - background_blur: blur, - ..Default::default() - }, - placement: cap_recording::camera_placement::RecordingCameraPlacement::from_bounds( - [860.0, 20.0, 200.0, 200.0], - [0.0, 0.0, 1920.0, 1080.0], - ), - }; - let original = ProjectConfiguration::default(); - let mut preparing = - preparing_presentation_snapshot(Some(&original), Some(&target)).unwrap(); - let mut finished = original.clone(); - snapshot.apply(&mut preparing); - snapshot.apply(&mut finished); - assert_eq!( - serde_json::to_value(&preparing.camera).unwrap(), - serde_json::to_value(&finished.camera).unwrap() - ); - assert!(finished.camera.manual_position.is_none()); - assert_eq!( - serde_json::to_value(&finished.camera.position).unwrap(), - serde_json::json!({"x": "center", "y": "top"}) - ); - assert_eq!(finished.camera.size, original.camera.size); - assert_eq!( - serde_json::to_value(&finished.timeline).unwrap(), - serde_json::to_value(&original.timeline).unwrap() - ); - } - } - - #[test] - fn preparing_never_guesses_default_wallpaper_or_animated_background() { - assert!(preparing_presentation_snapshot(None, None).is_err()); - let config = ProjectConfiguration::default(); - assert!(preparing_presentation_snapshot(Some(&config), None).is_err()); - assert!( - preparing_presentation_snapshot(Some(&config), Some(&ScreenCaptureTarget::CameraOnly)) - .is_err() - ); - } - - #[test] - fn static_preset_defaults_match_the_existing_screen_defaults() { - let target = ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }; - let mut config = ProjectConfiguration::default(); - config.cursor.size = 173; - config.background.padding = 0.0; - config.background.rounding = 0.0; - let projected = preparing_presentation_snapshot(Some(&config), Some(&target)).unwrap(); - let mut ordinary = config; - ordinary.cursor.size = cap_project::CursorConfiguration::default().size; - apply_screen_recording_presentation_defaults(&mut ordinary, Some(&target), false, None); - assert_eq!( - serde_json::to_value(projected).unwrap(), - serde_json::to_value(ordinary).unwrap() - ); - } -} - -#[cfg(test)] -mod preparing_presentation_parity_tests { - use super::*; - use cap_project::{BackgroundSource, ClipConfiguration, ClipOffsets, ScreenMovementSpring}; - - fn value(config: &ProjectConfiguration) -> serde_json::Value { - serde_json::to_value(config).unwrap() - } - - fn targets() -> Vec { - vec![ - ScreenCaptureTarget::Display { - id: "1".parse().unwrap(), - }, - ScreenCaptureTarget::Window { - id: "1".parse().unwrap(), - }, - ScreenCaptureTarget::Area { - screen: "1".parse().unwrap(), - bounds: scap_targets::bounds::LogicalBounds::new( - scap_targets::bounds::LogicalPosition::new(10.0, 20.0), - scap_targets::bounds::LogicalSize::new(320.0, 240.0), - ), - }, - ] - } - - fn one_segment(end: f64) -> Vec { - vec![TimelineSegment { - recording_clip: 0, - start: 0.0, - end, - timescale: 1.0, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }] - } - - fn preset() -> ProjectConfiguration { - ProjectConfiguration::default() - } - - fn preparing_static_projection( - preset: &ProjectConfiguration, - target: &ScreenCaptureTarget, - stopped: &TimelineConfiguration, - ) -> ProjectConfiguration { - let mut snapshot = preparing_presentation_snapshot(Some(preset), Some(target)).unwrap(); - apply_recording_camera_preview_state( - &mut snapshot, - &crate::camera::CameraPreviewState::default(), - ); - crate::editor_preparing::project_from_preparing_presentation(&snapshot, stopped) - } - - fn ordinary_static_projection( - preset: &ProjectConfiguration, - target: &ScreenCaptureTarget, - segments: Vec, - ) -> ProjectConfiguration { - let mut config = preset.clone(); - config.cursor.size = cap_project::CursorConfiguration::default().size; - apply_screen_recording_presentation_defaults(&mut config, Some(target), false, None); - apply_recording_camera_preview_state( - &mut config, - &crate::camera::CameraPreviewState::default(), - ); - config.timeline = Some(recording_timeline(segments, Vec::new())); - config - } - - #[test] - fn static_color_projection_preserves_preset_and_matches_ordinary_screen_defaults() { - for target in targets() { - let mut preset = preset(); - preset.cursor.size = 173; - preset.background.padding = 0.0; - preset.background.rounding = 0.0; - preset.background.source = BackgroundSource::Color { - value: [32, 64, 128], - alpha: 160, - }; - preset.screen_movement_spring = ScreenMovementSpring { - stiffness: 120.0, - damping: 14.0, - mass: 1.0, - }; - let original = value(&preset); - let stopped = recording_timeline(one_segment(6.0), Vec::new()); - let projected = preparing_static_projection(&preset, &target, &stopped); - let ordinary = ordinary_static_projection(&preset, &target, one_segment(6.0)); - assert_eq!(value(&projected), value(&ordinary)); - assert_eq!(value(&preset), original); - assert_eq!(projected.background.padding, 10.0); - assert_eq!( - projected.background.rounding, - if matches!(target, ScreenCaptureTarget::Area { .. }) { - 0.0 - } else { - 7.5 - } - ); - assert_eq!( - projected.cursor.size, - cap_project::CursorConfiguration::default().size - ); - assert_eq!( - serde_json::to_value(projected.screen_movement_spring).unwrap(), - serde_json::to_value(ScreenMovementSpring::default()).unwrap() - ); - assert!(matches!( - projected.background.source, - BackgroundSource::Color { - value: [32, 64, 128], - alpha: 160 - } - )); - } - } - - #[test] - fn static_gradients_preserve_explicit_presentation_values_for_each_screen_target() { - for target in targets() { - for animated in [None, Some(false)] { - let mut preset = preset(); - preset.background.padding = 23.0; - preset.background.rounding = 11.0; - preset.background.source = BackgroundSource::Gradient { - from: [17, 41, 65], - to: [193, 211, 227], - angle: 217, - noise_intensity: Some(0.125), - noise_scale: Some(1.5), - animated, - animation_speed: Some(0.25), - }; - preset.screen_movement_spring = ScreenMovementSpring { - stiffness: 150.0, - damping: 22.0, - mass: 0.8, - }; - let stopped = recording_timeline(one_segment(6.0), Vec::new()); - let projected = preparing_static_projection(&preset, &target, &stopped); - assert_eq!( - value(&projected), - value(&ordinary_static_projection( - &preset, - &target, - one_segment(6.0) - )) - ); - assert_eq!(projected.background.padding, 23.0); - assert_eq!(projected.background.rounding, 11.0); - assert_eq!( - serde_json::to_value(&projected.background.source).unwrap(), - serde_json::to_value(&preset.background.source).unwrap() - ); - } - } - } - - #[test] - fn unresolved_and_late_presentation_inputs_are_declined_without_mutating_presets() { - let target = targets().remove(0); - assert!(preparing_presentation_snapshot(None, Some(&target)).is_err()); - let mut preset = preset(); - for source in [ - BackgroundSource::Wallpaper { - path: Some("wallpaper.jpg".into()), - }, - BackgroundSource::Image { - path: Some("image.png".into()), - }, - BackgroundSource::AnimatedGradient { - config: Default::default(), - }, - BackgroundSource::Gradient { - from: [0, 0, 0], - to: [255, 255, 255], - angle: 90, - noise_intensity: None, - noise_scale: None, - animated: Some(true), - animation_speed: None, - }, - ] { - preset.background.source = source; - let original = value(&preset); - assert!(preparing_presentation_snapshot(Some(&preset), Some(&target)).is_err()); - assert_eq!(value(&preset), original); - } - preset.background.source = BackgroundSource::default(); - for enabled in [false, true] { - preset.background.notch = Some(cap_project::NotchConfiguration { - enabled, - ..Default::default() - }); - assert!(preparing_presentation_snapshot(Some(&preset), Some(&target)).is_err()); - } - preset.background.notch = None; - assert!(preparing_presentation_snapshot(Some(&preset), None).is_err()); - assert!( - preparing_presentation_snapshot(Some(&preset), Some(&ScreenCaptureTarget::CameraOnly)) - .is_err() - ); - } - - #[test] - fn stopped_timeline_replaces_preset_edits_but_keeps_explicit_clip_offsets() { - let target = targets().remove(0); - let mut preset = preset(); - preset.clips = vec![ClipConfiguration { - index: 0, - offsets: ClipOffsets { - camera: 0.125, - mic: -0.25, - system_audio: 0.0625, - }, - offsets_auto_calculated: false, - }]; - let old_zoom: ZoomSegment = serde_json::from_value(serde_json::json!({ - "start": 1.0, "end": 4.0, "amount": 2.0, "mode": "auto" - })) - .unwrap(); - preset.timeline = Some(recording_timeline(one_segment(2.0), vec![old_zoom])); - let stopped = recording_timeline(one_segment(6.0), Vec::new()); - let projected = preparing_static_projection(&preset, &target, &stopped); - let ordinary = ordinary_static_projection(&preset, &target, one_segment(6.0)); - assert_eq!(value(&projected), value(&ordinary)); - let timeline = projected.timeline.as_ref().unwrap(); - assert_eq!(timeline.segments.len(), 1); - assert_eq!(timeline.segments[0].end, 6.0); - assert!(timeline.zoom_segments.is_empty()); - assert_eq!( - serde_json::to_value(&projected.clips).unwrap(), - serde_json::to_value(&preset.clips).unwrap() - ); - assert_eq!(preset.timeline.as_ref().unwrap().segments[0].end, 2.0); - assert_eq!(preset.timeline.as_ref().unwrap().zoom_segments.len(), 1); - } - - #[test] - fn ordinary_late_camera_state_preserves_unrelated_camera_fields() { - for (shape, expected_shape, rounding) in [ - (CameraPreviewShape::Round, CameraShape::Square, 100.0), - (CameraPreviewShape::Square, CameraShape::Square, 25.0), - (CameraPreviewShape::Full, CameraShape::Source, 25.0), - ] { - for blur in [ - cap_project::BackgroundBlurMode::Off, - cap_project::BackgroundBlurMode::Light, - cap_project::BackgroundBlurMode::Heavy, - ] { - let mut config = preset(); - config.camera.size = 41.0; - config.camera.mirror = true; - let mut expected = config.clone(); - expected.camera.shape = expected_shape; - expected.camera.rounding = rounding; - if shape == CameraPreviewShape::Round { - expected.camera.rounding_type = CornerStyle::Rounded; - } - expected.camera.background_blur.mode = blur; - apply_recording_camera_preview_state( - &mut config, - &crate::camera::CameraPreviewState { - size: 99.0, - shape: shape.clone(), - mirrored: false, - background_blur: blur, - }, - ); - assert_eq!(value(&config), value(&expected)); - assert_eq!(config.camera.size, 41.0); - assert!(config.camera.mirror); - } - } - } -} diff --git a/apps/desktop/src-tauri/src/recording_settings.rs b/apps/desktop/src-tauri/src/recording_settings.rs deleted file mode 100644 index 4341d34688b..00000000000 --- a/apps/desktop/src-tauri/src/recording_settings.rs +++ /dev/null @@ -1,168 +0,0 @@ -use cap_recording::{ - RecordingMode, - feeds::{ - camera::{CameraDeviceSettings, DeviceOrModelID}, - microphone::MicrophoneDeviceSettings, - }, - sources::screen_capture::ScreenCaptureTarget, -}; -use std::collections::HashMap; -use tauri::{AppHandle, Wry}; -use tauri_plugin_store::StoreExt; - -use crate::tray; - -#[derive(serde::Serialize, serde::Deserialize, specta::Type, Debug, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum RecordingTargetMode { - Display, - Window, - Area, - Camera, -} - -#[derive(serde::Serialize, serde::Deserialize, specta::Type, Debug, Clone, Default)] -#[serde(rename_all = "camelCase", default)] -pub struct RecordingSettingsStore { - pub target: Option, - pub mic_name: Option, - #[serde(deserialize_with = "deserialize_camera_id")] - pub camera_id: Option, - pub mode: Option, - pub system_audio: bool, - pub organization_id: Option, - pub camera_device_settings: HashMap, - pub microphone_device_settings: HashMap, -} - -fn deserialize_camera_id<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = ::deserialize(deserializer)?; - match serde_json::from_value(value) { - Ok(camera_id) => Ok(camera_id), - Err(error) => { - tracing::warn!(%error, "Ignoring invalid saved camera selection"); - Ok(None) - } - } -} - -impl RecordingSettingsStore { - const KEY: &'static str = "recording_settings"; - - pub fn get(app: &AppHandle) -> Result, String> { - match app.store("store").map(|s| s.get(Self::KEY)) { - Ok(Some(store)) => match serde_json::from_value(store) { - Ok(settings) => Ok(Some(settings)), - Err(e) => Err(format!( - "Failed to deserialize recording settings store: {e}" - )), - }, - _ => Ok(None), - } - } - - pub fn set_mode(app: &AppHandle, mode: RecordingMode) -> Result<(), String> { - let store = app.store("store").map_err(|e| e.to_string())?; - - let mut settings = Self::get(app)?.unwrap_or_default(); - settings.mode = Some(mode); - - store.set(Self::KEY, serde_json::json!(settings)); - store.save().map_err(|e| e.to_string()) - } - - pub fn camera_settings_for( - app: &AppHandle, - id: &DeviceOrModelID, - ) -> Option { - Self::get(app).ok().flatten().and_then(|settings| { - settings - .camera_device_settings - .get(&camera_key(id)) - .copied() - }) - } - - pub fn microphone_settings_for( - app: &AppHandle, - label: &str, - ) -> Option { - Self::get(app) - .ok() - .flatten() - .and_then(|settings| settings.microphone_device_settings.get(label).copied()) - } -} - -pub fn camera_key(id: &DeviceOrModelID) -> String { - match id { - DeviceOrModelID::DeviceID(device_id) => format!("device:{device_id}"), - DeviceOrModelID::ModelID(model_id) => format!("model:{model_id}"), - } -} - -#[tauri::command] -#[specta::specta] -pub fn set_recording_mode(app: AppHandle, mode: RecordingMode) -> Result<(), String> { - RecordingSettingsStore::set_mode(&app, mode)?; - tray::update_tray_icon_for_mode(&app, mode); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::RecordingSettingsStore; - use serde_json::json; - - #[test] - fn invalid_camera_selection_preserves_other_recording_preferences() { - for camera_id in [ - json!({"DeviceID": "camera-a", "ModelID": "046d:08e5"}), - json!({"ModelID": "missing-separator"}), - json!({"DeviceID": 123}), - json!({"unknown": "camera-a"}), - json!({}), - json!("camera-a"), - ] { - let mut saved = json!({ - "target": {"variant": "cameraOnly"}, - "micName": "Saved microphone", - "cameraId": camera_id, - "mode": "studio", - "systemAudio": true, - "organizationId": "saved-organization", - "cameraDeviceSettings": { - "model:046d:08e5": {"width": 1280, "height": 720, "frameRate": 60.0} - }, - "microphoneDeviceSettings": { - "Saved microphone": {"sampleRate": 48000, "channels": 1} - } - }); - let settings: RecordingSettingsStore = serde_json::from_value(saved.clone()).unwrap(); - assert!(settings.camera_id.is_none()); - saved["cameraId"] = serde_json::Value::Null; - assert_eq!(serde_json::to_value(settings).unwrap(), saved); - } - } - - #[test] - fn valid_camera_variants_and_none_round_trip() { - for camera_id in [ - json!({"DeviceID": "camera-a"}), - json!({"ModelID": "046d:08e5"}), - serde_json::Value::Null, - ] { - let settings: RecordingSettingsStore = - serde_json::from_value(json!({"cameraId": camera_id})).unwrap(); - assert_eq!( - serde_json::to_value(settings).unwrap()["cameraId"], - camera_id - ); - } - let settings: RecordingSettingsStore = serde_json::from_value(json!({})).unwrap(); - assert!(settings.camera_id.is_none()); - } -} diff --git a/apps/desktop/src-tauri/src/recording_telemetry.rs b/apps/desktop/src-tauri/src/recording_telemetry.rs deleted file mode 100644 index 5d3238f8762..00000000000 --- a/apps/desktop/src-tauri/src/recording_telemetry.rs +++ /dev/null @@ -1,271 +0,0 @@ -use cap_recording::PipelineHealthEvent; -use std::{ - sync::{ - Arc, - atomic::{AtomicBool, AtomicU64, Ordering}, - }, - time::Instant, -}; - -#[derive(Debug)] -pub struct RecordingHealthAccumulator { - started_at: Instant, - capture_stalls_count: AtomicU64, - capture_stalls_max_ms: AtomicU64, - mixer_stalls_count: AtomicU64, - mixer_stalls_max_ms: AtomicU64, - audio_gaps_count: AtomicU64, - audio_gaps_total_ms: AtomicU64, - frame_drop_rate_high_count: AtomicU64, - source_restarts_count: AtomicU64, - muxer_crash_count: AtomicU64, - audio_degraded_count: AtomicU64, - muxer_crashed_fired: AtomicBool, - audio_degraded_fired: AtomicBool, -} - -#[derive(Debug, Clone, Copy)] -pub struct RecordingHealthSnapshot { - pub capture_stalls_count: u64, - pub capture_stalls_max_ms: u64, - pub mixer_stalls_count: u64, - pub mixer_stalls_max_ms: u64, - pub audio_gaps_count: u64, - pub audio_gaps_total_ms: u64, - pub frame_drop_rate_high_count: u64, - pub source_restarts_count: u64, - pub muxer_crash_count: u64, - pub audio_degraded_count: u64, -} - -#[derive(Debug, Clone, Copy)] -pub enum CriticalEvent { - MuxerCrashed { - reason_index: usize, - seconds_into_recording: f64, - }, - AudioDegraded { - reason_index: usize, - seconds_into_recording: f64, - }, -} - -impl RecordingHealthAccumulator { - pub fn new() -> Arc { - Arc::new(Self { - started_at: Instant::now(), - capture_stalls_count: AtomicU64::new(0), - capture_stalls_max_ms: AtomicU64::new(0), - mixer_stalls_count: AtomicU64::new(0), - mixer_stalls_max_ms: AtomicU64::new(0), - audio_gaps_count: AtomicU64::new(0), - audio_gaps_total_ms: AtomicU64::new(0), - frame_drop_rate_high_count: AtomicU64::new(0), - source_restarts_count: AtomicU64::new(0), - muxer_crash_count: AtomicU64::new(0), - audio_degraded_count: AtomicU64::new(0), - muxer_crashed_fired: AtomicBool::new(false), - audio_degraded_fired: AtomicBool::new(false), - }) - } - - pub fn seconds_since_start(&self) -> f64 { - self.started_at.elapsed().as_secs_f64() - } - - pub fn record_event(&self, event: &PipelineHealthEvent) -> Option<(String, CriticalEvent)> { - match event { - PipelineHealthEvent::Stalled { source, waited_ms } => { - let is_mixer = source.starts_with("mixer:"); - let waited = *waited_ms; - if is_mixer { - self.mixer_stalls_count.fetch_add(1, Ordering::Relaxed); - update_max(&self.mixer_stalls_max_ms, waited); - } else { - self.capture_stalls_count.fetch_add(1, Ordering::Relaxed); - update_max(&self.capture_stalls_max_ms, waited); - } - None - } - PipelineHealthEvent::AudioGapDetected { gap_ms } => { - self.audio_gaps_count.fetch_add(1, Ordering::Relaxed); - self.audio_gaps_total_ms - .fetch_add(*gap_ms, Ordering::Relaxed); - None - } - PipelineHealthEvent::FrameDropRateHigh { .. } => { - self.frame_drop_rate_high_count - .fetch_add(1, Ordering::Relaxed); - None - } - PipelineHealthEvent::SourceRestarting => { - self.source_restarts_count.fetch_add(1, Ordering::Relaxed); - None - } - PipelineHealthEvent::SourceRestarted => None, - PipelineHealthEvent::MuxerCrashed { reason } => { - self.muxer_crash_count.fetch_add(1, Ordering::Relaxed); - let first = !self.muxer_crashed_fired.swap(true, Ordering::AcqRel); - if first { - Some(( - reason.clone(), - CriticalEvent::MuxerCrashed { - reason_index: 0, - seconds_into_recording: self.seconds_since_start(), - }, - )) - } else { - None - } - } - PipelineHealthEvent::AudioDegradedToVideoOnly { reason } => { - self.audio_degraded_count.fetch_add(1, Ordering::Relaxed); - let first = !self.audio_degraded_fired.swap(true, Ordering::AcqRel); - if first { - Some(( - reason.clone(), - CriticalEvent::AudioDegraded { - reason_index: 0, - seconds_into_recording: self.seconds_since_start(), - }, - )) - } else { - None - } - } - PipelineHealthEvent::DiskSpaceLow { .. } - | PipelineHealthEvent::DiskSpaceExhausted { .. } - | PipelineHealthEvent::DeviceLost { .. } - | PipelineHealthEvent::EncoderRebuilt { .. } - | PipelineHealthEvent::SourceAudioReset { .. } - | PipelineHealthEvent::RecoveryFragmentCorrupt { .. } - | PipelineHealthEvent::CaptureTargetLost { .. } - | PipelineHealthEvent::MicrophoneDigitalSilence => None, - } - } - - pub fn snapshot(&self) -> RecordingHealthSnapshot { - RecordingHealthSnapshot { - capture_stalls_count: self.capture_stalls_count.load(Ordering::Acquire), - capture_stalls_max_ms: self.capture_stalls_max_ms.load(Ordering::Acquire), - mixer_stalls_count: self.mixer_stalls_count.load(Ordering::Acquire), - mixer_stalls_max_ms: self.mixer_stalls_max_ms.load(Ordering::Acquire), - audio_gaps_count: self.audio_gaps_count.load(Ordering::Acquire), - audio_gaps_total_ms: self.audio_gaps_total_ms.load(Ordering::Acquire), - frame_drop_rate_high_count: self.frame_drop_rate_high_count.load(Ordering::Acquire), - source_restarts_count: self.source_restarts_count.load(Ordering::Acquire), - muxer_crash_count: self.muxer_crash_count.load(Ordering::Acquire), - audio_degraded_count: self.audio_degraded_count.load(Ordering::Acquire), - } - } -} - -fn update_max(cell: &AtomicU64, candidate: u64) { - let mut current = cell.load(Ordering::Acquire); - while candidate > current { - match cell.compare_exchange_weak(current, candidate, Ordering::AcqRel, Ordering::Acquire) { - Ok(_) => return, - Err(observed) => current = observed, - } - } -} - -pub fn mode_label(mode: cap_recording::RecordingMode) -> &'static str { - match mode { - cap_recording::RecordingMode::Studio => "studio", - cap_recording::RecordingMode::Instant => "instant", - cap_recording::RecordingMode::Screenshot => "screenshot", - } -} - -pub fn target_kind_label( - target: &cap_recording::sources::screen_capture::ScreenCaptureTarget, -) -> &'static str { - use cap_recording::sources::screen_capture::ScreenCaptureTarget; - match target { - ScreenCaptureTarget::Display { .. } => "display", - ScreenCaptureTarget::Window { .. } => "window", - ScreenCaptureTarget::Area { .. } => "area", - ScreenCaptureTarget::CameraOnly => "camera_only", - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_stall(source: &str, waited_ms: u64) -> PipelineHealthEvent { - PipelineHealthEvent::Stalled { - source: source.to_string(), - waited_ms, - } - } - - #[test] - fn accumulator_separates_capture_and_mixer_stalls() { - let acc = RecordingHealthAccumulator::new(); - acc.record_event(&make_stall("screen-video", 80)); - acc.record_event(&make_stall("screen-system-audio", 50)); - acc.record_event(&make_stall("mixer:mic", 200)); - acc.record_event(&make_stall("mixer:sys", 950)); - let snap = acc.snapshot(); - assert_eq!(snap.capture_stalls_count, 2); - assert_eq!(snap.capture_stalls_max_ms, 80); - assert_eq!(snap.mixer_stalls_count, 2); - assert_eq!(snap.mixer_stalls_max_ms, 950); - } - - #[test] - fn accumulator_fires_muxer_crash_once() { - let acc = RecordingHealthAccumulator::new(); - let first = acc.record_event(&PipelineHealthEvent::MuxerCrashed { - reason: "boom".to_string(), - }); - let second = acc.record_event(&PipelineHealthEvent::MuxerCrashed { - reason: "again".to_string(), - }); - assert!( - first.is_some(), - "first muxer crash must fire critical event" - ); - assert!( - second.is_none(), - "second muxer crash must not re-fire (still increments counter)" - ); - let snap = acc.snapshot(); - assert_eq!(snap.muxer_crash_count, 2); - } - - #[test] - fn accumulator_fires_audio_degraded_once() { - let acc = RecordingHealthAccumulator::new(); - let first = acc.record_event(&PipelineHealthEvent::AudioDegradedToVideoOnly { - reason: "mic died".to_string(), - }); - let second = acc.record_event(&PipelineHealthEvent::AudioDegradedToVideoOnly { - reason: "sys died".to_string(), - }); - assert!(first.is_some()); - assert!(second.is_none()); - let snap = acc.snapshot(); - assert_eq!(snap.audio_degraded_count, 2); - } - - #[test] - fn accumulator_accumulates_audio_gaps_and_drop_rate() { - let acc = RecordingHealthAccumulator::new(); - acc.record_event(&PipelineHealthEvent::AudioGapDetected { gap_ms: 120 }); - acc.record_event(&PipelineHealthEvent::AudioGapDetected { gap_ms: 340 }); - acc.record_event(&PipelineHealthEvent::FrameDropRateHigh { - source: "screen-video".to_string(), - rate_pct: 12.5, - }); - acc.record_event(&PipelineHealthEvent::SourceRestarting); - acc.record_event(&PipelineHealthEvent::SourceRestarted); - let snap = acc.snapshot(); - assert_eq!(snap.audio_gaps_count, 2); - assert_eq!(snap.audio_gaps_total_ms, 460); - assert_eq!(snap.frame_drop_rate_high_count, 1); - assert_eq!(snap.source_restarts_count, 1); - } -} diff --git a/apps/desktop/src-tauri/src/recordings_locations.rs b/apps/desktop/src-tauri/src/recordings_locations.rs deleted file mode 100644 index 13f3c57ea55..00000000000 --- a/apps/desktop/src-tauri/src/recordings_locations.rs +++ /dev/null @@ -1,655 +0,0 @@ -use std::{ - collections::HashSet, - path::{Path, PathBuf}, -}; - -use serde::{Deserialize, Serialize}; -use specta::Type; -use tauri::{AppHandle, Manager, Wry}; -use tauri_specta::Event; -use tracing::{info, warn}; - -use crate::{ - ArcLock, RecordingState, editor_window::EditorInstances, general_settings::GeneralSettingsStore, -}; - -/// Staging directory (inside the destination) used for cross-volume moves. -/// Projects are copied here first, then renamed into place, so a partially -/// copied project can never appear in the recordings list: the staging dir -/// itself has no recording-meta.json and every scanner skips it. -const MIGRATION_STAGING_DIR: &str = ".migrating-tmp"; - -/// How many previously used custom folders we remember so their recordings -/// stay visible after switching away from them. -const MAX_PREVIOUS_PATHS: usize = 20; - -pub fn default_recordings_dir(app: &AppHandle) -> PathBuf { - app.path().app_data_dir().unwrap().join("recordings") -} - -/// Every directory that may contain recordings: the active directory first, -/// then the default location and any previously used custom folders. -/// Deduplicated by canonical path; directories that don't exist are skipped. -pub fn known_recordings_dirs(app: &AppHandle) -> Vec { - let mut dirs = vec![ - GeneralSettingsStore::recordings_dir(app), - default_recordings_dir(app), - ]; - - if let Ok(Some(settings)) = GeneralSettingsStore::get(app) { - dirs.extend( - settings - .previous_recordings_paths - .iter() - .map(PathBuf::from) - .filter(|p| p.is_absolute()), - ); - } - - dedupe_existing_dirs(dirs) -} - -fn dedupe_existing_dirs(dirs: Vec) -> Vec { - let mut seen = HashSet::new(); - let mut out = Vec::new(); - for dir in dirs { - if !dir.is_dir() { - continue; - } - // Canonicalize so the same folder reached via different spellings - // (trailing components, symlinks, Windows case differences) is only - // scanned once; otherwise every recording would be listed twice. - let key = dir.canonicalize().unwrap_or_else(|_| dir.clone()); - if seen.insert(key) { - out.push(dir); - } - } - out -} - -/// Records the folder we're switching away from so its recordings remain -/// visible. `new_path` is removed from history since it's current again. -pub fn remember_previous_recordings_path( - settings: &mut GeneralSettingsStore, - previous: Option, - new_path: Option<&str>, -) { - if let Some(new_path) = new_path { - settings.previous_recordings_paths.retain(|p| p != new_path); - } - - if let Some(previous) = previous - && new_path != Some(previous.as_str()) - && !settings.previous_recordings_paths.contains(&previous) - { - settings.previous_recordings_paths.push(previous); - let len = settings.previous_recordings_paths.len(); - if len > MAX_PREVIOUS_PATHS { - settings - .previous_recordings_paths - .drain(..len - MAX_PREVIOUS_PATHS); - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] -pub struct RecordingsMigrationProgress { - pub total: u32, - pub done: u32, - pub current: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct RecordingsMigrationFailure { - pub name: String, - pub error: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct RecordingsMigrationSummary { - pub moved: u32, - pub skipped_in_use: u32, - pub failed: Vec, -} - -/// Project directories that live in a known location other than the current -/// recordings directory. -fn migratable_projects(app: &AppHandle) -> Vec { - let current = GeneralSettingsStore::recordings_dir(app); - let current_key = current.canonicalize().unwrap_or(current); - - let mut projects = Vec::new(); - for dir in known_recordings_dirs(app) { - if dir.canonicalize().unwrap_or_else(|_| dir.clone()) == current_key { - continue; - } - let Ok(entries) = std::fs::read_dir(&dir) else { - continue; - }; - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() || path.extension().and_then(|e| e.to_str()) != Some("cap") { - continue; - } - // Never try to move a folder into itself if the user nested the - // new location inside an old project dir. Compare canonicalized - // paths on both sides (canonicalize is not prefix-stable on - // Windows, where it adds `\\?\`). - let path_key = path.canonicalize().unwrap_or_else(|_| path.clone()); - if current_key.starts_with(&path_key) { - continue; - } - projects.push(path); - } - } - projects -} - -#[tauri::command] -#[specta::specta] -pub async fn count_recordings_to_migrate(app: AppHandle) -> Result { - let app = app.clone(); - tokio::task::spawn_blocking(move || migratable_projects(&app).len() as u32) - .await - .map_err(|e| format!("Migration scan failed: {e}")) -} - -static MIGRATION_IN_PROGRESS: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -/// Releases the migration lock on every exit path, including `?` returns. -struct MigrationLockGuard; - -impl Drop for MigrationLockGuard { - fn drop(&mut self) { - MIGRATION_IN_PROGRESS.store(false, std::sync::atomic::Ordering::SeqCst); - } -} - -#[tauri::command] -#[specta::specta] -pub async fn migrate_recordings_to_current_dir( - app: AppHandle, -) -> Result { - // Two concurrent migrations would race on the shared staging dir and - // report projects the other run already moved as failures. - if MIGRATION_IN_PROGRESS - .compare_exchange( - false, - true, - std::sync::atomic::Ordering::SeqCst, - std::sync::atomic::Ordering::SeqCst, - ) - .is_err() - { - return Err("A recordings migration is already in progress".to_string()); - } - let _lock = MigrationLockGuard; - - let dest = GeneralSettingsStore::recordings_dir(&app); - - // Leftover staging from an interrupted previous run: the copies in there - // are incomplete by definition (their source was kept), so remove them. - let stale_staging = dest.join(MIGRATION_STAGING_DIR); - if stale_staging.exists() - && let Err(e) = std::fs::remove_dir_all(&stale_staging) - { - warn!("Failed to clean stale migration staging dir: {e}"); - } - - let projects = { - let app = app.clone(); - tokio::task::spawn_blocking(move || migratable_projects(&app)) - .await - .map_err(|e| format!("Migration scan failed: {e}"))? - }; - - let total = projects.len() as u32; - let mut summary = RecordingsMigrationSummary { - moved: 0, - skipped_in_use: 0, - failed: Vec::new(), - }; - - for (i, project) in projects.into_iter().enumerate() { - let name = project - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| project.display().to_string()); - - RecordingsMigrationProgress { - total, - done: i as u32, - current: Some(name.clone()), - } - .emit(&app) - .ok(); - - // Re-resolved for every project: an editor can open (or a recording - // stop) while earlier projects are still being copied. - let in_use = in_use_project_dirs(&app).await; - - if project_is_in_use(&project, &in_use) { - info!(?project, "Migration: skipping in-use recording"); - summary.skipped_in_use += 1; - continue; - } - - let dest = dest.clone(); - let src = project.clone(); - let result = tokio::task::spawn_blocking(move || move_project_dir(&src, &dest)) - .await - .map_err(|e| format!("Move task failed: {e}")) - .and_then(|r| r); - - match result { - Ok(new_path) => { - info!(from = ?project, to = ?new_path, "Migration: moved recording"); - summary.moved += 1; - } - Err(error) => { - warn!(?project, %error, "Migration: failed to move recording; kept in place"); - summary - .failed - .push(RecordingsMigrationFailure { name, error }); - } - } - } - - RecordingsMigrationProgress { - total, - done: total, - current: None, - } - .emit(&app) - .ok(); - - Ok(summary) -} - -async fn in_use_project_dirs(app: &AppHandle) -> HashSet { - let mut in_use = HashSet::new(); - - let state = app.state::>(); - { - let state = state.read().await; - if let RecordingState::Active(recording) = &state.recording_state { - let dir = &recording.common().recording_dir; - in_use.insert(dir.canonicalize().unwrap_or_else(|_| dir.clone())); - } - } - - for path in EditorInstances::open_project_paths(app).await { - in_use.insert(path.canonicalize().unwrap_or(path)); - } - - in_use -} - -fn project_is_in_use(project: &Path, in_use: &HashSet) -> bool { - let key = project - .canonicalize() - .unwrap_or_else(|_| project.to_path_buf()); - if in_use.contains(&key) { - return true; - } - - // Belt and braces: a recording that is mid-write has InProgress status in - // its meta even if we failed to resolve it from app state. - match cap_project::RecordingMeta::load_for_project(project) { - Ok(meta) => match &meta.inner { - cap_project::RecordingMetaInner::Studio(studio) => { - if let cap_project::StudioRecordingMeta::MultipleSegments { inner, .. } = - studio.as_ref() - { - matches!( - inner.status, - Some(cap_project::StudioRecordingStatus::InProgress) - ) - } else { - false - } - } - cap_project::RecordingMetaInner::Instant(instant) => { - matches!( - instant, - cap_project::InstantRecordingMeta::InProgress { .. } - ) - } - }, - // Unreadable meta is not proof of activity; old or partially written - // projects still deserve to be moved rather than stranded. - Err(_) => false, - } -} - -/// Moves one project directory into `dest_dir`, keeping the name unique. -/// Tries a plain rename first; on failure (typically a cross-volume move) -/// falls back to copy + verify + delete, staged so the destination never -/// exposes a partially copied project. The source is only removed after the -/// copy has been verified. -pub fn move_project_dir(src: &Path, dest_dir: &Path) -> Result { - let name = src - .file_name() - .and_then(|n| n.to_str()) - .ok_or_else(|| "Invalid project directory name".to_string())?; - - // A destination inside the source would make the recursive copy descend - // into its own output and never terminate. Refuse outright. - let src_key = src.canonicalize().unwrap_or_else(|_| src.to_path_buf()); - let dest_key = dest_dir - .canonicalize() - .unwrap_or_else(|_| dest_dir.to_path_buf()); - if dest_key.starts_with(&src_key) { - return Err("Destination folder is inside this recording".to_string()); - } - - let unique = cap_utils::ensure_unique_filename(name, dest_dir)?; - let target = dest_dir.join(&unique); - - match std::fs::rename(src, &target) { - Ok(()) => Ok(target), - Err(rename_err) => copy_verify_delete(src, dest_dir, &unique).map_err(|copy_err| { - format!("rename failed ({rename_err}); copy fallback failed: {copy_err}") - }), - } -} - -fn copy_verify_delete(src: &Path, dest_dir: &Path, unique: &str) -> Result { - let staging_root = dest_dir.join(MIGRATION_STAGING_DIR); - let staging = staging_root.join(unique); - - if staging.exists() { - std::fs::remove_dir_all(&staging) - .map_err(|e| format!("Failed to clear staging dir: {e}"))?; - } - std::fs::create_dir_all(&staging_root) - .map_err(|e| format!("Failed to create staging dir: {e}"))?; - - let cleanup = |err: String| -> String { - let _ = std::fs::remove_dir_all(&staging); - err - }; - - copy_dir_recursive(src, &staging).map_err(cleanup)?; - - let src_stats = dir_stats(src).map_err(cleanup)?; - let staged_stats = dir_stats(&staging).map_err(cleanup)?; - if src_stats != staged_stats { - return Err(cleanup(format!( - "verification failed: source has {} files / {} bytes, copy has {} files / {} bytes", - src_stats.0, src_stats.1, staged_stats.0, staged_stats.1 - ))); - } - - let target = dest_dir.join(unique); - std::fs::rename(&staging, &target) - .map_err(|e| cleanup(format!("Failed to finalize copied project: {e}")))?; - - let _ = std::fs::remove_dir_all(&staging_root); - - // The copy is verified and in place; a failure to delete the source only - // leaves a duplicate behind, which is safe. Don't fail the migration. - if let Err(e) = std::fs::remove_dir_all(src) { - warn!( - ?src, - "Migration: copied project but failed to remove the original: {e}" - ); - } - - Ok(target) -} - -fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), String> { - std::fs::create_dir_all(dest).map_err(|e| format!("Failed to create {dest:?}: {e}"))?; - - for entry in std::fs::read_dir(src).map_err(|e| format!("Failed to read {src:?}: {e}"))? { - let entry = entry.map_err(|e| format!("Failed to read entry in {src:?}: {e}"))?; - let from = entry.path(); - let to = dest.join(entry.file_name()); - let file_type = entry - .file_type() - .map_err(|e| format!("Failed to stat {from:?}: {e}"))?; - - if file_type.is_dir() { - copy_dir_recursive(&from, &to)?; - } else { - std::fs::copy(&from, &to).map_err(|e| format!("Failed to copy {from:?}: {e}"))?; - } - } - - Ok(()) -} - -/// (file count, total bytes), following the same traversal as -/// `copy_dir_recursive` so the two are comparable. -fn dir_stats(dir: &Path) -> Result<(u64, u64), String> { - let mut files = 0u64; - let mut bytes = 0u64; - - for entry in std::fs::read_dir(dir).map_err(|e| format!("Failed to read {dir:?}: {e}"))? { - let entry = entry.map_err(|e| format!("Failed to read entry in {dir:?}: {e}"))?; - let file_type = entry - .file_type() - .map_err(|e| format!("Failed to stat {:?}: {e}", entry.path()))?; - - if file_type.is_dir() { - let (f, b) = dir_stats(&entry.path())?; - files += f; - bytes += b; - } else { - files += 1; - bytes += entry - .metadata() - .map_err(|e| format!("Failed to stat {:?}: {e}", entry.path()))? - .len(); - } - } - - Ok((files, bytes)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_project(parent: &Path, name: &str, files: &[(&str, &str)]) -> PathBuf { - let dir = parent.join(name); - std::fs::create_dir_all(dir.join("content")).unwrap(); - for (rel, contents) in files { - std::fs::write(dir.join(rel), contents).unwrap(); - } - dir - } - - #[test] - fn move_project_same_volume() { - let tmp = tempfile::tempdir().unwrap(); - let src_dir = tmp.path().join("old"); - let dest_dir = tmp.path().join("new"); - std::fs::create_dir_all(&src_dir).unwrap(); - std::fs::create_dir_all(&dest_dir).unwrap(); - - let project = make_project( - &src_dir, - "Test.cap", - &[ - ("recording-meta.json", "{}"), - ("content/display.mp4", "vid"), - ], - ); - - let moved = move_project_dir(&project, &dest_dir).unwrap(); - - assert_eq!(moved, dest_dir.join("Test.cap")); - assert!(!project.exists()); - assert_eq!( - std::fs::read_to_string(moved.join("content/display.mp4")).unwrap(), - "vid" - ); - } - - #[test] - fn move_project_renames_on_collision() { - let tmp = tempfile::tempdir().unwrap(); - let src_dir = tmp.path().join("old"); - let dest_dir = tmp.path().join("new"); - std::fs::create_dir_all(&src_dir).unwrap(); - std::fs::create_dir_all(&dest_dir).unwrap(); - - make_project( - &dest_dir, - "Test.cap", - &[("recording-meta.json", "existing")], - ); - let project = make_project(&src_dir, "Test.cap", &[("recording-meta.json", "incoming")]); - - let moved = move_project_dir(&project, &dest_dir).unwrap(); - - assert_ne!(moved, dest_dir.join("Test.cap")); - assert!(moved.starts_with(&dest_dir)); - assert!(!project.exists()); - assert_eq!( - std::fs::read_to_string(dest_dir.join("Test.cap/recording-meta.json")).unwrap(), - "existing" - ); - assert_eq!( - std::fs::read_to_string(moved.join("recording-meta.json")).unwrap(), - "incoming" - ); - } - - #[test] - fn copy_fallback_moves_and_removes_source() { - let tmp = tempfile::tempdir().unwrap(); - let src_dir = tmp.path().join("old"); - let dest_dir = tmp.path().join("new"); - std::fs::create_dir_all(&src_dir).unwrap(); - std::fs::create_dir_all(&dest_dir).unwrap(); - - let project = make_project( - &src_dir, - "Test.cap", - &[ - ("recording-meta.json", "{}"), - ("content/display.mp4", "0123456789"), - ], - ); - - let moved = copy_verify_delete(&project, &dest_dir, "Test.cap").unwrap(); - - assert_eq!(moved, dest_dir.join("Test.cap")); - assert!(!project.exists()); - assert!(!dest_dir.join(MIGRATION_STAGING_DIR).exists()); - assert_eq!( - std::fs::read_to_string(moved.join("content/display.mp4")).unwrap(), - "0123456789" - ); - } - - #[test] - fn copy_fallback_cleans_stale_staging_of_same_project() { - let tmp = tempfile::tempdir().unwrap(); - let src_dir = tmp.path().join("old"); - let dest_dir = tmp.path().join("new"); - std::fs::create_dir_all(&src_dir).unwrap(); - - // A leftover partial copy from an interrupted run must not corrupt - // the fresh copy or trip verification. - let stale = dest_dir.join(MIGRATION_STAGING_DIR).join("Test.cap"); - std::fs::create_dir_all(&stale).unwrap(); - std::fs::write(stale.join("recording-meta.json"), "stale partial").unwrap(); - - let project = make_project(&src_dir, "Test.cap", &[("recording-meta.json", "fresh")]); - - let moved = copy_verify_delete(&project, &dest_dir, "Test.cap").unwrap(); - - assert_eq!( - std::fs::read_to_string(moved.join("recording-meta.json")).unwrap(), - "fresh" - ); - assert!(!project.exists()); - } - - #[test] - fn move_refuses_destination_inside_source() { - let tmp = tempfile::tempdir().unwrap(); - let project = make_project(tmp.path(), "Test.cap", &[("recording-meta.json", "{}")]); - let nested_dest = project.join("content/new-storage"); - std::fs::create_dir_all(&nested_dest).unwrap(); - - let result = move_project_dir(&project, &nested_dest); - - assert!(result.is_err()); - assert!(project.exists()); - assert_eq!( - std::fs::read_to_string(project.join("recording-meta.json")).unwrap(), - "{}" - ); - } - - #[test] - fn dir_stats_detects_mismatch() { - let tmp = tempfile::tempdir().unwrap(); - let a = make_project(tmp.path(), "a.cap", &[("recording-meta.json", "same")]); - let b = make_project( - tmp.path(), - "b.cap", - &[("recording-meta.json", "different!")], - ); - let c = make_project(tmp.path(), "c.cap", &[("recording-meta.json", "same")]); - - assert_ne!(dir_stats(&a).unwrap(), dir_stats(&b).unwrap()); - assert_eq!(dir_stats(&a).unwrap(), dir_stats(&c).unwrap()); - } - - #[test] - fn dedupe_existing_dirs_skips_missing_and_duplicates() { - let tmp = tempfile::tempdir().unwrap(); - let a = tmp.path().join("a"); - let b = tmp.path().join("b"); - std::fs::create_dir_all(&a).unwrap(); - std::fs::create_dir_all(&b).unwrap(); - - let deduped = dedupe_existing_dirs(vec![ - a.clone(), - b.clone(), - a.clone(), - tmp.path().join("missing"), - ]); - - assert_eq!(deduped, vec![a, b]); - } - - #[test] - fn remember_previous_path_dedupes_and_caps() { - let mut settings = GeneralSettingsStore::default(); - - remember_previous_recordings_path(&mut settings, Some("/old".into()), Some("/new")); - assert_eq!(settings.previous_recordings_paths, vec!["/old"]); - - // Switching again to the same target doesn't duplicate history. - remember_previous_recordings_path(&mut settings, Some("/old".into()), Some("/new")); - assert_eq!(settings.previous_recordings_paths, vec!["/old"]); - - // Switching back to a remembered folder removes it from history. - remember_previous_recordings_path(&mut settings, Some("/new".into()), Some("/old")); - assert_eq!(settings.previous_recordings_paths, vec!["/new"]); - - // Re-picking the current folder records nothing and keeps history. - remember_previous_recordings_path(&mut settings, Some("/old".into()), Some("/old")); - assert_eq!(settings.previous_recordings_paths, vec!["/new"]); - - for i in 0..(MAX_PREVIOUS_PATHS + 5) { - remember_previous_recordings_path(&mut settings, Some(format!("/dir{i}")), None); - } - assert_eq!(settings.previous_recordings_paths.len(), MAX_PREVIOUS_PATHS); - assert!( - settings - .previous_recordings_paths - .contains(&format!("/dir{}", MAX_PREVIOUS_PATHS + 4)) - ); - } -} diff --git a/apps/desktop/src-tauri/src/recovery.rs b/apps/desktop/src-tauri/src/recovery.rs deleted file mode 100644 index 2538b2c4e03..00000000000 --- a/apps/desktop/src-tauri/src/recovery.rs +++ /dev/null @@ -1,242 +0,0 @@ -use cap_project::StudioRecordingMeta; -use cap_recording::recovery::{RecoveryError, RecoveryManager}; -use chrono::NaiveDate; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::path::{Path, PathBuf}; -use tauri::{AppHandle, Emitter, Manager}; -use tracing::info; - -use crate::create_screenshot; - -const RECOVERY_CUTOFF_DATE: (i32, u32, u32) = (2025, 12, 31); - -pub(crate) fn finalization_storage_error(path: &Path) -> String { - format!( - "Not enough space to finish this recording. Your recording files have been kept at {}. Free up space, then click Recover Recording.", - path.display() - ) -} - -fn ensure_finalization_storage_with( - path: &Path, - inspect: impl FnOnce(&Path) -> std::io::Result, -) -> Result<(), String> { - let storage = inspect(path) - .map_err(|error| format!("Could not check available space for this recording: {error}"))?; - if !storage.can_finalize() { - return Err(finalization_storage_error(path)); - } - Ok(()) -} - -pub(crate) fn ensure_finalization_storage( - work_path: &Path, - display_path: &Path, -) -> Result<(), String> { - ensure_finalization_storage_with(display_path, |_| { - cap_utils::disk_space::recording_storage(work_path) - }) -} - -fn is_storage_full_remux_error(error: &cap_enc_ffmpeg::remux::RemuxError) -> bool { - match error { - cap_enc_ffmpeg::remux::RemuxError::Io(error) => { - error.kind() == std::io::ErrorKind::StorageFull - } - cap_enc_ffmpeg::remux::RemuxError::Ffmpeg(ffmpeg::Error::Other { errno }) => { - *errno == ffmpeg::error::ENOSPC - } - _ => false, - } -} - -pub(crate) fn is_storage_full_recovery_error(error: &RecoveryError) -> bool { - match error { - RecoveryError::Io(error) => error.kind() == std::io::ErrorKind::StorageFull, - RecoveryError::VideoConcat(error) - | RecoveryError::AudioConcat(error) - | RecoveryError::MediaMerge(error) => is_storage_full_remux_error(error), - _ => false, - } -} - -fn recovery_error_message(path: &Path, error: RecoveryError) -> String { - let storage_full = is_storage_full_recovery_error(&error); - tracing::error!(project_path = %path.display(), error = %error, "Recording recovery failed"); - if storage_full { - finalization_storage_error(path) - } else { - error.to_string() - } -} - -fn parse_recording_date(pretty_name: &str) -> Option { - let date_part = pretty_name.strip_prefix("Cap ")?; - let date_str = date_part.split(" at ").next()?; - NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok() -} - -fn is_recording_after_cutoff(pretty_name: &str) -> bool { - let Some(recording_date) = parse_recording_date(pretty_name) else { - return false; - }; - let cutoff = NaiveDate::from_ymd_opt( - RECOVERY_CUTOFF_DATE.0, - RECOVERY_CUTOFF_DATE.1, - RECOVERY_CUTOFF_DATE.2, - ) - .expect("Invalid cutoff date"); - recording_date > cutoff -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct IncompleteRecordingInfo { - pub project_path: String, - pub pretty_name: String, - pub segment_count: u32, - pub estimated_duration_secs: f64, -} - -#[tauri::command] -#[specta::specta] -pub async fn find_incomplete_recordings( - app: AppHandle, -) -> Result, String> { - let recordings_dirs = crate::recordings_locations::known_recordings_dirs(&app); - - let result = tokio::task::spawn_blocking(move || { - let incomplete_list = recordings_dirs - .iter() - .flat_map(|dir| RecoveryManager::find_incomplete(dir)); - - incomplete_list - .into_iter() - .filter(|recording| is_recording_after_cutoff(&recording.meta.pretty_name)) - .map(|recording| IncompleteRecordingInfo { - project_path: recording.project_path.to_string_lossy().to_string(), - pretty_name: recording.meta.pretty_name.clone(), - segment_count: recording.recoverable_segments.len() as u32, - estimated_duration_secs: recording.estimated_duration.as_secs_f64(), - }) - .collect::>() - }) - .await - .map_err(|e| format!("Recovery scan task failed: {e}"))?; - - Ok(result) -} - -#[tauri::command] -pub async fn get_recording_recovery_success( - app: AppHandle, - project_path: String, -) -> Result, String> { - app.state::() - .recovery_success(Path::new(&project_path)) - .await -} - -#[tauri::command] -#[specta::specta] -pub async fn recover_recording(app: AppHandle, project_path: String) -> Result { - let project = crate::FinalizationProject::admit(PathBuf::from(&project_path)).await?; - let token = app - .state::() - .start_recovering(project)?; - let recover_start = std::time::Instant::now(); - let app_for_recovery = app.clone(); - let result = crate::run_finalization_worker(token, move |project| { - let path = project.work_path(); - ensure_finalization_storage(path, project.display_path())?; - let recording = RecoveryManager::inspect_recording(path) - .ok_or_else(|| "No recoverable segments found".to_string())?; - if recording.recoverable_segments.is_empty() { - return Err("No recoverable segments found".to_string()); - } - let estimated_duration_secs = recording.estimated_duration.as_secs(); - let recovered = RecoveryManager::recover(&recording) - .map_err(|error| recovery_error_message(project.display_path(), error))?; - project.validate()?; - let validation_took_ms = recover_start.elapsed().as_millis() as u64; - - let segment_count = match &recovered.meta { - StudioRecordingMeta::SingleSegment { .. } => 1, - StudioRecordingMeta::MultipleSegments { inner } => inner.segments.len(), - }; - - info!( - "Recovered recording with {} segments: {}", - segment_count, project_path - ); - - crate::telemetry::async_capture_event( - &app_for_recovery, - crate::telemetry::AnalyticsEvent::RecordingRecovered { - trigger: "app_startup", - recovered_duration_secs: estimated_duration_secs, - segments_recovered: segment_count as u32, - validation_took_ms, - }, - ); - - let display_output_path = match &recovered.meta { - StudioRecordingMeta::SingleSegment { segment } => { - segment.display.path.to_path(&recovered.project_path) - } - StudioRecordingMeta::MultipleSegments { inner, .. } => inner.segments[0] - .display - .path - .to_path(&recovered.project_path), - }; - - let screenshots_dir = recovered.project_path.join("screenshots"); - match std::fs::create_dir_all(&screenshots_dir) { - Ok(()) => { - let display_screenshot = screenshots_dir.join("display.jpg"); - tokio::spawn(async move { - if let Err(e) = create_screenshot(display_output_path, display_screenshot, None).await { - tracing::error!("Failed to create screenshot during recovery: {}", e); - } - }); - } - Err(error) => { - tracing::warn!(project_path = %project_path, error = %error, "Failed to create recovery screenshots directory"); - } - } - - if let Err(error) = app_for_recovery.emit("recording-recovery-completed", &project_path) { - tracing::warn!(project_path = %project_path, error = %error, "Failed to notify editors of completed recording recovery"); - } - - Ok(project_path) - }) - .await; - if let Err(reason) = &result { - crate::telemetry::async_capture_event( - &app, - crate::telemetry::AnalyticsEvent::RecordingRecoveryFailed { - trigger: "app_startup", - reason: reason.clone(), - }, - ); - } - result -} - -#[tauri::command] -#[specta::specta] -pub async fn discard_incomplete_recording(project_path: String) -> Result<(), String> { - let path = PathBuf::from(&project_path); - - if !path.exists() { - return Err("Recording path does not exist".to_string()); - } - - std::fs::remove_dir_all(&path).map_err(|e| e.to_string())?; - - info!("Discarded incomplete recording: {}", project_path); - - Ok(()) -} diff --git a/apps/desktop/src-tauri/src/render_frame_event.rs b/apps/desktop/src-tauri/src/render_frame_event.rs deleted file mode 100644 index 41092676035..00000000000 --- a/apps/desktop/src-tauri/src/render_frame_event.rs +++ /dev/null @@ -1,140 +0,0 @@ -use tauri::{EventId, Listener, Runtime}; -use tauri_specta::Event; - -use crate::RenderFrameEvent; - -impl RenderFrameEvent { - pub(crate) fn listen_checked( - handle: &impl Listener, - handler: impl Fn(Self) + Send + 'static, - ) -> EventId { - // tauri-specta's typed listener panics when a numeric JS payload becomes JSON null. - handle.listen_any(Self::NAME, move |event| { - let Ok(payload) = serde_json::from_str::(event.payload()) else { - return; - }; - handler(payload); - }) - } -} - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - - use serde_json::{Value, json}; - use tauri::{Emitter, EventTarget, Listener, test::mock_app}; - use tauri_specta::Event; - - use crate::RenderFrameEvent; - - fn request(frame_number: u32) -> Value { - json!({ - "frame_number": frame_number, - "fps": 60, - "resolution_base": { "x": 1248, "y": 702 } - }) - } - - #[test] - fn malformed_requests_do_not_dispatch_and_valid_requests_still_arrive() { - let app = mock_app(); - let received = Arc::new(Mutex::new(Vec::new())); - let received_by_handler = received.clone(); - let id = RenderFrameEvent::listen_checked(&app, move |event| { - received_by_handler.lock().unwrap().push(( - event.frame_number, - event.fps, - event.resolution_base.x, - event.resolution_base.y, - )); - }); - - app.emit(RenderFrameEvent::NAME, request(0)).unwrap(); - for pointer in [ - "/frame_number", - "/fps", - "/resolution_base/x", - "/resolution_base/y", - ] { - for invalid in [ - Value::Null, - json!(-1), - json!(u64::from(u32::MAX) + 1), - json!(1.5), - json!("60"), - json!(false), - json!({}), - json!([]), - ] { - let mut payload = request(1); - *payload.pointer_mut(pointer).unwrap() = invalid; - app.emit(RenderFrameEvent::NAME, payload).unwrap(); - } - } - for malformed in [ - "null", - "{}", - "[]", - "{", - "false", - r#"{"frame_number":0,"fps":60}"#, - r#"{"frame_number":0,"fps":60,"resolution_base":null}"#, - ] { - app.emit_str(RenderFrameEvent::NAME, malformed.to_string()) - .unwrap(); - } - assert_eq!(*received.lock().unwrap(), [(0, 60, 1248, 702)]); - - app.emit_to(EventTarget::app(), RenderFrameEvent::NAME, request(150)) - .unwrap(); - app.emit_to( - EventTarget::labeled("editor-1"), - RenderFrameEvent::NAME, - request(u32::MAX), - ) - .unwrap(); - assert_eq!( - *received.lock().unwrap(), - [ - (0, 60, 1248, 702), - (150, 60, 1248, 702), - (u32::MAX, 60, 1248, 702) - ] - ); - - app.unlisten(id); - app.emit(RenderFrameEvent::NAME, request(200)).unwrap(); - assert_eq!(received.lock().unwrap().len(), 3); - } - - #[test] - fn checked_listener_preserves_the_existing_u32_contract() { - let app = mock_app(); - let received = Arc::new(Mutex::new(Vec::new())); - let received_by_handler = received.clone(); - RenderFrameEvent::listen_checked(&app, move |event| { - received_by_handler.lock().unwrap().push(( - event.frame_number, - event.fps, - event.resolution_base.x, - event.resolution_base.y, - )); - }); - for value in [0, u32::MAX] { - app.emit( - RenderFrameEvent::NAME, - json!({ - "frame_number": value, - "fps": value, - "resolution_base": { "x": value, "y": value } - }), - ) - .unwrap(); - } - assert_eq!( - *received.lock().unwrap(), - [(0, 0, 0, 0), (u32::MAX, u32::MAX, u32::MAX, u32::MAX)] - ); - } -} diff --git a/apps/desktop/src-tauri/src/resource.rs b/apps/desktop/src-tauri/src/resource.rs deleted file mode 100644 index 46fdda50169..00000000000 --- a/apps/desktop/src-tauri/src/resource.rs +++ /dev/null @@ -1,52 +0,0 @@ -// i've got an idea - -use std::{future::Future, sync::Arc}; - -use futures::pin_mut; -use tokio::sync::{watch, Mutex}; - -#[derive(Clone)] -pub struct Resource(Arc>); - -struct ResourceInner { - fetcher: Box Box>>, - value: watch::Receiver>, - value_sender: watch::Sender>, - current_fetch: Mutex>>, -} - -impl Resource { - fn new(fetcher: impl Fn() -> Box + 'static> + 'static) -> Self { - let (tx, rx) = watch::channel(None); - let value = tx.clone(); - - let this = Self(Arc::new(ResourceInner { - fetcher: Box::new(fetcher), - value: rx, - value_sender: tx, - current_fetch: Mutex::new(None), - })); - - tokio::spawn({ - let this = this.clone(); - async move { this.refetch().await } - }); - - this - } - - pub async fn refetch(&self) -> T { - let this = self.clone(); - - tokio::spawn(async move { - let fut = this.0.fetcher(); - pin_mut!(fut); - let value = fut.await; - tx.send(Some(value)).unwrap(); - }); - } - - pub fn get(&self) -> Option<&T> { - self.value.borrow().as_ref() - } -} diff --git a/apps/desktop/src-tauri/src/screenshot_editor.rs b/apps/desktop/src-tauri/src/screenshot_editor.rs deleted file mode 100644 index 0d903fdc4ca..00000000000 --- a/apps/desktop/src-tauri/src/screenshot_editor.rs +++ /dev/null @@ -1,1897 +0,0 @@ -use crate::PendingScreenshots; -use crate::frame_ws::{WSFrame, create_watch_frame_ws}; -use crate::gpu_context; -use crate::windows::{CapWindowId, ScreenshotEditorWindowIds}; -use cap_project::{ - ProjectConfiguration, RecordingMeta, RecordingMetaInner, SingleSegment, StudioRecordingMeta, - VideoMeta, -}; -use cap_rendering::{ - DecodedFrame, DecodedSegmentFrames, FrameRenderer, ProjectUniforms, RenderVideoConstants, - RendererLayers, ZoomTransformTimeline, -}; -use image::{ - GenericImageView, ImageEncoder, RgbImage, buffer::ConvertBuffer, codecs::png::PngEncoder, -}; -use relative_path::RelativePathBuf; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::io::Cursor; -use std::str::FromStr; -use std::sync::atomic::{AtomicU8, Ordering}; -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; - -const MAX_DIMENSION: u32 = 16_384; - -type PendingResult = Result, String>; -type PendingReceiver = watch::Receiver>; - -pub(crate) struct ScreenshotEditorInstanceDelivery { - instance: Arc, - cleanup_runtime: tokio::runtime::Handle, - state: AtomicU8, -} - -impl ScreenshotEditorInstanceDelivery { - const PENDING: u8 = 0; - const ADOPTED: u8 = 1; - const RETIRED: u8 = 2; - - fn new( - instance: Arc, - cleanup_runtime: tokio::runtime::Handle, - ) -> Arc { - Arc::new(Self { - instance, - cleanup_runtime, - state: AtomicU8::new(Self::PENDING), - }) - } - - fn adopt_into( - &self, - instances: &mut HashMap>, - window_label: &str, - ) -> Result, String> { - self.state - .compare_exchange( - Self::PENDING, - Self::ADOPTED, - Ordering::AcqRel, - Ordering::Acquire, - ) - .map_err(|_| "Editor instance delivery is no longer pending".to_string())?; - let instance = self.instance.clone(); - let _ = instances.insert(window_label.to_string(), instance.clone()); - Ok(instance) - } - - fn retire(&self) -> Option> { - self.state - .compare_exchange( - Self::PENDING, - Self::RETIRED, - Ordering::AcqRel, - Ordering::Acquire, - ) - .ok()?; - let instance = self.instance.clone(); - Some(self.cleanup_runtime.spawn(async move { - instance.dispose().await; - })) - } - - async fn dispose(&self) { - if let Some(cleanup) = self.retire() { - let _ = cleanup.await; - } - } -} - -impl Drop for ScreenshotEditorInstanceDelivery { - fn drop(&mut self) { - drop(self.retire()); - } -} - -#[derive(Clone)] -pub struct ScreenshotConfigUpdate { - pub revision: u32, - pub config: ProjectConfiguration, -} - -pub struct ScreenshotEditorInstance { - pub ws_port: u16, - pub ws_shutdown_token: CancellationToken, - pub config_tx: watch::Sender, - pub path: PathBuf, - pub pretty_name: String, - pub image_width: u32, - pub image_height: u32, - source_rgba: Arc>, -} - -impl ScreenshotEditorInstance { - pub async fn dispose(&self) { - self.ws_shutdown_token.cancel(); - } -} - -impl Drop for ScreenshotEditorInstance { - fn drop(&mut self) { - self.ws_shutdown_token.cancel(); - } -} - -#[derive(Clone, Default)] -pub struct PendingScreenshotEditorInstances(Arc>>); - -#[derive(Clone)] -pub struct ScreenshotEditorInstances(Arc>>>); - -pub struct WindowScreenshotEditorInstance(pub Arc); - -impl specta::function::FunctionArg for WindowScreenshotEditorInstance { - fn to_datatype(_: &mut specta::TypeMap) -> Option { - None - } -} - -impl Deref for WindowScreenshotEditorInstance { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -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)?; - - let instances = window.state::(); - let instance = futures::executor::block_on(instances.0.read()); - - if let Some(instance) = instance.get(window.label()).cloned() { - Ok(Self(instance)) - } else { - Err(InvokeError::from(format!( - "no ScreenshotEditor instance for window '{}'", - window.label(), - ))) - } - } -} - -impl ScreenshotEditorInstances { - async fn create_standalone_instance( - app_handle: &AppHandle, - path: PathBuf, - start_preview: bool, - ) -> Result, String> { - let create_started = Instant::now(); - - let (data, width, height) = { - let key = path - .parent() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_default(); - let pending = app_handle.try_state::(); - let pending_frame = pending.and_then(|p| p.remove(&key)); - - if let Some(frame) = pending_frame { - let width = frame.width; - let height = frame.height; - let channels = frame.channels; - - if width > MAX_DIMENSION || height > MAX_DIMENSION { - return Err(format!("Image dimensions exceed maximum: {width}x{height}")); - } - - let expected_len = width - .checked_mul(height) - .and_then(|p| p.checked_mul(channels)) - .ok_or_else(|| format!("Image dimensions overflow: {width}x{height}"))?; - let expected_len = usize::try_from(expected_len) - .map_err(|_| format!("Image size too large: {width}x{height}"))?; - - let data = frame.data; - - if data.len() != expected_len { - return Err(format!( - "Image data length mismatch: expected {expected_len} bytes for {width}x{height}x{channels} frame, got {}", - data.len() - )); - } - - let rgba_data = if channels == 4 { - let rgba_img = image::RgbaImage::from_raw(width, height, data) - .ok_or_else(|| format!("Invalid RGBA data for {width}x{height} frame"))?; - rgba_img.into_raw() - } else { - let rgb_img = RgbImage::from_raw(width, height, data) - .ok_or_else(|| format!("Invalid RGB data for {width}x{height} frame"))?; - let rgba_img: image::RgbaImage = rgb_img.convert(); - rgba_img.into_raw() - }; - (rgba_data, width, height) - } else { - let image_path = if path.is_dir() { - let original = path.join("original.png"); - if original.exists() { - original - } else { - 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()) - }) - .ok_or_else(|| format!("No PNG file found in directory: {path:?}"))? - } - } else { - path.clone() - }; - - let img = - image::open(&image_path).map_err(|e| format!("Failed to open image: {e}"))?; - let (w, h) = img.dimensions(); - - if w > MAX_DIMENSION || h > MAX_DIMENSION { - return Err(format!("Image dimensions exceed maximum: {w}x{h}")); - } - - w.checked_mul(h) - .and_then(|p| p.checked_mul(4)) - .ok_or_else(|| format!("Image dimensions overflow: {w}x{h}"))?; - - (img.to_rgba8().into_raw(), w, h) - } - }; - - tracing::info!( - elapsed_ms = create_started.elapsed().as_millis() as u64, - width, - height, - "screenshot_editor timing: source image ready" - ); - - let cap_dir = if path.extension().and_then(|s| s.to_str()) == Some("cap") { - Some(path.clone()) - } else if let Some(parent) = path.parent() { - if parent.extension().and_then(|s| s.to_str()) == Some("cap") { - Some(parent.to_path_buf()) - } else { - None - } - } else { - None - }; - - let (recording_meta, loaded_config) = if let Some(cap_dir) = &cap_dir { - let meta = RecordingMeta::load_for_project(cap_dir).ok(); - let config = ProjectConfiguration::load(cap_dir).ok(); - (meta, config) - } else { - (None, None) - }; - - if !start_preview { - let pretty_name = recording_meta - .as_ref() - .map(|meta| meta.pretty_name.clone()) - .unwrap_or_else(|| "Screenshot".to_string()); - let (config_tx, _) = watch::channel(ScreenshotConfigUpdate { - revision: 0, - config: loaded_config.unwrap_or_default(), - }); - - return Ok(Arc::new(ScreenshotEditorInstance { - ws_port: 0, - ws_shutdown_token: CancellationToken::new(), - config_tx, - path, - pretty_name, - image_width: width, - image_height: height, - source_rgba: Arc::new(data), - })); - } - - let (frame_tx, frame_rx) = watch::channel(None); - let (ws_port, ws_shutdown_token) = - create_watch_frame_ws(frame_rx, Default::default()).await; - let ws_guard = ws_shutdown_token.clone().drop_guard(); - if ws_port == 0 { - return Err("Failed to start screenshot editor frame websocket".to_string()); - } - - let recording_meta = if let Some(meta) = recording_meta { - meta - } else { - let filename = path - .file_name() - .ok_or_else(|| "Invalid path".to_string())? - .to_string_lossy(); - let relative_path = RelativePathBuf::from(filename.as_ref()); - let video_meta = VideoMeta { - path: relative_path.clone(), - fps: 30, - start_time: Some(0.0), - device_id: None, - }; - let segment = SingleSegment { - display: video_meta.clone(), - camera: None, - audio: None, - cursor: None, - }; - let studio_meta = StudioRecordingMeta::SingleSegment { segment }; - RecordingMeta { - platform: None, - project_path: path.parent().unwrap().to_path_buf(), - pretty_name: "Screenshot".to_string(), - sharing: None, - inner: RecordingMetaInner::Studio(Box::new(studio_meta.clone())), - upload: None, - } - }; - - let (shared, background_cache) = if let Some(gpu) = gpu_context::get_shared_gpu().await { - ( - cap_rendering::SharedWgpuDevice { - instance: (*gpu.instance).clone(), - adapter: (*gpu.adapter).clone(), - device: (*gpu.device).clone(), - queue: (*gpu.queue).clone(), - is_software_adapter: gpu.is_software_adapter, - }, - gpu.background_cache.clone(), - ) - } else { - let instance = cap_rendering::create_wgpu_instance().await; - let force_software_adapter = cap_rendering::force_software_wgpu_adapter(); - let hardware_adapter = if force_software_adapter { - None - } else { - instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - force_fallback_adapter: false, - compatible_surface: None, - }) - .await - .ok() - }; - let adapter = match hardware_adapter { - Some(adapter) => adapter, - None => instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - force_fallback_adapter: true, - compatible_surface: None, - }) - .await - .map_err(|_| "No GPU adapter found".to_string())?, - }; - let adapter_info = adapter.get_info(); - let is_software_adapter = cap_rendering::is_software_wgpu_adapter(&adapter_info); - - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("cap-rendering-device"), - required_features: wgpu::Features::empty(), - ..Default::default() - }) - .await - .map_err(|e| e.to_string())?; - ( - cap_rendering::SharedWgpuDevice { - instance, - adapter, - device, - queue, - is_software_adapter, - }, - Arc::new(cap_rendering::BackgroundTextureCache::default()), - ) - }; - - let options = cap_rendering::RenderOptions { - screen_size: cap_project::XY::new(width, height), - camera_size: None, - preserve_screen_alpha: true, - }; - - let studio_meta = match &recording_meta.inner { - RecordingMetaInner::Studio(meta) => meta.clone(), - _ => return Err("Invalid recording meta for screenshot".to_string()), - }; - - let constants = RenderVideoConstants::from_shared_device( - shared, - options, - *studio_meta, - recording_meta.clone(), - background_cache, - ); - - tracing::info!( - elapsed_ms = create_started.elapsed().as_millis() as u64, - "screenshot_editor timing: gpu + render constants ready" - ); - - let (config_tx, mut config_rx) = watch::channel(ScreenshotConfigUpdate { - revision: 0, - config: loaded_config.unwrap_or_default(), - }); - - let render_shutdown_token = ws_shutdown_token.clone(); - - let source_rgba = Arc::new(data); - - let instance = Arc::new(ScreenshotEditorInstance { - ws_port, - ws_shutdown_token, - config_tx, - path: path.clone(), - pretty_name: recording_meta.pretty_name.clone(), - image_width: width, - image_height: height, - source_rgba: source_rgba.clone(), - }); - ws_guard.disarm(); - - let decoded_frame = DecodedFrame::new(source_rgba.as_ref().clone(), width, height); - - tokio::spawn(async move { - let layers_started = Instant::now(); - let mut frame_renderer = FrameRenderer::new(&constants); - let mut layers = RendererLayers::new_with_options( - &constants.device, - &constants.queue, - constants.is_software_adapter, - ); - tracing::info!( - layers_init_ms = layers_started.elapsed().as_millis() as u64, - total_ms = create_started.elapsed().as_millis() as u64, - "screenshot_editor timing: renderer layers initialized" - ); - let shutdown_token = render_shutdown_token; - let mut current_update = config_rx.borrow().clone(); - let mut current_config = std::sync::Arc::new(current_update.config.clone()); - let mut current_revision = current_update.revision; - let mut first_frame_logged = false; - - loop { - if shutdown_token.is_cancelled() { - break; - } - let segment_frames = DecodedSegmentFrames { - screen_size: cap_project::XY::new( - decoded_frame.width(), - decoded_frame.height(), - ), - screen_frame: Some(DecodedFrame::new( - decoded_frame.data().to_vec(), - decoded_frame.width(), - decoded_frame.height(), - )), - camera_frame: None, - segment_time: 0.0, - recording_time: 0.0, - segment_has_camera: false, - }; - - let (base_w, base_h) = - ProjectUniforms::get_base_size(&constants.options, ¤t_config); - - let cursor_events = cap_project::CursorEvents::default(); - let mut zoom_timeline = ZoomTransformTimeline::from_project( - ¤t_config, - &cursor_events, - 0.0, - constants.options.screen_size, - ); - zoom_timeline.ensure_precomputed_until(1.0 / 30.0); - - let uniforms = ProjectUniforms::new( - &constants, - ¤t_config, - 0, - 30, - cap_project::XY::new(base_w, base_h), - &cursor_events, - &segment_frames, - 0.0, - &zoom_timeline, - ); - - let render_started = Instant::now(); - let rendered_frame = frame_renderer - .render_immediate( - segment_frames, - uniforms, - &cap_project::CursorEvents::default(), - true, - &mut layers, - ) - .await; - - match rendered_frame { - Ok(frame) => { - if !first_frame_logged { - first_frame_logged = true; - tracing::info!( - render_ms = render_started.elapsed().as_millis() as u64, - total_ms = create_started.elapsed().as_millis() as u64, - frame_width = frame.width, - frame_height = frame.height, - frame_bytes = frame.data.len(), - "screenshot_editor timing: first frame rendered + sent" - ); - } - let _ = frame_tx.send(Some(std::sync::Arc::new(WSFrame { - data: frame.data.into(), - width: frame.width, - height: frame.height, - stride: frame.padded_bytes_per_row, - frame_number: current_revision, - target_time_ns: frame.target_time_ns, - format: crate::frame_ws::WSFrameFormat::Rgba, - created_at: Instant::now(), - }))); - } - Err(e) => { - tracing::error!("Failed to render screenshot frame: {e}"); - } - } - - tokio::select! { - res = config_rx.changed() => { - if res.is_err() { - break; - } - current_update = config_rx.borrow().clone(); - current_revision = current_update.revision; - current_config = std::sync::Arc::new(current_update.config.clone()); - } - _ = shutdown_token.cancelled() => { - break; - } - } - } - let _ = frame_tx.send(None); - }); - - Ok(instance) - } - - pub async fn get_or_create( - window: &Window, - path: PathBuf, - ) -> Result, String> { - let CapWindowId::ScreenshotEditor { id } = - CapWindowId::from_str(window.label()).map_err(|error| error.to_string())? - else { - return Err("Invalid screenshot editor window".to_string()); - }; - let window_ids = ScreenshotEditorWindowIds::get(window.app_handle()); - with_registered_screenshot_editor(&window_ids, id, || ())?; - let instances = match window.try_state::() { - Some(s) => (*s).clone(), - None => { - window.manage(Self(Arc::new(RwLock::new(HashMap::new())))); - (*window.state::()).clone() - } - }; - let mut instances = instances.0.write().await; - if let Some(instance) = with_registered_screenshot_editor(&window_ids, id, || { - instances.get(window.label()).map(|instance| { - let instance = instance.clone(); - let config = instance.config_tx.borrow().clone(); - let _ = instance.config_tx.send(config); - instance - }) - })? { - return Ok(instance); - } - - let pending = PendingScreenshotEditorInstances::get(window.app_handle()); - let mut prewarmed = None; - if let Some(mut prewarmed_rx) = pending.take_prewarmed(window.label()).await { - loop { - let result = prewarmed_rx.borrow_and_update().clone(); - if let Some(result) = result { - prewarmed = Some(result?); - break; - } - if prewarmed_rx.changed().await.is_err() { - break; - } - } - } - let instance = match prewarmed { - Some(instance) => instance, - None => { - with_registered_screenshot_editor(&window_ids, id, || ())?; - let cleanup_runtime = tokio::runtime::Handle::current(); - let instance = - Self::create_standalone_instance(window.app_handle(), path.clone(), true) - .await?; - ScreenshotEditorInstanceDelivery::new(instance, cleanup_runtime) - } - }; - let published = with_registered_screenshot_editor(&window_ids, id, || { - instance.adopt_into(&mut instances, window.label()) - }); - drop(instances); - let instance = match published { - Ok(Ok(instance)) => instance, - Ok(Err(error)) | Err(error) => { - instance.dispose().await; - return Err(error); - } - }; - Ok(instance) - } - - pub async fn remove(window: Window) { - let instances = match window.try_state::() { - Some(s) => (*s).clone(), - None => return, - }; - - let mut instances = instances.0.write().await; - if let Some(instance) = instances.remove(window.label()) { - instance.dispose().await; - } - } - - pub async fn dispose_all(app: &AppHandle) { - let Some(instances) = app.try_state::() else { - return; - }; - - let instances = { - let mut instances = instances.0.write().await; - std::mem::take(&mut *instances) - }; - - let count = instances.len(); - for (_, instance) in instances { - instance.dispose().await; - } - - if count > 0 { - tracing::info!( - count, - "Disposed screenshot editor instances during app exit" - ); - } - } -} - -fn with_registered_screenshot_editor( - window_ids: &ScreenshotEditorWindowIds, - id: u32, - action: impl FnOnce() -> T, -) -> Result { - let ids = window_ids.ids.lock().map_err(|error| error.to_string())?; - if !ids.iter().any(|(_, registered_id)| *registered_id == id) { - return Err("Screenshot editor window is no longer registered".to_string()); - } - Ok(action()) -} - -impl PendingScreenshotEditorInstances { - pub fn get(app: &AppHandle) -> Self { - match app.try_state::() { - Some(s) => (*s).clone(), - None => { - let pending = Self::default(); - app.manage(pending); - (*app.state::()).clone() - } - } - } - - pub async fn start_prewarm(app: &AppHandle, window_label: String, path: PathBuf) { - let Ok(CapWindowId::ScreenshotEditor { id }) = CapWindowId::from_str(&window_label) else { - return; - }; - let window_ids = ScreenshotEditorWindowIds::get(app); - let pending = Self::get(app); - let app = app.clone(); - let tx = { - let mut instances = pending.0.write().await; - let admitted = with_registered_screenshot_editor(&window_ids, id, || { - use std::collections::hash_map::Entry; - match instances.entry(window_label) { - Entry::Vacant(entry) => { - let (tx, rx) = watch::channel(None); - entry.insert(rx); - Some(tx) - } - Entry::Occupied(_) => None, - } - }); - match admitted { - Ok(Some(tx)) => tx, - Ok(None) => return, - Err(error) => { - tracing::debug!(%error, "Skipping prewarm for a retired screenshot editor"); - return; - } - } - }; - - let cleanup_runtime = tokio::runtime::Handle::current(); - tokio::spawn(async move { - let result = ScreenshotEditorInstances::create_standalone_instance(&app, path, true) - .await - .map(|instance| ScreenshotEditorInstanceDelivery::new(instance, cleanup_runtime)); - tx.send(Some(result)).ok(); - }); - } - - pub async fn take_prewarmed(&self, window_label: &str) -> Option { - let mut instances = self.0.write().await; - instances.remove(window_label) - } - - pub async fn cancel_prewarm(&self, window_label: &str) { - let mut instances = self.0.write().await; - if let Some(mut rx) = instances.remove(window_label) { - tokio::spawn(async move { - let timeout = tokio::time::timeout(std::time::Duration::from_secs(10), async { - loop { - let instance_to_dispose = { - let borrowed = rx.borrow_and_update().clone(); - match borrowed { - Some(Ok(instance)) => Some(instance), - Some(Err(_)) => break, - None => None, - } - }; - - if let Some(instance) = instance_to_dispose { - instance.dispose().await; - break; - } - - if rx.changed().await.is_err() { - break; - } - } - }); - if timeout.await.is_err() { - tracing::warn!( - "Timed out waiting for prewarmed screenshot editor instance to complete for cleanup" - ); - } - }); - } - } - - pub async fn dispose_all(app: &AppHandle) { - let Some(pending) = app.try_state::() else { - return; - }; - - let pending = { - let mut instances = pending.0.write().await; - std::mem::take(&mut *instances) - }; - - let count = pending.len(); - for (_, mut rx) in pending { - let result = tokio::time::timeout(std::time::Duration::from_millis(500), async { - loop { - let instance_to_dispose = { - let borrowed = rx.borrow_and_update().clone(); - match borrowed { - Some(Ok(instance)) => Some(instance), - Some(Err(_)) => break, - None => None, - } - }; - - if let Some(instance) = instance_to_dispose { - instance.dispose().await; - break; - } - - if rx.changed().await.is_err() { - break; - } - } - }) - .await; - - if result.is_err() { - tracing::warn!( - "Timed out disposing pending screenshot editor instance during app exit" - ); - } - } - - if count > 0 { - tracing::info!( - count, - "Disposed pending screenshot editor instances during app exit" - ); - } - } -} - -#[derive(Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SerializedScreenshotEditorInstance { - pub frames_socket_url: String, - pub path: PathBuf, - pub config: Option, - pub pretty_name: String, - pub image_width: u32, - pub image_height: u32, -} - -#[derive(Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ScreenshotProjectExport { - pub image_bytes: Vec, - pub config: ProjectConfiguration, - pub image_width: u32, - pub image_height: u32, -} - -#[derive(Clone, Copy, Deserialize, Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ScreenshotOcrRegion { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, -} - -#[derive(Clone, Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ScreenshotOcrLine { - pub text: String, - pub confidence: Option, - pub bounds: ScreenshotOcrRegion, -} - -#[derive(Clone, Serialize, Type, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ScreenshotOcrResult { - pub text: String, - pub lines: Vec, - pub engine: String, -} - -struct ScreenshotOcrImage { - bgra: Vec, - width: u32, - height: u32, -} - -#[tauri::command] -#[specta::specta] -pub async fn create_screenshot_editor_instance( - window: Window, -) -> Result { - let CapWindowId::ScreenshotEditor { id } = - CapWindowId::from_str(window.label()).map_err(|e| e.to_string())? - else { - return Err("Invalid window".to_string()); - }; - - let path = { - let window_ids = ScreenshotEditorWindowIds::get(window.app_handle()); - let window_ids = window_ids.ids.lock().unwrap(); - let Some((path, _)) = window_ids.iter().find(|(_, _id)| *_id == id) else { - return Err("Screenshot editor instance not found".to_string()); - }; - path.clone() - }; - - let instance = ScreenshotEditorInstances::get_or_create(&window, path).await?; - let config = instance.config_tx.borrow().config.clone(); - - Ok(SerializedScreenshotEditorInstance { - frames_socket_url: format!("ws://localhost:{}", instance.ws_port), - path: instance.path.clone(), - config: Some(config), - pretty_name: instance.pretty_name.clone(), - image_width: instance.image_width, - image_height: instance.image_height, - }) -} - -/// Renders one tiny throwaway frame on the shared GPU at startup so the Metal -/// render pipelines are compiled before the user opens the editor. On Apple GPUs -/// pipeline *creation* is cheap but the driver defers shader compilation to the -/// first draw, which was costing ~3.5s on the first real frame. Doing it here moves -/// that cost into background startup time; the compiled pipelines are cached on the -/// shared device, so the first editor open renders immediately. -pub async fn prewarm_screenshot_renderer() { - use std::sync::atomic::{AtomicBool, Ordering}; - - static PREWARMED: AtomicBool = AtomicBool::new(false); - if PREWARMED.swap(true, Ordering::SeqCst) { - return; - } - - let Some(gpu) = gpu_context::get_shared_gpu().await else { - return; - }; - - let _ = tokio::task::spawn_blocking(cap_rendering::prewarm_fonts).await; - - let started = Instant::now(); - - let shared = cap_rendering::SharedWgpuDevice { - instance: (*gpu.instance).clone(), - adapter: (*gpu.adapter).clone(), - device: (*gpu.device).clone(), - queue: (*gpu.queue).clone(), - is_software_adapter: gpu.is_software_adapter, - }; - - let width = 64u32; - let height = 64u32; - - let video_meta = VideoMeta { - path: RelativePathBuf::from("prewarm.png"), - fps: 30, - start_time: Some(0.0), - device_id: None, - }; - let studio_meta = StudioRecordingMeta::SingleSegment { - segment: SingleSegment { - display: video_meta, - camera: None, - audio: None, - cursor: None, - }, - }; - let recording_meta = RecordingMeta { - platform: None, - project_path: std::env::temp_dir(), - pretty_name: "Prewarm".to_string(), - sharing: None, - inner: RecordingMetaInner::Studio(Box::new(studio_meta.clone())), - upload: None, - }; - - let options = cap_rendering::RenderOptions { - screen_size: cap_project::XY::new(width, height), - camera_size: None, - preserve_screen_alpha: true, - }; - - let constants = RenderVideoConstants::from_shared_device( - shared, - options, - studio_meta, - recording_meta, - Arc::new(cap_rendering::BackgroundTextureCache::default()), - ); - - let config = std::sync::Arc::new(ProjectConfiguration::default()); - let mut frame_renderer = FrameRenderer::new(&constants); - let mut layers = RendererLayers::new_with_options( - &constants.device, - &constants.queue, - constants.is_software_adapter, - ); - - let segment_frames = DecodedSegmentFrames { - screen_size: cap_project::XY::new(width, height), - screen_frame: Some(DecodedFrame::new( - vec![255u8; (width * height * 4) as usize], - width, - height, - )), - camera_frame: None, - segment_time: 0.0, - recording_time: 0.0, - segment_has_camera: false, - }; - - let (base_w, base_h) = ProjectUniforms::get_base_size(&constants.options, &config); - let cursor_events = cap_project::CursorEvents::default(); - let mut zoom_timeline = ZoomTransformTimeline::new( - &[], - None, - &cursor_events, - config.screen_movement_spring, - 0.0, - None, - ); - zoom_timeline.ensure_precomputed_until(1.0 / 30.0); - let uniforms = ProjectUniforms::new( - &constants, - &config, - 0, - 30, - cap_project::XY::new(base_w, base_h), - &cursor_events, - &segment_frames, - 0.0, - &zoom_timeline, - ); - - match frame_renderer - .render_immediate( - segment_frames, - uniforms, - &cap_project::CursorEvents::default(), - true, - &mut layers, - ) - .await - { - Ok(_) => { - tracing::info!( - elapsed_ms = started.elapsed().as_millis() as u64, - "screenshot_editor timing: render pipeline prewarm complete" - ); - } - Err(e) => { - tracing::warn!("screenshot_editor render pipeline prewarm failed: {e}"); - } - } -} - -#[tauri::command] -#[specta::specta] -pub async fn prewarm_screenshot_background(path: String) -> Result<(), String> { - let Some(gpu) = gpu_context::get_shared_gpu().await else { - return Ok(()); - }; - - let Some(clean_path) = cap_rendering::clean_background_path(&path) else { - return Ok(()); - }; - - let _ = gpu - .background_cache - .ensure(&gpu.device, &gpu.queue, &clean_path) - .await; - - Ok(()) -} - -#[tauri::command] -#[specta::specta] -pub async fn update_screenshot_config( - instance: WindowScreenshotEditorInstance, - config: ProjectConfiguration, - save: bool, - revision: u32, -) -> Result<(), String> { - config.validate().map_err(|error| error.to_string())?; - - let _ = instance.config_tx.send(ScreenshotConfigUpdate { - revision, - config: config.clone(), - }); - - if !save { - return Ok(()); - } - - let Some(parent) = instance.path.parent() else { - return Ok(()); - }; - - if parent.extension().and_then(|s| s.to_str()) == Some("cap") { - let path = parent.to_path_buf(); - if let Err(e) = config.write(&path) { - eprintln!("Failed to save screenshot config: {e}"); - } else { - println!("Saved screenshot config to {path:?}"); - } - } else { - println!("Not saving config: parent {parent:?} is not a .cap directory"); - } - Ok(()) -} - -#[tauri::command] -#[specta::specta] -pub async fn recognize_screenshot_text( - instance: WindowScreenshotEditorInstance, - region: ScreenshotOcrRegion, -) -> Result { - let region = clamp_screenshot_ocr_region(region, instance.image_width, instance.image_height)?; - let image = create_screenshot_ocr_image( - instance.source_rgba.as_ref(), - instance.image_width, - instance.image_height, - region, - )?; - let mut result = recognize_screenshot_ocr_image(image).await?; - - for line in &mut result.lines { - line.bounds.x = line.bounds.x.saturating_add(region.x); - line.bounds.y = line.bounds.y.saturating_add(region.y); - } - - Ok(result) -} - -pub async fn recognize_text_from_image_path(path: &std::path::Path) -> Result { - let dynamic = image::open(path).map_err(|e| format!("Failed to open image for OCR: {e}"))?; - let rgba = dynamic.to_rgba8(); - let width = rgba.width(); - let height = rgba.height(); - - if width == 0 || height == 0 { - return Err("Image is empty".to_string()); - } - - let rgba_bytes = rgba.into_raw(); - let mut bgra = vec![0u8; rgba_bytes.len()]; - for (src, dst) in rgba_bytes.chunks_exact(4).zip(bgra.chunks_exact_mut(4)) { - dst[0] = src[2]; - dst[1] = src[1]; - dst[2] = src[0]; - dst[3] = src[3]; - } - - let image = ScreenshotOcrImage { - bgra, - width, - height, - }; - - let result = recognize_screenshot_ocr_image(image).await?; - Ok(result.text) -} - -fn clamp_screenshot_ocr_region( - region: ScreenshotOcrRegion, - image_width: u32, - image_height: u32, -) -> Result { - if image_width == 0 || image_height == 0 { - return Err("Screenshot image is empty".to_string()); - } - - let x = region.x.min(image_width.saturating_sub(1)); - let y = region.y.min(image_height.saturating_sub(1)); - let width = region.width.min(image_width.saturating_sub(x)); - let height = region.height.min(image_height.saturating_sub(y)); - - if width < 4 || height < 4 { - return Err("Select a larger text area".to_string()); - } - - Ok(ScreenshotOcrRegion { - x, - y, - width, - height, - }) -} - -fn create_screenshot_ocr_image( - source_rgba: &[u8], - image_width: u32, - image_height: u32, - region: ScreenshotOcrRegion, -) -> Result { - let image_width = usize::try_from(image_width) - .map_err(|_| "Screenshot width is too large for OCR".to_string())?; - let image_height = usize::try_from(image_height) - .map_err(|_| "Screenshot height is too large for OCR".to_string())?; - let region_x = - usize::try_from(region.x).map_err(|_| "OCR region x is too large".to_string())?; - let region_y = - usize::try_from(region.y).map_err(|_| "OCR region y is too large".to_string())?; - let region_width = - usize::try_from(region.width).map_err(|_| "OCR region width is too large".to_string())?; - let region_height = - usize::try_from(region.height).map_err(|_| "OCR region height is too large".to_string())?; - - let expected_len = image_width - .checked_mul(image_height) - .and_then(|pixels| pixels.checked_mul(4)) - .ok_or_else(|| "Screenshot image is too large for OCR".to_string())?; - - if source_rgba.len() != expected_len { - return Err("Screenshot image data is invalid for OCR".to_string()); - } - - let output_len = region_width - .checked_mul(region_height) - .and_then(|pixels| pixels.checked_mul(4)) - .ok_or_else(|| "OCR region is too large".to_string())?; - let mut bgra = vec![0; output_len]; - let source_row_bytes = image_width - .checked_mul(4) - .ok_or_else(|| "Screenshot row is too large for OCR".to_string())?; - let region_row_bytes = region_width - .checked_mul(4) - .ok_or_else(|| "OCR row is too large".to_string())?; - let region_x_bytes = region_x - .checked_mul(4) - .ok_or_else(|| "OCR region x is too large".to_string())?; - - for row in 0..region_height { - let source_start = region_y - .checked_add(row) - .and_then(|source_row| source_row.checked_mul(source_row_bytes)) - .and_then(|source_offset| source_offset.checked_add(region_x_bytes)) - .ok_or_else(|| "OCR source region is invalid".to_string())?; - let source_end = source_start - .checked_add(region_row_bytes) - .ok_or_else(|| "OCR source region is invalid".to_string())?; - let output_start = row - .checked_mul(region_row_bytes) - .ok_or_else(|| "OCR output region is invalid".to_string())?; - let output_end = output_start - .checked_add(region_row_bytes) - .ok_or_else(|| "OCR output region is invalid".to_string())?; - let source_row = source_rgba - .get(source_start..source_end) - .ok_or_else(|| "OCR source region is outside the screenshot".to_string())?; - let output_row = bgra - .get_mut(output_start..output_end) - .ok_or_else(|| "OCR output region is invalid".to_string())?; - - for (source_pixel, output_pixel) in source_row - .chunks_exact(4) - .zip(output_row.chunks_exact_mut(4)) - { - output_pixel[0] = source_pixel[2]; - output_pixel[1] = source_pixel[1]; - output_pixel[2] = source_pixel[0]; - output_pixel[3] = source_pixel[3]; - } - } - - Ok(ScreenshotOcrImage { - bgra, - width: region.width, - height: region.height, - }) -} - -#[cfg(target_os = "macos")] -async fn recognize_screenshot_ocr_image( - image: ScreenshotOcrImage, -) -> Result { - tokio::task::spawn_blocking(move || recognize_screenshot_ocr_image_macos(image)) - .await - .map_err(|e| format!("OCR task failed: {e}"))? -} - -#[cfg(target_os = "windows")] -async fn recognize_screenshot_ocr_image( - image: ScreenshotOcrImage, -) -> Result { - tokio::task::spawn_blocking(move || recognize_screenshot_ocr_image_windows(image)) - .await - .map_err(|e| format!("OCR task failed: {e}"))? -} - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -async fn recognize_screenshot_ocr_image( - ScreenshotOcrImage { - bgra: _bgra, - width: _width, - height: _height, - }: ScreenshotOcrImage, -) -> Result { - Err("OCR is only available on macOS and Windows".to_string()) -} - -#[cfg(target_os = "macos")] -fn recognize_screenshot_ocr_image_macos( - image: ScreenshotOcrImage, -) -> Result { - cidre::objc::ar_pool(|| { - use cidre::{cv, ns, vn}; - use std::ffi::c_void; - - extern "C" fn release_pixel_buffer_data( - release_ref_con: *mut c_void, - _base_address: *const *const c_void, - ) { - if !release_ref_con.is_null() { - unsafe { - drop(Box::from_raw(release_ref_con.cast::>())); - } - } - } - - let width = - usize::try_from(image.width).map_err(|_| "OCR image width is too large".to_string())?; - let height = usize::try_from(image.height) - .map_err(|_| "OCR image height is too large".to_string())?; - let bytes_per_row = width - .checked_mul(4) - .ok_or_else(|| "OCR image row is too large".to_string())?; - let mut data = Box::new(image.bgra); - let base_address = data.as_mut_ptr().cast::(); - let release_ref_con = Box::into_raw(data).cast::(); - - let pixel_buffer = match cv::PixelBuf::with_bytes( - width, - height, - base_address, - bytes_per_row, - release_pixel_buffer_data, - release_ref_con, - cv::PixelFormat::_32_BGRA, - None, - ) { - Ok(pixel_buffer) => pixel_buffer, - Err(e) => { - unsafe { - drop(Box::from_raw(release_ref_con.cast::>())); - } - return Err(format!("Failed to create OCR image: {e}")); - } - }; - - let mut request = vn::RecognizeTextRequest::new(); - request.set_recognition_level(vn::RequestTextRecognitionLevel::Accurate); - request.set_uses_lang_correction(true); - - if cidre::version!(macos = 13.0) { - request.set_revision(vn::RecognizeTextRequest::REVISION_3); - unsafe { - request.set_automatically_detects_lang(true); - } - } else { - request.set_revision(vn::RecognizeTextRequest::REVISION_2); - } - - let handler = vn::ImageRequestHandler::with_cv_pixel_buf(&pixel_buffer, None) - .ok_or_else(|| "Failed to initialize OCR image handler".to_string())?; - let requests = ns::Array::::from_slice(&[&request]); - handler - .perform(&requests) - .map_err(|e| format!("macOS OCR failed: {e}"))?; - - let observations = request.results().unwrap_or_else(ns::Array::new); - let mut lines = Vec::new(); - - for observation in observations.iter() { - let candidates = observation.top_candidates(1); - let Some(candidate) = candidates.first() else { - continue; - }; - let text = candidate.string().to_string(); - if text.trim().is_empty() { - continue; - } - lines.push(ScreenshotOcrLine { - text, - confidence: Some(candidate.confidence()), - bounds: normalized_macos_ocr_rect_to_region( - observation.bounding_box(), - image.width, - image.height, - ), - }); - } - - let text = lines - .iter() - .map(|line| line.text.as_str()) - .collect::>() - .join("\n"); - - Ok(ScreenshotOcrResult { - text, - lines, - engine: "macos-vision".to_string(), - }) - }) -} - -#[cfg(target_os = "macos")] -fn normalized_macos_ocr_rect_to_region( - rect: cidre::cg::Rect, - width: u32, - height: u32, -) -> ScreenshotOcrRegion { - let width_f = f64::from(width); - let height_f = f64::from(height); - let left = clamp_f64(rect.origin.x * width_f, 0.0, width_f); - let right = clamp_f64((rect.origin.x + rect.size.width) * width_f, 0.0, width_f); - let top = clamp_f64( - (1.0 - rect.origin.y - rect.size.height) * height_f, - 0.0, - height_f, - ); - let bottom = clamp_f64((1.0 - rect.origin.y) * height_f, 0.0, height_f); - let x = left.round() as u32; - let y = top.round() as u32; - let right = right.round() as u32; - let bottom = bottom.round() as u32; - - ScreenshotOcrRegion { - x, - y, - width: right.saturating_sub(x), - height: bottom.saturating_sub(y), - } -} - -#[cfg(target_os = "macos")] -fn clamp_f64(value: f64, min: f64, max: f64) -> f64 { - if value.is_finite() { - value.clamp(min, max) - } else { - min - } -} - -#[cfg(target_os = "windows")] -struct WindowsRuntimeGuard; - -#[cfg(target_os = "windows")] -impl Drop for WindowsRuntimeGuard { - fn drop(&mut self) { - unsafe { - windows::Win32::System::WinRT::RoUninitialize(); - } - } -} - -#[cfg(target_os = "windows")] -fn initialize_windows_runtime() -> Result { - use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; - - unsafe { RoInitialize(RO_INIT_MULTITHREADED) } - .map_err(|e| format!("Windows OCR runtime failed: {e}"))?; - - Ok(WindowsRuntimeGuard) -} - -#[cfg(target_os = "windows")] -fn recognize_screenshot_ocr_image_windows( - image: ScreenshotOcrImage, -) -> Result { - use windows::Graphics::Imaging::{BitmapAlphaMode, BitmapPixelFormat, SoftwareBitmap}; - use windows::Media::Ocr::OcrEngine; - use windows::Storage::Streams::DataWriter; - - let _runtime = initialize_windows_runtime()?; - - let max_dimension = - OcrEngine::MaxImageDimension().map_err(|e| format!("Windows OCR failed: {e}"))?; - - if image.width > max_dimension || image.height > max_dimension { - return Err(format!( - "Select a smaller text area. Windows OCR supports up to {max_dimension}px per side" - )); - } - - let width = i32::try_from(image.width).map_err(|_| "OCR image width is too large")?; - let height = i32::try_from(image.height).map_err(|_| "OCR image height is too large")?; - let writer = DataWriter::new().map_err(|e| format!("Windows OCR failed: {e}"))?; - writer - .WriteBytes(&image.bgra) - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let buffer = writer - .DetachBuffer() - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let bitmap = SoftwareBitmap::CreateCopyWithAlphaFromBuffer( - &buffer, - BitmapPixelFormat::Bgra8, - width, - height, - BitmapAlphaMode::Premultiplied, - ) - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let engine = OcrEngine::TryCreateFromUserProfileLanguages() - .map_err(|e| format!("Windows OCR is not available: {e}"))?; - let result = engine - .RecognizeAsync(&bitmap) - .map_err(|e| format!("Windows OCR failed: {e}"))? - .get() - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let text = result - .Text() - .map_err(|e| format!("Windows OCR failed: {e}"))? - .to_string_lossy(); - let ocr_lines = result - .Lines() - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let mut lines = Vec::new(); - - for index in 0..ocr_lines - .Size() - .map_err(|e| format!("Windows OCR failed: {e}"))? - { - let line = ocr_lines - .GetAt(index) - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let line_text = line - .Text() - .map_err(|e| format!("Windows OCR failed: {e}"))? - .to_string_lossy(); - if line_text.trim().is_empty() { - continue; - } - let words = line - .Words() - .map_err(|e| format!("Windows OCR failed: {e}"))?; - let mut bounds: Option<(f32, f32, f32, f32)> = None; - - for word_index in 0..words - .Size() - .map_err(|e| format!("Windows OCR failed: {e}"))? - { - let rect = words - .GetAt(word_index) - .and_then(|word| word.BoundingRect()) - .map_err(|e| format!("Windows OCR failed: {e}"))?; - bounds = Some(match bounds { - Some((left, top, right, bottom)) => ( - left.min(rect.X), - top.min(rect.Y), - right.max(rect.X + rect.Width), - bottom.max(rect.Y + rect.Height), - ), - None => (rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height), - }); - } - - lines.push(ScreenshotOcrLine { - text: line_text, - confidence: None, - bounds: bounds - .map(windows_ocr_bounds_to_region) - .unwrap_or(ScreenshotOcrRegion { - x: 0, - y: 0, - width: 0, - height: 0, - }), - }); - } - - Ok(ScreenshotOcrResult { - text, - lines, - engine: "windows-media-ocr".to_string(), - }) -} - -#[cfg(target_os = "windows")] -fn windows_ocr_bounds_to_region( - (left, top, right, bottom): (f32, f32, f32, f32), -) -> ScreenshotOcrRegion { - let x = clamp_f32_to_u32(left); - let y = clamp_f32_to_u32(top); - let right = clamp_f32_to_u32(right); - let bottom = clamp_f32_to_u32(bottom); - - ScreenshotOcrRegion { - x, - y, - width: right.saturating_sub(x), - height: bottom.saturating_sub(y), - } -} - -#[cfg(target_os = "windows")] -fn clamp_f32_to_u32(value: f32) -> u32 { - if value.is_finite() && value > 0.0 { - value.round().min(u32::MAX as f32) as u32 - } else { - 0 - } -} - -#[tauri::command] -#[specta::specta] -pub async fn render_screenshot_for_export( - instance: WindowScreenshotEditorInstance, -) -> Result, String> { - render_screenshot_png(&instance).await -} - -#[tauri::command] -#[specta::specta] -pub async fn render_screenshot_project_for_export( - app: AppHandle, - path: PathBuf, -) -> Result { - let instance = ScreenshotEditorInstances::create_standalone_instance(&app, path, false).await?; - let config = instance.config_tx.borrow().config.clone(); - let image_width = instance.image_width; - let image_height = instance.image_height; - - let result = - render_screenshot_png(&instance) - .await - .map(|image_bytes| ScreenshotProjectExport { - image_bytes, - config, - image_width, - image_height, - }); - instance.dispose().await; - result -} - -pub async fn render_screenshot_png(instance: &ScreenshotEditorInstance) -> Result, String> { - let path = instance.path.clone(); - let config = std::sync::Arc::new(instance.config_tx.borrow().config.clone()); - let width = instance.image_width; - let height = instance.image_height; - - if width > MAX_DIMENSION || height > MAX_DIMENSION { - return Err(format!("Image dimensions exceed maximum: {width}x{height}")); - } - - let data = instance.source_rgba.as_ref().clone(); - - let cap_dir = if path.extension().and_then(|s| s.to_str()) == Some("cap") { - Some(path.clone()) - } else if let Some(parent) = path.parent() { - if parent.extension().and_then(|s| s.to_str()) == Some("cap") { - Some(parent.to_path_buf()) - } else { - None - } - } else { - None - }; - - let recording_meta = if let Some(cap_dir) = &cap_dir { - RecordingMeta::load_for_project(cap_dir).map_err(|e| e.to_string())? - } else { - let filename = path - .file_name() - .ok_or_else(|| "Invalid path".to_string())? - .to_string_lossy(); - let relative_path = RelativePathBuf::from(filename.as_ref()); - let video_meta = VideoMeta { - path: relative_path.clone(), - fps: 30, - start_time: Some(0.0), - device_id: None, - }; - let segment = SingleSegment { - display: video_meta.clone(), - camera: None, - audio: None, - cursor: None, - }; - let studio_meta = StudioRecordingMeta::SingleSegment { segment }; - RecordingMeta { - platform: None, - project_path: path.parent().unwrap_or(&path).to_path_buf(), - pretty_name: "Screenshot".to_string(), - sharing: None, - inner: RecordingMetaInner::Studio(Box::new(studio_meta)), - upload: None, - } - }; - - let (shared, background_cache) = if let Some(gpu) = gpu_context::get_shared_gpu().await { - ( - cap_rendering::SharedWgpuDevice { - instance: (*gpu.instance).clone(), - adapter: (*gpu.adapter).clone(), - device: (*gpu.device).clone(), - queue: (*gpu.queue).clone(), - is_software_adapter: gpu.is_software_adapter, - }, - gpu.background_cache.clone(), - ) - } else { - let instance = cap_rendering::create_wgpu_instance().await; - let force_software_adapter = cap_rendering::force_software_wgpu_adapter(); - let hardware_adapter = if force_software_adapter { - None - } else { - instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - force_fallback_adapter: false, - compatible_surface: None, - }) - .await - .ok() - }; - let adapter = match hardware_adapter { - Some(adapter) => adapter, - None => instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - force_fallback_adapter: true, - compatible_surface: None, - }) - .await - .map_err(|_| "No GPU adapter found".to_string())?, - }; - let adapter_info = adapter.get_info(); - let is_software_adapter = cap_rendering::is_software_wgpu_adapter(&adapter_info); - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("cap-rendering-device"), - required_features: wgpu::Features::empty(), - ..Default::default() - }) - .await - .map_err(|e| e.to_string())?; - ( - cap_rendering::SharedWgpuDevice { - instance, - adapter, - device, - queue, - is_software_adapter, - }, - Arc::new(cap_rendering::BackgroundTextureCache::default()), - ) - }; - - let options = cap_rendering::RenderOptions { - screen_size: cap_project::XY::new(width, height), - camera_size: None, - preserve_screen_alpha: true, - }; - - let studio_meta = match &recording_meta.inner { - RecordingMetaInner::Studio(meta) => meta.clone(), - _ => return Err("Invalid recording meta for screenshot".to_string()), - }; - - let constants = RenderVideoConstants::from_shared_device( - shared, - options, - *studio_meta, - recording_meta.clone(), - background_cache, - ); - - let (base_width, base_height) = ProjectUniforms::get_base_size(&constants.options, &config); - let display_size = ProjectUniforms::display_size( - &constants.options, - &config, - cap_project::XY::new(base_width, base_height), - ) - .coord; - let crop = ProjectUniforms::get_crop(&constants.options, &config); - let export_scale = f64::max( - f64::max( - crop.size.x as f64 / f64::max(display_size.x, 1.0), - crop.size.y as f64 / f64::max(display_size.y, 1.0), - ), - 1.0, - ); - - let resolution_base = cap_project::XY::new( - (((base_width as f64 * export_scale).ceil() as u32) + 3) & !3, - (((base_height as f64 * export_scale).ceil() as u32) + 1) & !1, - ); - - if resolution_base.x > MAX_DIMENSION || resolution_base.y > MAX_DIMENSION { - return Err(format!( - "Export dimensions exceed maximum: {}x{}", - resolution_base.x, resolution_base.y - )); - } - - let mut frame_renderer = FrameRenderer::new(&constants); - let mut layers = RendererLayers::new_with_options( - &constants.device, - &constants.queue, - constants.is_software_adapter, - ); - let decoded_frame = DecodedFrame::new(data, width, height); - let segment_frames = DecodedSegmentFrames { - screen_size: cap_project::XY::new(width, height), - screen_frame: Some(DecodedFrame::new( - decoded_frame.data().to_vec(), - decoded_frame.width(), - decoded_frame.height(), - )), - camera_frame: None, - segment_time: 0.0, - recording_time: 0.0, - segment_has_camera: false, - }; - let cursor_events = cap_project::CursorEvents::default(); - let mut zoom_timeline = ZoomTransformTimeline::from_project( - &config, - &cursor_events, - 0.0, - constants.options.screen_size, - ); - zoom_timeline.ensure_precomputed_until(1.0 / 30.0); - let uniforms = ProjectUniforms::new( - &constants, - &config, - 0, - 30, - resolution_base, - &cursor_events, - &segment_frames, - 0.0, - &zoom_timeline, - ); - let rendered_frame = frame_renderer - .render_immediate( - segment_frames, - uniforms, - &cap_project::CursorEvents::default(), - true, - &mut layers, - ) - .await - .map_err(|e| format!("Failed to render screenshot export: {e}"))?; - - let width_usize = - usize::try_from(rendered_frame.width).map_err(|_| "Invalid export width".to_string())?; - let height_usize = - usize::try_from(rendered_frame.height).map_err(|_| "Invalid export height".to_string())?; - let unpadded_bytes_per_row = width_usize - .checked_mul(4) - .ok_or_else(|| "Export row size overflow".to_string())?; - let padded_bytes_per_row = usize::try_from(rendered_frame.padded_bytes_per_row) - .map_err(|_| "Invalid export stride".to_string())?; - - if padded_bytes_per_row < unpadded_bytes_per_row { - return Err(format!( - "Invalid export stride: {} for {}x{} image", - rendered_frame.padded_bytes_per_row, rendered_frame.width, rendered_frame.height - )); - } - - let expected_padded_len = padded_bytes_per_row - .checked_mul(height_usize) - .ok_or_else(|| "Export buffer size overflow".to_string())?; - if rendered_frame.data.len() < expected_padded_len { - return Err(format!( - "Invalid export buffer length: expected at least {} got {} for {}x{} image", - expected_padded_len, - rendered_frame.data.len(), - rendered_frame.width, - rendered_frame.height - )); - } - - let rgba_data: Vec = rendered_frame - .data - .chunks(padded_bytes_per_row) - .take(height_usize) - .flat_map(|row| row[..unpadded_bytes_per_row].iter().copied()) - .collect(); - - let mut png_data = Cursor::new(Vec::new()); - let encoder = PngEncoder::new(&mut png_data); - encoder - .write_image( - &rgba_data, - rendered_frame.width, - rendered_frame.height, - image::ExtendedColorType::Rgba8, - ) - .map_err(|e| format!("Failed to encode screenshot export: {e}"))?; - - Ok(png_data.into_inner()) -} diff --git a/apps/desktop/src-tauri/src/startup.rs b/apps/desktop/src-tauri/src/startup.rs deleted file mode 100644 index 36347e3b659..00000000000 --- a/apps/desktop/src-tauri/src/startup.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{ - path::Path, - time::{Duration, Instant, SystemTime}, -}; -use tauri::{AppHandle, Manager}; -use tokio::sync::watch; - -pub(crate) struct Startup { - started: Instant, - started_at: SystemTime, - ready: watch::Sender, -} - -impl Default for Startup { - fn default() -> Self { - Self { - started: Instant::now(), - started_at: SystemTime::now(), - ready: watch::channel(false).0, - } - } -} - -impl Startup { - pub(crate) fn predates_launch(&self, path: &Path) -> bool { - let created = path.metadata().and_then(|metadata| metadata.created()).ok(); - let modified = path - .join("recording-meta.json") - .metadata() - .and_then(|metadata| metadata.modified()) - .ok(); - created.is_none_or(|created| created <= self.started_at) - && modified.is_none_or(|modified| modified <= self.started_at) - } - - pub(crate) fn mark_ready(&self) { - if !self.ready.send_replace(true) { - tracing::info!( - elapsed_ms = self.started.elapsed().as_millis(), - "Startup frontend ready" - ); - } - } - - async fn wait(&self) { - let mut ready = self.ready.subscribe(); - let remaining = Duration::from_secs(5).saturating_sub(self.started.elapsed()); - let _ = tokio::time::timeout(remaining, ready.wait_for(|ready| *ready)).await; - } -} - -pub(crate) async fn wait_for_window(app: &AppHandle) -> bool { - app.state::().wait().await; - !crate::app_is_exiting(app) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn delayed_reconciliation_does_not_mark_this_sessions_recordings_crashed() { - let project = tempfile::tempdir().unwrap(); - let path = project.path().join("recording-meta.json"); - std::fs::write(&path, b"{}").unwrap(); - let metadata_time = path.metadata().unwrap().modified().unwrap(); - let before_recording = Startup { - started_at: metadata_time.checked_sub(Duration::from_secs(1)).unwrap(), - ..Startup::default() - }; - assert!(!before_recording.predates_launch(project.path())); - let after_recording = Startup { - started_at: SystemTime::now() + Duration::from_secs(1), - ..Startup::default() - }; - assert!(after_recording.predates_launch(project.path())); - } - - #[tokio::test] - async fn readiness_releases_both_existing_and_late_waiters() { - let startup = Startup::default(); - assert!( - tokio::time::timeout(Duration::from_millis(10), startup.wait()) - .await - .is_err() - ); - startup.mark_ready(); - startup.mark_ready(); - tokio::time::timeout(Duration::from_millis(100), startup.wait()) - .await - .unwrap(); - } - - #[tokio::test] - async fn a_failed_frontend_does_not_disable_recovery() { - let startup = Startup { - started: Instant::now().checked_sub(Duration::from_secs(6)).unwrap(), - ..Startup::default() - }; - tokio::time::timeout(Duration::from_millis(100), startup.wait()) - .await - .unwrap(); - } -} diff --git a/apps/desktop/src-tauri/src/stop-editor-benchmark.js b/apps/desktop/src-tauri/src/stop-editor-benchmark.js deleted file mode 100644 index 7f991f5f853..00000000000 --- a/apps/desktop/src-tauri/src/stop-editor-benchmark.js +++ /dev/null @@ -1,30 +0,0 @@ -(() => { - let reported = false; - const report = () => { - if (reported) return; - reported = true; - requestAnimationFrame(() => { - requestAnimationFrame(() => { - void window.__TAURI_INTERNALS__.invoke("plugin:event|emit", { - event: "cap-stop-benchmark-frame", - payload: {}, - }); - }); - }); - }; - const deadline = performance.now() + 180_000; - const observe = () => { - const canvas = document.getElementById("canvas"); - const stats = window.__capFpsStats?.(); - if ( - stats?.renderCount > 0 && - canvas?.width > 0 && - getComputedStyle(canvas).visibility === "visible" - ) { - report(); - } else if (performance.now() < deadline) { - requestAnimationFrame(observe); - } - }; - requestAnimationFrame(observe); -})(); diff --git a/apps/desktop/src-tauri/src/stop-editor-parity-dom.js b/apps/desktop/src-tauri/src/stop-editor-parity-dom.js deleted file mode 100644 index 3900ad50144..00000000000 --- a/apps/desktop/src-tauri/src/stop-editor-parity-dom.js +++ /dev/null @@ -1,214 +0,0 @@ -(() => { - const ids = new WeakMap(); - let nextId = 1; - let sequence = 0; - let lastState = ""; - let previousPreparingIds = []; - let firstOrdinaryVisibleAt; - const started = performance.now(); - const deadline = started + 180_000; - const idFor = (element) => { - if (!ids.has(element)) ids.set(element, nextId++); - return ids.get(element); - }; - const describe = (canvas) => { - const rect = canvas.getBoundingClientRect(); - let ancestorsVisible = true; - for (let element = canvas; element; element = element.parentElement) { - const style = getComputedStyle(element); - if ( - style.display === "none" || - style.visibility !== "visible" || - Number(style.opacity) === 0 - ) { - ancestorsVisible = false; - break; - } - } - return { - id: idFor(canvas), - connected: canvas.isConnected, - width: canvas.width, - height: canvas.height, - bounds: { - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height, - }, - visible: - ancestorsVisible && - canvas.width > 0 && - canvas.height > 0 && - rect.width > 0 && - rect.height > 0 && - rect.bottom > 0 && - rect.right > 0 && - rect.top < innerHeight && - rect.left < innerWidth, - }; - }; - const geometry = (element) => { - if (!element) return null; - const rect = element.getBoundingClientRect(); - const style = getComputedStyle(element); - return { - id: idFor(element), - connected: element.isConnected, - bounds: { - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height, - }, - inline: { - width: element.style.width, - height: element.style.height, - minHeight: element.style.minHeight, - }, - computed: { - width: style.width, - height: style.height, - minHeight: style.minHeight, - maxHeight: style.maxHeight, - display: style.display, - visibility: style.visibility, - flex: style.flex, - flexDirection: style.flexDirection, - gap: style.gap, - paddingTop: style.paddingTop, - paddingBottom: style.paddingBottom, - }, - }; - }; - const ordinaryLayout = (canvas) => { - if (!canvas) return null; - const ancestors = []; - for ( - let element = canvas.parentElement; - element && ancestors.length < 10; - element = element.parentElement - ) { - ancestors.push(element); - } - const [wrapper, visibilityLayer, container, player, card, row, layout] = - ancestors; - const timelineSeparator = document.querySelector( - '[role="separator"][aria-label="Resize timeline height"]', - ); - const timeline = timelineSeparator?.parentElement; - const sourceStructureMatches = Boolean( - container?.parentElement === player && - player?.children.length === 3 && - player?.children[1] === container && - timeline?.parentElement === layout && - row?.nextElementSibling === timeline, - ); - return { - sourceStructureMatches, - canvas: geometry(canvas), - ancestors: ancestors.map(geometry), - roles: sourceStructureMatches - ? { - wrapper: idFor(wrapper), - visibilityLayer: idFor(visibilityLayer), - container: idFor(container), - player: idFor(player), - card: idFor(card), - row: idFor(row), - layout: idFor(layout), - } - : null, - topToolbar: sourceStructureMatches - ? geometry(container.previousElementSibling) - : null, - bottomToolbar: sourceStructureMatches - ? geometry(container.nextElementSibling) - : null, - timeline: geometry(timeline), - }; - }; - const emit = (value) => { - void window.__TAURI_INTERNALS__ - .invoke("plugin:event|emit", { - event: "cap-stop-benchmark-dom", - payload: value, - }) - .catch(() => {}); - }; - const sample = () => { - const now = performance.now(); - const preparing = Array.from( - document.querySelectorAll( - 'canvas[aria-label="Recording preview while the editor prepares"]', - ), - ).map((canvas) => { - const siblings = Array.from(canvas.parentElement.children).filter( - (element) => element.tagName === "CANVAS", - ); - return { - ...describe(canvas), - role: siblings.indexOf(canvas) === 1 ? "retained" : "live", - }; - }); - const ordinaryCanvas = document.getElementById("canvas"); - const ordinary = ordinaryCanvas ? describe(ordinaryCanvas) : null; - const stats = window.__capFpsStats?.(); - const ordinaryRenderedVisible = Boolean( - ordinary?.visible && stats?.renderCount > 0, - ); - if (ordinaryRenderedVisible && firstOrdinaryVisibleAt === undefined) { - firstOrdinaryVisibleAt = now; - } - const state = { - preparing, - ordinary, - ordinaryLayout: ordinaryLayout(ordinaryCanvas), - ordinaryRenderedVisible, - documentVisibility: document.visibilityState, - viewport: { - width: innerWidth, - height: innerHeight, - dpr: devicePixelRatio, - }, - }; - const serialized = JSON.stringify(state); - if (serialized !== lastState) { - const currentIds = preparing.map((canvas) => canvas.id); - emit({ - sequence: sequence++, - timeOriginMs: performance.timeOrigin, - elapsedMs: now - started, - observation: "canvas_state", - preparingSubtreesDetached: previousPreparingIds.filter( - (id) => !currentIds.includes(id), - ), - state, - }); - previousPreparingIds = currentIds; - lastState = serialized; - } - if ( - now < deadline && - sequence < 512 && - (firstOrdinaryVisibleAt === undefined || - now - firstOrdinaryVisibleAt < 1000) - ) { - requestAnimationFrame(sample); - } else { - emit({ - sequence: sequence++, - timeOriginMs: performance.timeOrigin, - elapsedMs: now - started, - observation: "complete", - reason: - firstOrdinaryVisibleAt !== undefined - ? "ordinary_visible_settled" - : now >= deadline - ? "deadline" - : "observation_limit", - }); - } - }; - requestAnimationFrame(sample); -})(); diff --git a/apps/desktop/src-tauri/src/stop_editor_benchmark.rs b/apps/desktop/src-tauri/src/stop_editor_benchmark.rs deleted file mode 100644 index 43f3ea1c831..00000000000 --- a/apps/desktop/src-tauri/src/stop_editor_benchmark.rs +++ /dev/null @@ -1,822 +0,0 @@ -use std::{ - ffi::OsString, - path::{Path, PathBuf}, - sync::{ - Arc, Mutex, OnceLock, - atomic::{AtomicBool, Ordering}, - }, - time::{Duration, Instant}, -}; - -use tauri::{Listener, Manager}; -use tokio::sync::RwLock; - -pub const SCRIPT: &str = include_str!("stop-editor-benchmark.js"); -pub const DOM_SCRIPT: &str = include_str!("stop-editor-parity-dom.js"); -static FRAME_CAPTURE: OnceLock> = OnceLock::new(); -static STARTED: AtomicBool = AtomicBool::new(false); -static CONFIG: OnceLock, String>> = OnceLock::new(); -static APP_ADMITTED: AtomicBool = AtomicBool::new(false); - -struct BenchmarkConfig { - output: PathBuf, - log_directory: PathBuf, - seconds: u64, - microphone: Option, - system_audio: bool, - fixture_window: bool, - frame_directory: Option, - dom_capture: bool, -} - -fn parse_config( - arguments: impl IntoIterator, - mut environment: impl FnMut(&'static str) -> Option, -) -> Result, String> { - let arguments: Vec<_> = arguments.into_iter().collect(); - let benchmark = arguments - .iter() - .filter(|arg| *arg == "--stop-editor-benchmark") - .count(); - let recording = arguments - .iter() - .filter(|arg| *arg == "--allow-real-recording") - .count(); - if benchmark == 0 && recording == 0 { - return Ok(None); - } - if arguments.len() != 2 || benchmark != 1 || recording != 1 { - return Err( - "Benchmark invocation requires exactly --stop-editor-benchmark --allow-real-recording" - .into(), - ); - } - let output = environment("CAP_STOP_EDITOR_BENCHMARK_OUTPUT") - .filter(|value| !value.is_empty()) - .ok_or("Benchmark invocation requires CAP_STOP_EDITOR_BENCHMARK_OUTPUT")?; - let log_directory = environment("CAP_STOP_BENCH_LOG_DIR") - .filter(|value| !value.is_empty()) - .ok_or("Benchmark invocation requires CAP_STOP_BENCH_LOG_DIR")?; - let seconds = environment("CAP_STOP_BENCH_SECONDS") - .map(|value| { - value - .into_string() - .map_err(|_| "Benchmark duration must be valid UTF-8".to_string())? - .parse::() - .map_err(|_| "Benchmark duration must be a whole number of seconds".to_string()) - }) - .transpose()? - .unwrap_or(15); - if !(1..=86_400).contains(&seconds) { - return Err("Benchmark duration must be between 1 and 86400 seconds".into()); - } - let microphone = environment("CAP_STOP_BENCH_MIC") - .map(|value| { - value - .into_string() - .map_err(|_| "Benchmark microphone must be valid UTF-8".to_string()) - .and_then(|value| { - if value.is_empty() { - Err("Benchmark microphone must not be empty".into()) - } else { - Ok(value) - } - }) - }) - .transpose()?; - Ok(Some(BenchmarkConfig { - output: PathBuf::from(output), - log_directory: PathBuf::from(log_directory), - seconds, - microphone, - system_audio: environment("CAP_STOP_BENCH_SYSTEM_AUDIO").is_some(), - fixture_window: environment("CAP_STOP_BENCH_FIXTURE_WINDOW").is_some(), - frame_directory: environment("CAP_STOP_BENCH_FRAME_DIR").map(PathBuf::from), - dom_capture: environment("CAP_STOP_BENCH_DOM_CAPTURE").is_some(), - })) -} - -fn validate_frame_directory(config: &BenchmarkConfig) -> Result<(), String> { - let Some(directory) = config.frame_directory.as_ref() else { - return Ok(()); - }; - if !directory.is_absolute() - || directory == &config.output - || directory == &config.log_directory - || directory.components().any(|component| { - !matches!( - component, - std::path::Component::Prefix(_) - | std::path::Component::RootDir - | std::path::Component::Normal(_) - ) - }) - { - return Err( - "Benchmark frame directory must be a distinct absolute path without traversal".into(), - ); - } - let parent = directory - .parent() - .ok_or("Benchmark frame directory has no parent")?; - if parent.canonicalize().map_err(|error| error.to_string())? != parent || !parent.is_dir() { - return Err("Benchmark frame directory requires an existing canonical parent".into()); - } - match std::fs::symlink_metadata(directory) { - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error.to_string()), - Ok(_) => Err("Benchmark frame directory must be new".into()), - } -} - -fn prepare_config( - config: Option, - create_log_directory: impl FnOnce(&Path, &Path) -> std::io::Result, -) -> Result, String> { - if let Some(config) = config.as_ref() { - validate_frame_directory(config)?; - create_log_directory(&config.output, &config.log_directory) - .map_err(|error| error.to_string())?; - } - Ok(config) -} - -pub fn initialize( - create_log_directory: impl FnOnce(&Path, &Path) -> std::io::Result, -) -> Result, String> { - match CONFIG.get_or_init(|| { - let config = parse_config(std::env::args_os().skip(1), std::env::var_os)?; - prepare_config(config, create_log_directory) - }) { - Ok(config) => Ok(config.as_ref().map(|config| config.log_directory.clone())), - Err(error) => Err(error.clone()), - } -} - -fn config() -> Option<&'static BenchmarkConfig> { - CONFIG.get()?.as_ref().ok()?.as_ref() -} - -fn admit_identifier(requested: bool, identifier: &str) -> Result { - if !requested { - return Ok(false); - } - if identifier - .strip_prefix("so.cap.desktop.stop-editor-benchmark.") - .is_some_and(|suffix| !suffix.is_empty()) - { - Ok(true) - } else { - Err("Benchmark invocation requires a private benchmark app identifier".into()) - } -} - -pub fn validate_app_identifier(identifier: &str) -> Result<(), String> { - let admitted = admit_identifier(config().is_some(), identifier)?; - APP_ADMITTED.store(admitted, Ordering::SeqCst); - Ok(()) -} - -pub fn enabled() -> bool { - APP_ADMITTED.load(Ordering::SeqCst) && config().is_some() -} - -pub fn run(app: tauri::AppHandle) { - if !enabled() { - return; - } - let Some(config) = config() else { - return; - }; - if STARTED.swap(true, Ordering::SeqCst) { - return; - } - tokio::spawn(async move { - let result = measure(&app, config).await; - let mut value = match result { - Ok(value) => value, - Err(error) => serde_json::json!({ "error": error }), - }; - match finish_frame_capture() { - Ok(Some(capture)) => value["frameCapture"] = capture, - Ok(None) => {} - Err(error) => { - value["frameCapture"] = - serde_json::json!({ "status": "incomplete", "error": error }) - } - } - if let Err(error) = std::fs::write(&config.output, value.to_string()) { - tracing::error!(%error, "Could not write Stop benchmark results"); - } - }); -} - -async fn measure( - app: &tauri::AppHandle, - config: &BenchmarkConfig, -) -> Result { - tokio::time::sleep(Duration::from_secs(3)).await; - let state = app.state::>>(); - crate::set_mic_input(app.clone(), state.clone(), config.microphone.clone()).await?; - let capture_target = benchmark_capture_target(config.fixture_window)?; - let action = crate::recording::start_recording( - app.clone(), - state.clone(), - crate::recording::StartRecordingInputs { - capture_target, - capture_system_audio: config.system_audio, - mode: cap_recording::RecordingMode::Studio, - organization_id: None, - }, - ) - .await?; - if !matches!(action, crate::recording::RecordingAction::Started) { - return Err("Benchmark recording did not start".into()); - } - let seconds = config.seconds; - tokio::time::sleep(Duration::from_secs(seconds)).await; - let project = state - .read() - .await - .current_recording() - .map(|recording| recording.recording_dir().clone()); - if let Err(error) = arm_frame_capture(app, project.as_deref()) { - let cleanup = crate::recording::stop_recording(app.clone(), state.clone()).await; - return Err(match cleanup { - Ok(()) => error, - Err(cleanup_error) => format!("{error}; recording stop also failed: {cleanup_error}"), - }); - } - let dom_capture = DomCapture::start(app); - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); - let listener = app.listen_any("cap-stop-benchmark-frame", move |_| { - let _ = sender.send(Instant::now()); - }); - let started = Instant::now(); - tracing::info!("Stop editor benchmark stop requested"); - let stopped = crate::recording::stop_recording(app.clone(), state).await; - let stop_ms = started.elapsed().as_secs_f64() * 1000.0; - if let Err(error) = stopped { - app.unlisten(listener); - return Err(error); - } - let frame = tokio::time::timeout(Duration::from_secs(180), receiver.recv()).await; - app.unlisten(listener); - let frame = frame - .map_err(|error| error.to_string())? - .ok_or("Editor frame channel closed")?; - let mut result = serde_json::json!({ - "project": project, - "recordingSeconds": seconds, - "stopCommandMs": stop_ms, - "editorFrameMs": frame.saturating_duration_since(started).as_secs_f64() * 1000.0, - }); - if let Some(dom_capture) = dom_capture { - result["domObservations"] = dom_capture.finish().await; - } - Ok(result) -} - -fn benchmark_capture_target( - fixture_window: bool, -) -> Result { - use cap_recording::screen_capture::ScreenCaptureTarget; - if !fixture_window { - return Ok(ScreenCaptureTarget::Display { - id: scap_targets::Display::primary().id(), - }); - } - let window = select_exact_fixture( - scap_targets::Window::list() - .into_iter() - .map(|window| (window, window.name())), - )?; - Ok(ScreenCaptureTarget::Window { id: window.id() }) -} - -fn select_exact_fixture( - windows: impl Iterator)>, -) -> Result { - let mut matching = - windows.filter(|(_, name)| name.as_deref() == Some("Cap Stop benchmark fixture")); - let window = matching - .next() - .ok_or("Exact benchmark fixture window is absent")? - .0; - if matching.next().is_some() { - return Err("Exact benchmark fixture window is ambiguous".into()); - } - Ok(window) -} - -pub fn frame_capture_requested() -> bool { - enabled() && config().is_some_and(|config| config.frame_directory.is_some()) -} - -pub fn dom_capture_requested() -> bool { - enabled() && config().is_some_and(|config| config.dom_capture) -} - -#[derive(Clone, Copy)] -pub enum CaptureKind { - Preparing, - Ordinary, -} - -struct CapturedFrame { - data: Arc>, - width: u32, - height: u32, - stride: u32, - frame_number: u32, - target_time_ns: u64, -} - -struct FrameCapture { - directory: PathBuf, - project: PathBuf, - preparing: Option, - ordinary: Option, - closed: bool, -} - -impl FrameCapture { - fn record(&mut self, kind: CaptureKind, project: &Path, frame: CapturedFrame) { - if self.closed || self.project != project { - return; - } - let slot = match kind { - CaptureKind::Preparing => &mut self.preparing, - CaptureKind::Ordinary => &mut self.ordinary, - }; - if slot.is_none() { - *slot = Some(frame); - } - } -} - -fn arm_frame_capture(app: &tauri::AppHandle, project: Option<&Path>) -> Result<(), String> { - let Some(directory) = config().and_then(|config| config.frame_directory.as_ref()) else { - return Ok(()); - }; - if !app - .config() - .identifier - .starts_with("so.cap.desktop.stop-editor-benchmark.") - { - return Err("Frame capture requires a private benchmark app identifier".into()); - } - let directory = directory.clone(); - if !directory.is_absolute() { - return Err("Frame capture directory must be absolute".into()); - } - let project = project - .ok_or("Frame capture has no active recording project")? - .to_path_buf(); - std::fs::create_dir(&directory).map_err(|error| error.to_string())?; - FRAME_CAPTURE - .set(Mutex::new(FrameCapture { - directory, - project, - preparing: None, - ordinary: None, - closed: false, - })) - .map_err(|_| "Frame capture was already armed".to_string()) -} - -pub fn capture_output(kind: CaptureKind, project: &Path, output: &cap_editor::EditorFrameOutput) { - let Some(capture) = FRAME_CAPTURE.get() else { - return; - }; - let cap_editor::EditorFrameOutput::Rgba(frame) = output else { - return; - }; - capture - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .record( - kind, - project, - CapturedFrame { - data: frame.data.clone(), - width: frame.width, - height: frame.height, - stride: frame.padded_bytes_per_row, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - }, - ); -} - -pub fn capture_ws_frame(kind: CaptureKind, project: &Path, frame: &crate::frame_ws::WSFrame) { - let Some(capture) = FRAME_CAPTURE.get() else { - return; - }; - if frame.format != crate::frame_ws::WSFrameFormat::Rgba { - return; - } - capture - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .record( - kind, - project, - CapturedFrame { - data: match &frame.data { - crate::frame_ws::WSFrameData::Raw(data) => data.clone(), - crate::frame_ws::WSFrameData::Packed(data) => { - Arc::new(data[..data.len() - 24].to_vec()) - } - }, - width: frame.width, - height: frame.height, - stride: frame.stride, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - }, - ); -} - -fn write_captured_frame( - directory: &Path, - name: &str, - frame: CapturedFrame, -) -> Result { - let row_bytes = frame - .width - .checked_mul(4) - .ok_or("Captured frame row overflow")?; - let expected_bytes = (frame.stride as usize) - .checked_mul(frame.height as usize) - .ok_or("Captured frame length overflow")?; - if frame.width == 0 - || frame.height == 0 - || frame.stride < row_bytes - || frame.data.len() != expected_bytes - { - return Err("Captured frame has an incomplete RGBA buffer".into()); - } - let file = directory.join(format!("{name}.rgba")); - std::fs::write(&file, frame.data.as_slice()).map_err(|error| error.to_string())?; - Ok(serde_json::json!({ - "file": file, - "format": "rgba", - "width": frame.width, - "height": frame.height, - "stride": frame.stride, - "frameNumber": frame.frame_number, - "targetTimeNs": frame.target_time_ns, - "bytes": expected_bytes, - })) -} - -fn finish_frame_capture() -> Result, String> { - let Some(capture) = FRAME_CAPTURE.get() else { - return Ok(None); - }; - let (directory, project, preparing, ordinary) = { - let mut capture = capture - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - capture.closed = true; - ( - capture.directory.clone(), - capture.project.clone(), - capture.preparing.take(), - capture.ordinary.take(), - ) - }; - let mut frames = serde_json::Map::new(); - for (name, frame) in [("preparing", preparing), ("ordinary", ordinary)] { - if let Some(frame) = frame { - let _ = frames.insert( - name.to_string(), - write_captured_frame(&directory, name, frame)?, - ); - } - } - let value = serde_json::json!({ - "status": if frames.len() == 2 { "captured_uncompared" } else { "incomplete" }, - "project": project, - "frames": frames, - "timingQualification": "Separate untimed parity capture; renderer callback bytes are not external compositor proof", - }); - std::fs::write(directory.join("frames.json"), value.to_string()) - .map_err(|error| error.to_string())?; - Ok(Some(value)) -} - -struct DomCapture { - app: tauri::AppHandle, - listener: tauri::EventId, - observations: Arc>>, -} - -impl DomCapture { - fn start(app: &tauri::AppHandle) -> Option { - if !dom_capture_requested() { - return None; - } - let observations = Arc::new(Mutex::new(Vec::new())); - let captured = observations.clone(); - let listener = app.listen_any("cap-stop-benchmark-dom", move |event| { - if let Ok(value) = serde_json::from_str(event.payload()) { - let mut observations = captured - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if observations.len() < 1024 { - observations.push(value); - } - } - }); - Some(Self { - app: app.clone(), - listener, - observations, - }) - } - - async fn finish(self) -> serde_json::Value { - tokio::time::sleep(Duration::from_secs(2)).await; - let observations = self - .observations - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .clone(); - serde_json::json!(observations) - } -} - -impl Drop for DomCapture { - fn drop(&mut self) { - self.app.unlisten(self.listener); - } -} - -#[cfg(test)] -mod invocation_tests { - use super::*; - - fn request(settings: &[(&str, &str)]) -> Result, String> { - parse_config( - ["--stop-editor-benchmark", "--allow-real-recording"].map(OsString::from), - |name| { - settings - .iter() - .find(|(key, _)| *key == name) - .map(|(_, value)| OsString::from(*value)) - }, - ) - } - - fn settings() -> [(&'static str, &'static str); 2] { - [ - ("CAP_STOP_EDITOR_BENCHMARK_OUTPUT", "/benchmark/result.json"), - ("CAP_STOP_BENCH_LOG_DIR", "/benchmark/logs"), - ] - } - - #[test] - fn ordinary_arguments_never_read_benchmark_environment_or_create_logs() { - for arguments in [ - vec![], - vec!["recording.cap"], - vec!["--stop-editor-benchmark=true"], - vec!["https://example.invalid/--allow-real-recording"], - ] { - let config = parse_config(arguments.into_iter().map(OsString::from), |_| { - panic!("Ordinary startup must not read benchmark environment") - }) - .unwrap(); - assert!( - prepare_config(config, |_, _| { - panic!("Ordinary startup must not create benchmark logs") - }) - .unwrap() - .is_none() - ); - } - } - - #[test] - fn incomplete_or_mixed_invocations_fail_before_reading_settings() { - for arguments in [ - vec!["--stop-editor-benchmark"], - vec!["--allow-real-recording"], - vec![ - "--stop-editor-benchmark", - "--stop-editor-benchmark", - "--allow-real-recording", - ], - vec![ - "--stop-editor-benchmark", - "--allow-real-recording", - "recording.cap", - ], - vec!["--", "--stop-editor-benchmark", "--allow-real-recording"], - ] { - assert!( - parse_config(arguments.into_iter().map(OsString::from), |_| { - panic!("Malformed invocation must fail before reading settings") - }) - .is_err() - ); - } - } - - #[test] - fn explicit_invocation_requires_both_private_paths() { - assert!(request(&[]).is_err()); - assert!(request(&settings()[..1]).is_err()); - assert!(request(&settings()[1..]).is_err()); - for missing in ["CAP_STOP_EDITOR_BENCHMARK_OUTPUT", "CAP_STOP_BENCH_LOG_DIR"] { - let mut settings = settings(); - for (name, value) in &mut settings { - if *name == missing { - *value = ""; - } - } - assert!(request(&settings).is_err()); - } - } - - #[test] - fn explicit_invocation_retains_default_duration_and_capture_options() { - let config = request(&settings()).unwrap().unwrap(); - assert_eq!(config.seconds, 15); - assert_eq!(config.output, Path::new("/benchmark/result.json")); - assert_eq!(config.log_directory, Path::new("/benchmark/logs")); - assert!(config.microphone.is_none()); - assert!(!config.system_audio); - assert!(!config.fixture_window); - assert!(config.frame_directory.is_none()); - assert!(!config.dom_capture); - } - - #[test] - fn explicit_invocation_freezes_all_capture_settings() { - let mut settings = settings().to_vec(); - settings.extend([ - ("CAP_STOP_BENCH_SECONDS", "7200"), - ("CAP_STOP_BENCH_MIC", "Test microphone"), - ("CAP_STOP_BENCH_SYSTEM_AUDIO", "1"), - ("CAP_STOP_BENCH_FIXTURE_WINDOW", "1"), - ("CAP_STOP_BENCH_FRAME_DIR", "/benchmark/frames"), - ("CAP_STOP_BENCH_DOM_CAPTURE", "1"), - ]); - let config = request(&settings).unwrap().unwrap(); - assert_eq!(config.seconds, 7200); - assert_eq!(config.microphone.as_deref(), Some("Test microphone")); - assert!(config.system_audio); - assert!(config.fixture_window); - assert_eq!( - config.frame_directory.as_deref(), - Some(Path::new("/benchmark/frames")) - ); - assert!(config.dom_capture); - } - - #[test] - fn invalid_duration_or_microphone_is_not_replaced_with_a_recording_default() { - for value in [ - "", - "invalid", - "-1", - "1.5", - "0", - "86401", - "18446744073709551616", - ] { - let mut settings = settings().to_vec(); - settings.push(("CAP_STOP_BENCH_SECONDS", value)); - assert!(request(&settings).is_err()); - } - let mut settings = settings().to_vec(); - settings.push(("CAP_STOP_BENCH_MIC", "")); - assert!(request(&settings).is_err()); - } - - #[test] - fn configured_invocation_must_pass_the_private_log_validator() { - let config = request(&settings()).unwrap(); - let mut calls = 0; - let prepared = prepare_config(config, |output, directory| { - calls += 1; - assert_eq!(output, Path::new("/benchmark/result.json")); - assert_eq!(directory, Path::new("/benchmark/logs")); - Ok(directory.to_path_buf()) - }) - .unwrap(); - assert_eq!(calls, 1); - assert!(prepared.is_some()); - assert!( - prepare_config(request(&settings()).unwrap(), |_, _| { - Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "retained evidence", - )) - }) - .is_err() - ); - } - - #[test] - fn app_admission_rejects_normal_and_empty_private_identifiers() { - for identifier in [ - "so.cap.desktop", - "so.cap.desktop.dev", - "so.cap.desktop.stop-editor-benchmark.", - "other.so.cap.desktop.stop-editor-benchmark.test", - ] { - assert!(!admit_identifier(false, identifier).unwrap()); - assert!(admit_identifier(true, identifier).is_err()); - } - let private = "so.cap.desktop.stop-editor-benchmark.test"; - assert!(!admit_identifier(false, private).unwrap()); - assert!(admit_identifier(true, private).unwrap()); - } -} - -#[cfg(test)] -mod parity_capture_tests { - use super::*; - - fn frame(value: u8) -> CapturedFrame { - CapturedFrame { - data: Arc::new(vec![value; 8]), - width: 2, - height: 1, - stride: 8, - frame_number: 0, - target_time_ns: 0, - } - } - - #[test] - fn exact_fixture_selection_rejects_missing_and_ambiguous_titles() { - assert!(select_exact_fixture(std::iter::empty::<(u8, Option)>()).is_err()); - assert_eq!( - select_exact_fixture( - [ - (1, Some("Cap Stop benchmark fixture".into())), - (2, Some("Cap Stop benchmark fixture extra".into())) - ] - .into_iter() - ) - .unwrap(), - 1 - ); - assert!( - select_exact_fixture( - [ - (1, Some("Cap Stop benchmark fixture".into())), - (2, Some("Cap Stop benchmark fixture".into())) - ] - .into_iter() - ) - .is_err() - ); - } - - #[test] - fn first_frames_are_bound_to_project_and_capture_closure() { - let project = PathBuf::from("owned.cap"); - let mut capture = FrameCapture { - directory: PathBuf::new(), - project: project.clone(), - preparing: None, - ordinary: None, - closed: false, - }; - capture.record(CaptureKind::Preparing, Path::new("other.cap"), frame(1)); - assert!(capture.preparing.is_none()); - capture.record(CaptureKind::Preparing, &project, frame(2)); - capture.record(CaptureKind::Preparing, &project, frame(3)); - capture.record(CaptureKind::Ordinary, &project, frame(4)); - assert_eq!(capture.preparing.as_ref().unwrap().data.as_slice(), &[2; 8]); - assert_eq!(capture.ordinary.as_ref().unwrap().data.as_slice(), &[4; 8]); - capture.closed = true; - capture.ordinary = None; - capture.record(CaptureKind::Ordinary, &project, frame(5)); - assert!(capture.ordinary.is_none()); - } - - #[test] - fn rgba_capture_rejects_truncation_and_preserves_full_padded_rows() { - let directory = tempfile::tempdir().unwrap(); - let mut truncated = frame(1); - truncated.data = Arc::new(vec![1; 7]); - assert!(write_captured_frame(directory.path(), "truncated", truncated).is_err()); - assert!(!directory.path().join("truncated.rgba").exists()); - let mut padded = frame(2); - padded.stride = 12; - padded.data = Arc::new(vec![2; 12]); - let metadata = write_captured_frame(directory.path(), "padded", padded).unwrap(); - assert_eq!(metadata["bytes"], 12); - assert_eq!( - std::fs::read(directory.path().join("padded.rgba")).unwrap(), - vec![2; 12] - ); - } -} diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs deleted file mode 100644 index 70a90cf59a5..00000000000 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ /dev/null @@ -1,1054 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, - sync::{Mutex, PoisonError}, - time::{Duration, Instant}, -}; - -use base64::prelude::*; -use cap_recording::screen_capture::ScreenCaptureTarget; - -use crate::exit_shutdown::{abort_join_handles, read_target_under_cursor}; -use crate::{ - App, ArcLock, general_settings, - recording_settings::RecordingTargetMode, - window_exclusion::WindowExclusion, - windows::{CapWindowId, ShowCapWindow, hide_overlay}, -}; -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}; - -mod lifecycle; - -#[derive(tauri_specta::Event, Serialize, Type, Clone)] -pub struct TargetUnderCursor { - display_id: Option, - window: Option, -} - -#[derive(Serialize, Type, Clone)] -pub struct WindowUnderCursor { - id: WindowId, - app_name: String, - bounds: LogicalBounds, -} - -#[derive(Serialize, Type, Clone)] -pub struct DisplayInformation { - name: Option, - physical_size: Option, - logical_size: Option, - logical_bounds: Option, - refresh_rate: String, -} - -#[specta::specta] -#[tauri::command] -#[instrument(skip(app, state))] -pub async fn open_target_select_overlays( - app: AppHandle, - state: tauri::State<'_, WindowFocusManager>, - focused_target: Option, - specific_display_id: Option, - target_mode: Option, -) -> Result<(), String> { - let session = open_target_select_overlays_for_session( - app, - state.inner(), - focused_target, - specific_display_id, - target_mode, - ) - .await?; - if state.picker_is_current(session) { - Ok(()) - } else { - Err("Target selection cancelled".into()) - } -} - -pub(crate) async fn open_target_select_overlays_for_session( - app: AppHandle, - state: &WindowFocusManager, - focused_target: Option, - specific_display_id: Option, - target_mode: Option, -) -> Result { - if crate::clean_capture::phase(&app).is_some() { - return Err( - "Close or finish clean Studio recording before opening the target picker".into(), - ); - } - let start = Instant::now(); - let picker_session = state.begin_picker(); - state.register_escape(app.global_shortcut()); - - let resolved_specific_display_id = specific_display_id.as_ref().map(|id_str| { - id_str - .parse::() - .unwrap_or_else(|_| Display::primary().id()) - }); - - let display_ids = if let Some(display_id) = resolved_specific_display_id.clone() { - vec![display_id] - } else if let Some(display) = focused_target.as_ref().and_then(|t| t.display()) { - vec![display.id()] - } else { - let displays = Display::list(); - if displays.is_empty() { - vec![Display::primary().id()] - } else { - displays.into_iter().map(|display| display.id()).collect() - } - }; - - let focus_display_id = resolved_specific_display_id - .or_else(|| { - focused_target - .as_ref() - .and_then(|t| t.display()) - .map(|d| d.id()) - }) - .or_else(|| Display::get_containing_cursor().map(|d| d.id())) - .unwrap_or_else(|| Display::primary().id()); - - for (id, window) in app.webview_windows() { - if let Ok(CapWindowId::TargetSelectOverlay { - display_id: existing_id, - }) = CapWindowId::from_str(&id) - && !display_ids - .iter() - .any(|display_id| display_id == &existing_id) - { - hide_overlay(&window); - state.destroy(&existing_id, app.global_shortcut()); - } - } - - for display_id in &display_ids { - if !state.picker_is_current(picker_session) { - return Ok(picker_session); - } - let should_focus = display_id == &focus_display_id; - - let existing_window = CapWindowId::TargetSelectOverlay { - display_id: display_id.clone(), - } - .get(&app); - - if let Some(window) = existing_window { - request_overlay_reveal(&window, picker_session, should_focus); - - if should_focus { - focus_target_select_overlay(&window, picker_session); - } - - state.spawn(display_id, window.clone(), picker_session); - } else if start.elapsed() < Duration::from_secs(1) { - if let Ok(window) = (ShowCapWindow::TargetSelectOverlay { - display_id: display_id.clone(), - target_mode, - }) - .show_for_picker(&app, picker_session) - .await - { - finish_created_target_select_overlay(&window, should_focus, picker_session); - } - } else { - let app_clone = app.clone(); - let display_id_clone = display_id.clone(); - tokio::spawn(async move { - if let Ok(window) = (ShowCapWindow::TargetSelectOverlay { - display_id: display_id_clone, - target_mode, - }) - .show_for_picker(&app_clone, picker_session) - .await - { - finish_created_target_select_overlay(&window, should_focus, picker_session); - } - }); - } - } - - let focus_window = CapWindowId::TargetSelectOverlay { - display_id: focus_display_id, - } - .get(&app); - - if let Some(window) = focus_window { - focus_target_select_overlay(&window, picker_session); - } - - let window_exclusions = general_settings::GeneralSettingsStore::get(&app) - .ok() - .flatten() - .map_or_else(general_settings::default_excluded_windows, |settings| { - settings.excluded_windows - }); - - let handle = tokio::spawn({ - let app = app.clone(); - - async move { - while let Some((display, window)) = read_target_under_cursor( - || { - crate::app_is_exiting(&app) - || !app - .state::() - .picker_is_current(picker_session) - }, - || { - focused_target - .as_ref() - .map(|v| v.display()) - .unwrap_or_else(scap_targets::Display::get_containing_cursor) - }, - || { - focused_target - .as_ref() - .map(|v| { - v.window() - .and_then(|id| scap_targets::Window::from_id(&id)) - .and_then(|window| { - describe_selectable_window(window, &window_exclusions) - }) - }) - .unwrap_or_else(|| target_window_under_cursor(&window_exclusions)) - }, - ) { - let _ = TargetUnderCursor { - display_id: display.map(|d| d.id()), - window, - } - .emit(&app); - - tokio::time::sleep(Duration::from_millis(50)).await; - } - } - }); - - let lifecycle = state - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner); - if !lifecycle.is_current(picker_session) { - handle.abort(); - return Ok(picker_session); - } - if let Some(task) = state - .task - .lock() - .unwrap_or_else(PoisonError::into_inner) - .replace(handle) - { - task.abort(); - } - drop(lifecycle); - - Ok(picker_session) -} - -fn describe_selectable_window( - window: Window, - exclusions: &[WindowExclusion], -) -> Option { - if should_skip_window(&window, exclusions) { - return None; - } - Some(WindowUnderCursor { - id: window.id(), - bounds: window.display_relative_logical_bounds()?, - app_name: window.owner_name()?, - }) -} - -fn target_window_under_cursor(exclusions: &[WindowExclusion]) -> Option { - #[cfg(target_os = "linux")] - { - let own_pid = std::process::id(); - let candidates = Window::list_containing_cursor() - .into_iter() - .filter_map(|window| { - let metadata = window.raw_handle().selection_metadata()?; - let title = if metadata.owner_pid == Some(own_pid) { - window.name() - } else { - None - }; - Some(LinuxPickerCandidate { - window, - is_viewable: metadata.is_viewable, - owner_pid: metadata.owner_pid, - title, - }) - }); - first_linux_picker_target(candidates, own_pid, |window| { - describe_selectable_window(window, exclusions) - }) - } - - #[cfg(not(target_os = "linux"))] - Window::get_topmost_at_cursor() - .and_then(|window| describe_selectable_window(window, exclusions)) -} - -#[cfg(any(target_os = "linux", test))] -struct LinuxPickerCandidate { - window: T, - is_viewable: bool, - owner_pid: Option, - title: Option, -} - -#[cfg(any(target_os = "linux", test))] -fn first_linux_picker_target( - candidates: impl IntoIterator>, - own_pid: u32, - mut describe: impl FnMut(T) -> Option, -) -> Option { - candidates.into_iter().find_map(|candidate| { - if !candidate.is_viewable - || (candidate.owner_pid == Some(own_pid) - && candidate.title.as_deref() == Some("Cap Target Select")) - { - return None; - } - describe(candidate.window) - }) -} - -fn finish_created_target_select_overlay( - window: &WebviewWindow, - should_focus: bool, - picker_session: u32, -) { - request_overlay_focus(window, picker_session, should_focus); -} - -fn focus_target_select_overlay(window: &WebviewWindow, picker_session: u32) { - #[cfg(target_os = "macos")] - let _ = (window, picker_session); - - #[cfg(not(target_os = "macos"))] - request_overlay_focus(window, picker_session, true); -} - -pub(crate) fn request_overlay_reveal(window: &WebviewWindow, session: u32, focus: bool) { - let reveal = window - .app_handle() - .state::() - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .request(window.label(), session, focus); - if let Some(reveal) = reveal { - schedule_ready_overlay(window, reveal); - } -} - -pub(crate) fn restore_overlay_reveal(window: &WebviewWindow, session: u32) { - let reveal = window - .app_handle() - .state::() - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .restore(window.label(), session, false); - if let Some(reveal) = reveal { - schedule_ready_overlay(window, reveal); - } -} - -fn request_overlay_focus(window: &WebviewWindow, session: u32, focus: bool) { - let reveal = window - .app_handle() - .state::() - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .focus(window.label(), session, focus); - if let Some(reveal) = reveal { - schedule_ready_overlay(window, reveal); - } -} - -fn schedule_ready_overlay(window: &WebviewWindow, reveal: lifecycle::Reveal) { - let handle = window.clone(); - let generation = crate::clean_capture::generation(window.app_handle()); - let _ = window.run_on_main_thread(move || { - let focus = handle - .app_handle() - .state::() - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .may_reveal(handle.label(), reveal); - let Some(focus) = focus else { - return; - }; - if !matches!( - crate::clean_capture::reveal_now(&handle, generation), - Ok(true) - ) { - return; - } - if let Err(error) = handle.set_ignore_cursor_events(false) { - error!(%error, "Failed to enable target picker interaction"); - hide_overlay(&handle); - return; - } - #[cfg(not(target_os = "macos"))] - if focus { - let _ = handle.set_focus(); - } - #[cfg(target_os = "macos")] - if focus { - use tauri_nspanel::WebviewWindowExt; - if let Ok(panel) = handle.to_panel() { - panel.show(); - } - } - }); -} - -pub(crate) fn mark_overlay_native_ready(window: &WebviewWindow, instance: u32) { - mark_overlay_ready(window, instance, false); -} - -fn mark_overlay_ready(window: &WebviewWindow, instance: u32, frontend: bool) { - let reveal = window - .app_handle() - .state::() - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .ready(window.label(), instance, frontend); - if let Some(reveal) = reveal { - schedule_ready_overlay(window, reveal); - } -} - -#[tauri::command] -#[specta::specta] -pub fn target_select_overlay_ready(window: WebviewWindow, instance: u32) { - mark_overlay_ready(&window, instance, true); -} - -fn should_skip_window(window: &Window, exclusions: &[WindowExclusion]) -> bool { - if exclusions.is_empty() { - return false; - } - - let owner_name = window.owner_name(); - let window_title = window.name(); - - #[cfg(target_os = "macos")] - let bundle_identifier = window.raw_handle().bundle_identifier(); - - #[cfg(not(target_os = "macos"))] - let bundle_identifier = None::; - - exclusions.iter().any(|entry| { - entry.matches( - bundle_identifier.as_deref(), - owner_name.as_deref(), - window_title.as_deref(), - ) - }) -} - -#[specta::specta] -#[tauri::command] -#[instrument(skip(app))] -pub async fn update_camera_overlay_bounds( - app: AppHandle, - x: f64, - y: f64, - width: f64, - height: f64, -) -> Result<(), String> { - let window = CapWindowId::Camera - .get(&app) - .ok_or("Camera window not found")?; - - let width_u32 = width as u32; - let height_u32 = height as u32; - - window - .set_size(tauri::Size::Physical(tauri::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, - })) - .map_err(|e| e.to_string())?; - - let scale_factor = window.scale_factor().unwrap_or(1.0); - let logical_width = (width / scale_factor) as u32; - let logical_height = (height / scale_factor) as u32; - - let state = app.state::>(); - let app_state = state.read().await; - app_state - .camera_preview - .notify_window_resized(logical_width, logical_height); - - Ok(()) -} - -#[specta::specta] -#[tauri::command] -#[instrument(skip(app, _state))] -pub async fn close_target_select_overlays( - app: AppHandle, - _state: tauri::State<'_, WindowFocusManager>, -) -> Result<(), String> { - close_target_select_overlay_windows(&app); - - Ok(()) -} - -#[specta::specta] -#[tauri::command] -pub fn suspend_target_select_overlays(app: AppHandle) { - let state = app.state::(); - state.shutdown(&app); - state.clear_overlay_restore_labels(); - for (label, window) in app.webview_windows() { - if matches!( - CapWindowId::from_str(&label), - Ok(CapWindowId::TargetSelectOverlay { .. }) - ) { - hide_overlay(&window); - } - } -} - -pub fn close_target_select_overlay_windows(app: &AppHandle) { - let state = app.try_state::(); - let mut saw_overlay = false; - - if let Some(state) = state.as_ref() { - state.cancel_picker(); - state.clear_overlay_restore_labels(); - } - - for (id, window) in app.webview_windows() { - if let Ok(CapWindowId::TargetSelectOverlay { display_id }) = CapWindowId::from_str(&id) { - saw_overlay = true; - // On Windows, hide() leaves the DirectComposition transparency surface composited on - // screen (ghost overlay). Closing the window fully releases the surface. - hide_overlay(&window); - #[cfg(windows)] - let _ = window.close(); - if let Some(state) = state.as_ref() { - state.destroy(&display_id, app.global_shortcut()); - } - } - } - - if !saw_overlay && let Some(state) = state { - state.shutdown(app); - } -} - -pub(crate) fn dismiss_picker_from_escape(app: &AppHandle) { - let Some(session) = app.state::().picker_session() else { - return; - }; - let owner = CapWindowId::Main.get(app); - close_target_select_overlay_windows(app); - if let Some(owner) = owner { - let app = app.clone(); - let generation = crate::clean_capture::generation(&app); - let _ = owner.clone().run_on_main_thread(move || { - let cancelled = app - .state::() - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .is_cancelled(session); - let idle = app - .state::>() - .try_read() - .is_ok_and(|state| !state.is_recording_active_or_pending()); - if cancelled - && idle - && matches!( - crate::clean_capture::reveal_now(&owner, generation), - Ok(true) - ) - { - let _ = owner.set_focus(); - } - }); - } -} - -#[specta::specta] -#[tauri::command] -#[instrument] -pub async fn get_window_icon(window_id: &str) -> Result, String> { - let window_id = window_id - .parse::() - .map_err(|err| format!("Invalid window ID: {err}"))?; - - Ok(Window::from_id(&window_id) - .ok_or("Window not found")? - .app_icon() - .map(|bytes| format!("data:image/png;base64,{}", BASE64_STANDARD.encode(&bytes)))) -} - -#[specta::specta] -#[tauri::command] -#[instrument] -pub async fn display_information(display_id: &str) -> Result { - let display_id = display_id - .parse::() - .map_err(|err| format!("Invalid display ID: {err}"))?; - let display = Display::from_id(&display_id).ok_or("Display not found")?; - - Ok(DisplayInformation { - name: display.name(), - physical_size: display.physical_size(), - logical_size: display.logical_size(), - logical_bounds: display.raw_handle().logical_bounds(), - refresh_rate: display.refresh_rate().to_string(), - }) -} - -#[specta::specta] -#[tauri::command] -#[instrument] -pub async fn focus_window(window_id: WindowId) -> Result<(), String> { - #[cfg(any(target_os = "macos", target_os = "windows"))] - let window = Window::from_id(&window_id).ok_or("Window not found")?; - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - Window::from_id(&window_id).ok_or("Window not found")?; - - #[cfg(target_os = "macos")] - { - use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; - - let pid = window - .raw_handle() - .owner_pid() - .ok_or("Could not get window owner PID")?; - - if let Some(app) = - unsafe { NSRunningApplication::runningApplicationWithProcessIdentifier(pid) } - { - unsafe { - app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); - } - } - } - - #[cfg(target_os = "windows")] - { - use windows::Win32::UI::WindowsAndMessaging::{ - GetWindowPlacement, IsIconic, SW_RESTORE, SetForegroundWindow, SetWindowPlacement, - ShowWindow, WINDOWPLACEMENT, - }; - - let hwnd = window.raw_handle().inner(); - - unsafe { - // Only restore if the window is actually minimized - if IsIconic(hwnd).as_bool() { - // Get current window placement to preserve size/position - let mut wp = WINDOWPLACEMENT { - length: std::mem::size_of::() as u32, - ..Default::default() - }; - - if GetWindowPlacement(hwnd, &mut wp).is_ok() { - // Restore using the previous placement to avoid resizing - wp.showCmd = SW_RESTORE.0 as u32; - let _ = SetWindowPlacement(hwnd, &wp); - } else { - // Fallback to simple restore if placement fails - let _ = ShowWindow(hwnd, SW_RESTORE); - } - } - - // Always try to bring to foreground - let _ = SetForegroundWindow(hwnd); - } - } - - Ok(()) -} - -#[derive(Default)] -pub struct WindowFocusManager { - lifecycle: Mutex, - pub(crate) creation: tokio::sync::Mutex<()>, - task: Mutex>>, - tasks: Mutex>>, - escape_registered: Mutex, - restore_overlay_labels: Mutex>, -} - -impl WindowFocusManager { - pub(crate) fn begin_picker(&self) -> u32 { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .begin() - } - - pub(crate) fn picker_session(&self) -> Option { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .current() - } - - pub(crate) fn picker_is_current(&self, session: u32) -> bool { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .is_current(session) - } - - pub(crate) fn cancel_picker(&self) -> bool { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .cancel() - } - - pub(crate) fn register_overlay(&self, label: &str, session: u32) -> Option { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .register(label, session) - } - - pub(crate) fn suspend_overlay(&self, label: &str) { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .suspend(label); - } - - pub(crate) fn suspend_all_overlays(&self) { - self.lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .suspend_all(); - } - - fn abort_all_tasks(&self) { - let tasks = { - let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); - tasks.drain().map(|(_, task)| task).collect::>() - }; - let task = self - .task - .lock() - .unwrap_or_else(PoisonError::into_inner) - .take(); - abort_join_handles(tasks, task); - } - - pub fn spawn(&self, id: &DisplayId, window: WebviewWindow, picker_session: u32) { - let display_id = id.clone(); - let task_id = id.to_string(); - let handle = tokio::spawn(async move { - let app = window.app_handle(); - let mut main_window_was_seen = false; - - loop { - if crate::app_is_exiting(app) - || !app - .state::() - .picker_is_current(picker_session) - { - break; - } - - let cap_main = CapWindowId::Main.get(app); - let cap_settings = CapWindowId::Settings.get(app); - - let main_window_available = cap_main.is_some(); - let settings_window_available = cap_settings.is_some(); - - if main_window_available || settings_window_available { - main_window_was_seen = true; - } - - if main_window_was_seen && !main_window_available && !settings_window_available { - hide_overlay(&window); - app.state::() - .finish(&display_id, app.global_shortcut()); - break; - } - - #[cfg(windows)] - if window.is_visible().unwrap_or(false) - && let Some(cap_main) = cap_main - { - // Every overlay window (one per display) runs this loop. Checking only - // `window`/main focus made each inactive overlay steal focus from the - // overlay the user is interacting with on multi-monitor setups, firing a - // `blur` that cancelled the in-progress crop drag. Focus held by any - // overlay is fine, so only refocus when nothing of ours is focused. - let overlay_focused = app.webview_windows().iter().any(|(label, other)| { - matches!( - CapWindowId::from_str(label), - Ok(CapWindowId::TargetSelectOverlay { .. }) - ) && other.is_focused().unwrap_or(false) - }); - - let should_refocus = - overlay_focused || cap_main.is_focused().ok().unwrap_or_default(); - - if !should_refocus { - request_overlay_focus(&window, picker_session, true); - } - } - - tokio::time::sleep(std::time::Duration::from_millis(400)).await; - } - }); - - let lifecycle = self - .lifecycle - .lock() - .unwrap_or_else(PoisonError::into_inner); - if !lifecycle.is_current(picker_session) { - handle.abort(); - return; - } - let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(task) = tasks.insert(task_id, handle) { - task.abort(); - } - } - - pub fn remember_overlay_for_restore(&self, label: String) { - let mut labels = self - .restore_overlay_labels - .lock() - .unwrap_or_else(PoisonError::into_inner); - labels.insert(label); - } - - pub fn take_overlay_restore_labels(&self) -> HashSet { - let mut labels = self - .restore_overlay_labels - .lock() - .unwrap_or_else(PoisonError::into_inner); - std::mem::take(&mut *labels) - } - - pub fn clear_overlay_restore_labels(&self) { - let mut labels = self - .restore_overlay_labels - .lock() - .unwrap_or_else(PoisonError::into_inner); - labels.clear(); - } - - fn finish(&self, id: &DisplayId, global_shortcut: &GlobalShortcut) { - 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); - } - - fn finish_if_idle(&self, global_shortcut: &GlobalShortcut) { - let idle = self - .tasks - .lock() - .unwrap_or_else(PoisonError::into_inner) - .is_empty(); - if idle && self.picker_session().is_none() { - self.unregister_escape(global_shortcut); - - if let Some(task) = self - .task - .lock() - .unwrap_or_else(PoisonError::into_inner) - .take() - { - task.abort(); - } - } - } - - /// Called when a specific overlay window is destroyed to cleanup it's resources - pub fn destroy(&self, id: &DisplayId, global_shortcut: &GlobalShortcut) { - 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); - } - - pub fn shutdown(&self, app: &AppHandle) { - self.cancel_picker(); - self.abort_all_tasks(); - - self.unregister_escape(app.global_shortcut()); - } - - fn register_escape(&self, global_shortcut: &GlobalShortcut) { - let mut registered = self - .escape_registered - .lock() - .unwrap_or_else(PoisonError::into_inner); - if *registered { - return; - } - - match global_shortcut.register("Escape") { - Ok(()) => *registered = true, - Err(err) => error!("Error registering global keyboard shortcut for Escape: {err}"), - } - } - - fn unregister_escape(&self, global_shortcut: &GlobalShortcut) { - { - let mut registered = self - .escape_registered - .lock() - .unwrap_or_else(PoisonError::into_inner); - if !*registered { - return; - } - *registered = false; - } - - if let Err(err) = global_shortcut.unregister("Escape") { - debug!("Error unregistering global keyboard shortcut for Escape: {err}"); - } - } -} - -#[cfg(test)] -mod linux_picker_tests { - use super::{LinuxPickerCandidate, WindowExclusion, first_linux_picker_target}; - - fn candidate( - title: &'static str, - owner_pid: Option, - is_viewable: bool, - ) -> LinuxPickerCandidate<&'static str> { - LinuxPickerCandidate { - window: title, - is_viewable, - owner_pid, - title: Some(title.into()), - } - } - - #[test] - fn selects_underlying_window_after_picker_and_configured_exclusion() { - let exclusion = WindowExclusion { - bundle_identifier: None, - owner_name: Some("Cap".into()), - window_title: Some("Cap Camera".into()), - }; - let candidates = [ - candidate("Cap Target Select", Some(42), true), - candidate("Cap Camera", Some(42), true), - candidate("Moving fixture", Some(100), true), - candidate("Lower window", Some(101), true), - ]; - let mut described = Vec::new(); - - let selected = first_linux_picker_target(candidates, 42, |title| { - described.push(title); - (!exclusion.matches(None, Some("Cap"), Some(title))).then_some(title) - }); - - assert_eq!(selected, Some("Moving fixture")); - assert_eq!(described, ["Cap Camera", "Moving fixture"]); - } - - #[test] - fn skips_own_picker_when_no_windows_are_configured_for_exclusion() { - let candidates = [ - candidate("Cap Target Select", Some(42), true), - candidate("Cap Camera", Some(42), true), - candidate("Moving fixture", Some(100), true), - ]; - - assert_eq!( - first_linux_picker_target(candidates, 42, Some), - Some("Cap Camera") - ); - } - - #[test] - fn does_not_exclude_foreign_or_unknown_owner_by_picker_title() { - for owner_pid in [Some(100), None] { - let candidates = [ - candidate("Cap Target Select", owner_pid, true), - candidate("Moving fixture", Some(101), true), - ]; - - assert_eq!( - first_linux_picker_target(candidates, 42, Some), - Some("Cap Target Select") - ); - } - } - - #[test] - fn bypasses_unmapped_and_undescribable_windows_without_reordering() { - let candidates = [ - candidate("Hidden window", Some(100), false), - candidate("Destroyed window", Some(101), true), - candidate("Moving fixture", Some(102), true), - candidate("Lower window", Some(103), true), - ]; - let mut described = Vec::new(); - - let selected = first_linux_picker_target(candidates, 42, |title| { - described.push(title); - (title != "Destroyed window").then_some(title) - }); - - assert_eq!(selected, Some("Moving fixture")); - assert_eq!(described, ["Destroyed window", "Moving fixture"]); - } - - #[test] - fn returns_no_target_when_every_candidate_is_ineligible() { - let candidates = [ - candidate("Cap Target Select", Some(42), true), - candidate("Hidden window", Some(100), false), - candidate("Excluded window", Some(101), true), - ]; - let mut described = Vec::new(); - - let selected = first_linux_picker_target(candidates, 42, |title| { - described.push(title); - None::<&str> - }); - - assert!(selected.is_none()); - assert_eq!(described, ["Excluded window"]); - } -} diff --git a/apps/desktop/src-tauri/src/target_select_overlay/lifecycle.rs b/apps/desktop/src-tauri/src/target_select_overlay/lifecycle.rs deleted file mode 100644 index cd5bfa336c7..00000000000 --- a/apps/desktop/src-tauri/src/target_select_overlay/lifecycle.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::collections::HashMap; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(super) struct Reveal { - pub session: u32, - pub instance: u32, -} - -#[derive(Default)] -struct Window { - instance: u32, - requested: bool, - frontend_ready: bool, - native_ready: bool, - focus: bool, -} - -#[derive(Default)] -pub(super) struct Lifecycle { - session: u32, - next_instance: u32, - active: bool, - accepting_requests: bool, - windows: HashMap, -} - -impl Lifecycle { - pub fn begin(&mut self) -> u32 { - self.session = self.session.wrapping_add(1); - self.active = true; - self.accepting_requests = true; - for window in self.windows.values_mut() { - window.requested = false; - } - self.session - } - - pub fn current(&self) -> Option { - self.active.then_some(self.session) - } - - pub fn is_current(&self, session: u32) -> bool { - self.current() == Some(session) - } - - pub fn is_cancelled(&self, session: u32) -> bool { - !self.active && self.session == session - } - - pub fn cancel(&mut self) -> bool { - let active = self.active; - self.active = false; - for window in self.windows.values_mut() { - window.requested = false; - } - active - } - - pub fn register(&mut self, label: &str, session: u32) -> Option { - if !self.is_current(session) { - return None; - } - self.next_instance = self.next_instance.checked_add(1)?; - self.windows.insert( - label.to_string(), - Window { - instance: self.next_instance, - requested: self.accepting_requests, - ..Default::default() - }, - ); - Some(self.next_instance) - } - - pub fn request(&mut self, label: &str, session: u32, focus: bool) -> Option { - if !self.accepting_requests { - return None; - } - self.restore(label, session, focus) - } - - pub fn suspend_all(&mut self) { - self.accepting_requests = false; - for window in self.windows.values_mut() { - window.requested = false; - } - } - - pub fn restore(&mut self, label: &str, session: u32, focus: bool) -> Option { - if !self.is_current(session) { - return None; - } - let window = self.windows.get_mut(label)?; - window.requested = true; - window.focus = focus; - Some(Reveal { - session, - instance: window.instance, - }) - } - - pub fn focus(&mut self, label: &str, session: u32, focus: bool) -> Option { - if !self.is_current(session) { - return None; - } - let window = self.windows.get_mut(label)?; - if !window.requested { - return None; - } - window.focus = focus; - Some(Reveal { - session, - instance: window.instance, - }) - } - - pub fn suspend(&mut self, label: &str) { - if let Some(window) = self.windows.get_mut(label) { - window.requested = false; - } - } - - pub fn ready(&mut self, label: &str, instance: u32, frontend: bool) -> Option { - let window = self.windows.get_mut(label)?; - if window.instance != instance { - return None; - } - if frontend { - window.frontend_ready = true; - } else { - window.native_ready = true; - } - Some(Reveal { - session: self.session, - instance, - }) - } - - pub fn may_reveal(&self, label: &str, reveal: Reveal) -> Option { - if !self.is_current(reveal.session) { - return None; - } - self.windows.get(label).and_then(|window| { - (window.instance == reveal.instance - && window.requested - && window.frontend_ready - && window.native_ready) - .then_some(window.focus) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn prepared(lifecycle: &mut Lifecycle, label: &str, session: u32) -> Reveal { - let instance = lifecycle.register(label, session).unwrap(); - lifecycle.ready(label, instance, true).unwrap(); - lifecycle.ready(label, instance, false).unwrap() - } - - #[test] - fn both_native_and_frontend_readiness_are_required_in_either_order() { - for first in [true, false] { - let mut lifecycle = Lifecycle::default(); - let session = lifecycle.begin(); - let instance = lifecycle.register("overlay", session).unwrap(); - let reveal = lifecycle.request("overlay", session, true).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", reveal), None); - lifecycle.ready("overlay", instance, first).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", reveal), None); - lifecycle.ready("overlay", instance, !first).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", reveal), Some(true)); - } - } - - #[test] - fn cancel_rejects_late_creation_readiness_and_queued_reveals() { - let mut lifecycle = Lifecycle::default(); - let session = lifecycle.begin(); - let reveal = prepared(&mut lifecycle, "first", session); - assert!(lifecycle.cancel()); - assert!(!lifecycle.cancel()); - assert!(lifecycle.register("late", session).is_none()); - lifecycle.ready("first", reveal.instance, true).unwrap(); - assert_eq!(lifecycle.may_reveal("first", reveal), None); - assert!(lifecycle.request("first", session, true).is_none()); - } - - #[test] - fn new_session_reuses_ready_windows_without_accepting_old_callbacks() { - let mut lifecycle = Lifecycle::default(); - let first = lifecycle.begin(); - let stale = prepared(&mut lifecycle, "overlay", first); - lifecycle.cancel(); - let second = lifecycle.begin(); - let current = lifecycle.request("overlay", second, false).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", stale), None); - assert_eq!(lifecycle.may_reveal("overlay", current), Some(false)); - assert!(lifecycle.request("overlay", first, true).is_none()); - } - - #[test] - fn replaced_window_cannot_be_revealed_by_destroyed_frontend() { - let mut lifecycle = Lifecycle::default(); - let session = lifecycle.begin(); - let old = prepared(&mut lifecycle, "overlay", session); - let replacement = lifecycle.register("overlay", session).unwrap(); - let reveal = lifecycle.ready("overlay", replacement, false).unwrap(); - assert!(lifecycle.ready("overlay", old.instance, true).is_none()); - assert_eq!(lifecycle.may_reveal("overlay", old), None); - assert_eq!(lifecycle.may_reveal("overlay", reveal), None); - lifecycle.ready("overlay", replacement, true).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", reveal), Some(false)); - } - - #[test] - fn modal_hiding_cannot_be_undone_by_readiness_or_a_queued_reveal() { - let mut lifecycle = Lifecycle::default(); - let session = lifecycle.begin(); - let reveal = prepared(&mut lifecycle, "overlay", session); - lifecycle.suspend("overlay"); - lifecycle.ready("overlay", reveal.instance, true).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", reveal), None); - assert!(lifecycle.focus("overlay", session, true).is_none()); - let restored = lifecycle.request("overlay", session, true).unwrap(); - assert_eq!(lifecycle.may_reveal("overlay", restored), Some(true)); - } - - #[test] - fn modal_suspends_pending_displays_until_explicit_restore_or_a_new_session() { - let mut lifecycle = Lifecycle::default(); - let session = lifecycle.begin(); - let first = prepared(&mut lifecycle, "one", session); - lifecycle.suspend_all(); - let late = prepared(&mut lifecycle, "late", session); - assert_eq!(lifecycle.may_reveal("one", first), None); - assert_eq!(lifecycle.may_reveal("late", late), None); - assert!(lifecycle.request("one", session, true).is_none()); - let restored = lifecycle.restore("one", session, true).unwrap(); - assert_eq!(lifecycle.may_reveal("one", restored), Some(true)); - assert_eq!(lifecycle.may_reveal("late", late), None); - let next = lifecycle.begin(); - let reopened = lifecycle.request("late", next, true).unwrap(); - assert_eq!(lifecycle.may_reveal("late", reopened), Some(true)); - } - - #[test] - fn displays_are_independent_and_cancelled_sessions_do_not_restore() { - let mut lifecycle = Lifecycle::default(); - let session = lifecycle.begin(); - let first = prepared(&mut lifecycle, "one", session); - let second = prepared(&mut lifecycle, "two", session); - lifecycle.suspend("one"); - assert_eq!(lifecycle.may_reveal("one", first), None); - assert_eq!(lifecycle.may_reveal("two", second), Some(false)); - lifecycle.cancel(); - assert!(lifecycle.request("one", session, false).is_none()); - assert_eq!(lifecycle.may_reveal("two", second), None); - } -} diff --git a/apps/desktop/src-tauri/src/telemetry.rs b/apps/desktop/src-tauri/src/telemetry.rs deleted file mode 100644 index 10d1d908667..00000000000 --- a/apps/desktop/src-tauri/src/telemetry.rs +++ /dev/null @@ -1,468 +0,0 @@ -use serde::Serialize; -use serde_json::{Map, Value}; -use std::{ - sync::{ - OnceLock, PoisonError, RwLock, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; -use tauri::AppHandle; -use tracing::debug; - -use crate::auth::AuthStore; - -/// Baked in at compile time so self-built binaries send nothing. -const CLIENT_ID: Option<&str> = option_env!("VITE_OPENPANEL_CLIENT_ID"); -/// `/track` accepts origin-allowlisted requests without a client secret — the -/// 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 API_URL: Option<&str> = option_env!("VITE_OPENPANEL_API_URL"); -const DEFAULT_API_URL: &str = "https://api.openpanel.dev"; - -#[derive(Debug)] -pub enum AnalyticsEvent { - MultipartUploadComplete { - duration: Duration, - length: Duration, - size: u64, - }, - MultipartUploadFailed { - duration: Duration, - error: String, - }, - RecordingStarted { - mode: &'static str, - target_kind: &'static str, - has_camera: bool, - has_mic: bool, - has_system_audio: bool, - target_fps: u32, - target_width: u32, - target_height: u32, - fragmented: bool, - custom_cursor_capture: bool, - }, - RecordingCompleted { - mode: &'static str, - status: &'static str, - duration_secs: u64, - segment_count: u32, - track_failure_count: u32, - error_class: Option, - video_frames_captured: u64, - video_frames_dropped: u64, - drop_rate_pct: f64, - capture_stalls_count: u64, - capture_stalls_max_ms: u64, - mixer_stalls_count: u64, - mixer_stalls_max_ms: u64, - audio_gaps_count: u64, - audio_gaps_total_ms: u64, - frame_drop_rate_high_count: u64, - source_restarts_count: u64, - muxer_crash_count: u64, - audio_degraded_count: u64, - dropped_mic_messages: u64, - }, - RecordingMuxerCrashed { - mode: &'static str, - reason: String, - seconds_into_recording: f64, - }, - RecordingAudioDegraded { - mode: &'static str, - reason: String, - seconds_into_recording: f64, - }, - RecordingRecovered { - trigger: &'static str, - recovered_duration_secs: u64, - segments_recovered: u32, - validation_took_ms: u64, - }, - RecordingRecoveryFailed { - trigger: &'static str, - reason: String, - }, - RecordingDiskSpaceLow { - mode: &'static str, - bytes_remaining: u64, - }, - RecordingDiskSpaceExhausted { - mode: &'static str, - bytes_remaining: u64, - }, - RecordingDeviceLost { - mode: &'static str, - subsystem: String, - }, - RecordingEncoderRebuilt { - mode: &'static str, - backend: String, - attempt: u32, - }, - RecordingSourceAudioReset { - mode: &'static str, - source: String, - starvation_ms: u64, - }, - RecordingCaptureTargetLost { - mode: &'static str, - target: String, - }, -} - -fn truncate_reason(mut s: String) -> String { - const MAX_LEN: usize = 240; - if s.len() > MAX_LEN { - // Reasons carry NSError debug strings whose localized text is - // multi-byte; String::truncate panics off a char boundary. - let end = (0..=MAX_LEN) - .rev() - .find(|&i| s.is_char_boundary(i)) - .unwrap_or(0); - s.truncate(end); - s.push('…'); - } - s -} - -fn analytics_event(event: AnalyticsEvent) -> (&'static str, Map) { - fn set(props: &mut Map, key: &str, value: impl Into) { - props.insert(key.to_string(), value.into()); - } - - let mut props = Map::new(); - - match event { - AnalyticsEvent::MultipartUploadComplete { - duration, - length, - size, - } => { - set(&mut props, "duration", duration.as_secs()); - set(&mut props, "length", length.as_secs()); - set(&mut props, "size", size); - ("multipart_upload_complete", props) - } - AnalyticsEvent::MultipartUploadFailed { duration, error } => { - set(&mut props, "duration", duration.as_secs()); - set(&mut props, "error", truncate_reason(error)); - ("multipart_upload_failed", props) - } - AnalyticsEvent::RecordingStarted { - mode, - target_kind, - has_camera, - has_mic, - has_system_audio, - target_fps, - target_width, - target_height, - fragmented, - custom_cursor_capture, - } => { - set(&mut props, "mode", mode); - set(&mut props, "target_kind", target_kind); - set(&mut props, "has_camera", has_camera); - set(&mut props, "has_mic", has_mic); - set(&mut props, "has_system_audio", has_system_audio); - set(&mut props, "target_fps", target_fps); - set(&mut props, "target_width", target_width); - set(&mut props, "target_height", target_height); - set(&mut props, "fragmented", fragmented); - set(&mut props, "custom_cursor_capture", custom_cursor_capture); - ("recording_started", props) - } - AnalyticsEvent::RecordingCompleted { - mode, - status, - duration_secs, - segment_count, - track_failure_count, - error_class, - video_frames_captured, - video_frames_dropped, - drop_rate_pct, - capture_stalls_count, - capture_stalls_max_ms, - mixer_stalls_count, - mixer_stalls_max_ms, - audio_gaps_count, - audio_gaps_total_ms, - frame_drop_rate_high_count, - source_restarts_count, - muxer_crash_count, - audio_degraded_count, - dropped_mic_messages, - } => { - set(&mut props, "mode", mode); - set(&mut props, "status", status); - set(&mut props, "duration_secs", duration_secs); - set(&mut props, "segment_count", segment_count); - set(&mut props, "track_failure_count", track_failure_count); - if let Some(ec) = error_class { - set(&mut props, "error_class", truncate_reason(ec)); - } - set(&mut props, "video_frames_captured", video_frames_captured); - set(&mut props, "video_frames_dropped", video_frames_dropped); - set( - &mut props, - "drop_rate_pct", - (drop_rate_pct * 100.0).round() / 100.0, - ); - set(&mut props, "capture_stalls_count", capture_stalls_count); - set(&mut props, "capture_stalls_max_ms", capture_stalls_max_ms); - set(&mut props, "mixer_stalls_count", mixer_stalls_count); - set(&mut props, "mixer_stalls_max_ms", mixer_stalls_max_ms); - set(&mut props, "audio_gaps_count", audio_gaps_count); - set(&mut props, "audio_gaps_total_ms", audio_gaps_total_ms); - set( - &mut props, - "frame_drop_rate_high_count", - frame_drop_rate_high_count, - ); - set(&mut props, "source_restarts_count", source_restarts_count); - set(&mut props, "muxer_crash_count", muxer_crash_count); - set(&mut props, "audio_degraded_count", audio_degraded_count); - set(&mut props, "dropped_mic_messages", dropped_mic_messages); - ("recording_completed", props) - } - AnalyticsEvent::RecordingMuxerCrashed { - mode, - reason, - seconds_into_recording, - } => { - set(&mut props, "mode", mode); - set(&mut props, "reason", truncate_reason(reason)); - set( - &mut props, - "seconds_into_recording", - (seconds_into_recording * 1000.0).round() / 1000.0, - ); - ("recording_muxer_crashed", props) - } - AnalyticsEvent::RecordingAudioDegraded { - mode, - reason, - seconds_into_recording, - } => { - set(&mut props, "mode", mode); - set(&mut props, "reason", truncate_reason(reason)); - set( - &mut props, - "seconds_into_recording", - (seconds_into_recording * 1000.0).round() / 1000.0, - ); - ("recording_audio_degraded", props) - } - AnalyticsEvent::RecordingRecovered { - trigger, - recovered_duration_secs, - segments_recovered, - validation_took_ms, - } => { - set(&mut props, "trigger", trigger); - set( - &mut props, - "recovered_duration_secs", - recovered_duration_secs, - ); - set(&mut props, "segments_recovered", segments_recovered); - set(&mut props, "validation_took_ms", validation_took_ms); - ("recording_recovered", props) - } - AnalyticsEvent::RecordingRecoveryFailed { trigger, reason } => { - set(&mut props, "trigger", trigger); - set(&mut props, "reason", truncate_reason(reason)); - ("recording_recovery_failed", props) - } - AnalyticsEvent::RecordingDiskSpaceLow { - mode, - bytes_remaining, - } => { - set(&mut props, "mode", mode); - set(&mut props, "bytes_remaining", bytes_remaining); - ("recording_disk_space_low", props) - } - AnalyticsEvent::RecordingDiskSpaceExhausted { - mode, - bytes_remaining, - } => { - set(&mut props, "mode", mode); - set(&mut props, "bytes_remaining", bytes_remaining); - ("recording_disk_space_exhausted", props) - } - AnalyticsEvent::RecordingDeviceLost { mode, subsystem } => { - set(&mut props, "mode", mode); - set(&mut props, "subsystem", subsystem); - ("recording_device_lost", props) - } - AnalyticsEvent::RecordingEncoderRebuilt { - mode, - backend, - attempt, - } => { - set(&mut props, "mode", mode); - set(&mut props, "backend", backend); - set(&mut props, "attempt", attempt); - ("recording_encoder_rebuilt", props) - } - AnalyticsEvent::RecordingSourceAudioReset { - mode, - source, - starvation_ms, - } => { - set(&mut props, "mode", mode); - set(&mut props, "source", source); - set(&mut props, "starvation_ms", starvation_ms); - ("recording_source_audio_reset", props) - } - AnalyticsEvent::RecordingCaptureTargetLost { mode, target } => { - set(&mut props, "mode", mode); - set(&mut props, "target", target); - ("recording_capture_target_lost", props) - } - } -} - -#[derive(Serialize)] -struct TrackPayload { - name: &'static str, - properties: Map, - /// Omitted entirely when signed out, so OpenPanel records an anonymous - /// device event instead of attributing it to a profile. - #[serde(rename = "profileId", skip_serializing_if = "Option::is_none")] - profile_id: Option, -} - -#[derive(Serialize)] -struct TrackBody { - #[serde(rename = "type")] - kind: &'static str, - payload: TrackPayload, -} - -static HTTP_CLIENT: OnceLock = OnceLock::new(); - -fn http_client() -> &'static reqwest::Client { - HTTP_CLIENT.get_or_init(|| { - // OpenPanel derives device/OS breakdowns from the user agent. - let user_agent = format!( - "Cap/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ); - reqwest::Client::builder() - .user_agent(user_agent) - .timeout(Duration::from_secs(10)) - .build() - .unwrap_or_default() - }) -} - -fn track_url() -> String { - let base = API_URL.unwrap_or(DEFAULT_API_URL).trim_end_matches('/'); - format!("{base}/track") -} - -pub fn init() { - if CLIENT_ID.is_none() { - return; - } - - // Building the client loads the platform root certificates, so do it off - // the startup path rather than on the first event. - tokio::spawn(async { - http_client(); - }); -} - -pub fn set_server_url(url: &str) { - *API_SERVER_IS_CAP_CLOUD - .get_or_init(Default::default) - .write() - .unwrap_or_else(PoisonError::into_inner) = Some(url == "https://cap.so"); -} - -static API_SERVER_IS_CAP_CLOUD: OnceLock>> = OnceLock::new(); - -static TELEMETRY_ENABLED: AtomicBool = AtomicBool::new(true); - -pub fn set_telemetry_enabled(enabled: bool) { - TELEMETRY_ENABLED.store(enabled, Ordering::Release); -} - -pub fn telemetry_enabled() -> bool { - TELEMETRY_ENABLED.load(Ordering::Acquire) -} - -pub fn async_capture_event(app: &AppHandle, event: AnalyticsEvent) { - let Some(client_id) = CLIENT_ID else { - return; - }; - - let live_enabled = crate::general_settings::GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|s| s.enable_telemetry) - .unwrap_or_else(telemetry_enabled); - TELEMETRY_ENABLED.store(live_enabled, Ordering::Release); - if !live_enabled { - return; - } - - let profile_id = AuthStore::get(app) - .ok() - .flatten() - .and_then(|auth| auth.user_id); - - tokio::spawn(async move { - let (name, mut properties) = analytics_event(event); - - properties.insert("cap_version".into(), env!("CARGO_PKG_VERSION").into()); - properties.insert( - "cap_backend".into(), - match *API_SERVER_IS_CAP_CLOUD - .get_or_init(Default::default) - .read() - .unwrap_or_else(PoisonError::into_inner) - { - Some(true) => "cloud", - Some(false) => "self_hosted", - None => "unknown", - } - .into(), - ); - properties.insert("os".into(), std::env::consts::OS.into()); - properties.insert("arch".into(), std::env::consts::ARCH.into()); - - let body = TrackBody { - kind: "track", - payload: TrackPayload { - name, - properties, - profile_id, - }, - }; - - let req = http_client() - .post(track_url()) - .header("openpanel-client-id", client_id) - .header("origin", ORIGIN); - - // Events are best-effort: log and drop, never retry. - match req.json(&body).send().await { - Ok(res) if res.status().is_success() => {} - Ok(res) => debug!( - "OpenPanel rejected event {name}: HTTP {}", - res.status().as_u16() - ), - Err(err) => debug!("Error sending event {name} to OpenPanel: {err:?}"), - } - }); -} diff --git a/apps/desktop/src-tauri/src/thumbnails/linux.rs b/apps/desktop/src-tauri/src/thumbnails/linux.rs deleted file mode 100644 index f163cce7bad..00000000000 --- a/apps/desktop/src-tauri/src/thumbnails/linux.rs +++ /dev/null @@ -1,51 +0,0 @@ -use cap_recording::screenshot::capture_screenshot; -use cap_recording::sources::screen_capture::ScreenCaptureTarget; -use image::{ImageEncoder, codecs::png::PngEncoder}; -use std::io::Cursor; - -use super::*; - -pub async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option { - capture_target_thumbnail(ScreenCaptureTarget::Display { id: display.id() }).await -} - -pub async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option { - capture_target_thumbnail(ScreenCaptureTarget::Window { id: window.id() }).await -} - -async fn capture_target_thumbnail(target: ScreenCaptureTarget) -> Option { - if cap_recording::screenshot::uses_wayland_portal() { - return None; - } - - let image = match capture_screenshot(target).await { - Ok(image) => image, - Err(error) => { - warn!(error = %error, "Failed to capture thumbnail on Linux"); - return None; - } - }; - - let rgba = image.to_rgba8(); - if rgba.width() == 0 || rgba.height() == 0 { - return None; - } - - let thumbnail = normalize_thumbnail_dimensions(&rgba); - let mut png_data = Cursor::new(Vec::new()); - let encoder = PngEncoder::new(&mut png_data); - if let Err(error) = encoder.write_image( - thumbnail.as_raw(), - thumbnail.width(), - thumbnail.height(), - image::ColorType::Rgba8.into(), - ) { - warn!(error = %error, "Failed to encode Linux thumbnail as PNG"); - return None; - } - - Some(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - png_data.into_inner(), - )) -} diff --git a/apps/desktop/src-tauri/src/thumbnails/mac.rs b/apps/desktop/src-tauri/src/thumbnails/mac.rs deleted file mode 100644 index a6367973824..00000000000 --- a/apps/desktop/src-tauri/src/thumbnails/mac.rs +++ /dev/null @@ -1,377 +0,0 @@ -use cidre::{arc, sc}; - -use crate::platform::get_shareable_content; - -use super::*; - -pub async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option { - let content = get_shareable_content().await.ok()??; - - let filter = display.raw_handle().as_content_filter(content)?; - capture_thumbnail_from_filter(filter).await -} - -pub async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option { - let content = get_shareable_content().await.ok()??; - - let sc_window = window.raw_handle().as_sc(content)?; - let filter = cidre::sc::ContentFilter::with_desktop_independent_window(&sc_window); - capture_thumbnail_from_filter(filter).await -} - -async fn capture_thumbnail_from_filter(filter: arc::R) -> Option { - use cidre::{cv, sc}; - use image::{ImageEncoder, RgbaImage, codecs::png::PngEncoder}; - use std::io::Cursor; - - let mut config = sc::StreamCfg::new(); - config.set_width(THUMBNAIL_WIDTH as usize); - config.set_height(THUMBNAIL_HEIGHT as usize); - config.set_shows_cursor(false); - - let sample_buf = match unsafe { - sc::ScreenshotManager::capture_sample_buf(filter.as_ref(), &config) - } - .await - { - Ok(buf) => buf, - Err(err) => { - warn!(error = ?err, "Failed to capture sample buffer for thumbnail"); - return None; - } - }; - - let Some(image_buf) = sample_buf.image_buf() else { - warn!("Sample buffer missing image data"); - return None; - }; - let mut image_buf = image_buf.retained(); - - let width = image_buf.width(); - let height = image_buf.height(); - if width == 0 || height == 0 { - warn!( - width = width, - height = height, - "Captured thumbnail had empty dimensions" - ); - return None; - } - - let pixel_format = image_buf.pixel_format(); - - let lock = - match PixelBufferLock::new(image_buf.as_mut(), cv::pixel_buffer::LockFlags::READ_ONLY) { - Ok(lock) => lock, - Err(err) => { - warn!(error = ?err, "Failed to lock pixel buffer for thumbnail"); - return None; - } - }; - - let rgba_data = match pixel_format { - cv::PixelFormat::_32_BGRA - | cv::PixelFormat::_32_RGBA - | cv::PixelFormat::_32_ARGB - | cv::PixelFormat::_32_ABGR => { - convert_32bit_pixel_buffer(&lock, width, height, pixel_format)? - } - cv::PixelFormat::_420V => { - convert_nv12_pixel_buffer(&lock, width, height, Nv12Range::Video)? - } - other => { - warn!(?other, "Unsupported pixel format for thumbnail capture"); - return None; - } - }; - - let Some(img) = RgbaImage::from_raw(width as u32, height as u32, rgba_data) else { - warn!("Failed to construct RGBA image for thumbnail"); - return None; - }; - let thumbnail = normalize_thumbnail_dimensions(&img); - let mut png_data = Cursor::new(Vec::new()); - let encoder = PngEncoder::new(&mut png_data); - if let Err(err) = encoder.write_image( - thumbnail.as_raw(), - thumbnail.width(), - thumbnail.height(), - image::ColorType::Rgba8.into(), - ) { - warn!(error = ?err, "Failed to encode thumbnail as PNG"); - return None; - } - - Some(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - png_data.into_inner(), - )) -} - -fn convert_32bit_pixel_buffer( - lock: &PixelBufferLock<'_>, - width: usize, - height: usize, - pixel_format: cidre::cv::PixelFormat, -) -> Option> { - let base_ptr = lock.base_address(); - if base_ptr.is_null() { - warn!("Pixel buffer base address was null"); - return None; - } - - let bytes_per_row = lock.bytes_per_row(); - let total_len = bytes_per_row.checked_mul(height)?; - let raw_data = unsafe { std::slice::from_raw_parts(base_ptr, total_len) }; - - let mut rgba_data = Vec::with_capacity(width * height * 4); - for y in 0..height { - let row_start = y * bytes_per_row; - let row_end = row_start + width * 4; - if row_end > raw_data.len() { - warn!( - row_start = row_start, - row_end = row_end, - raw_len = raw_data.len(), - "Row bounds exceeded raw data length during thumbnail capture", - ); - return None; - } - - let row = &raw_data[row_start..row_end]; - for chunk in row.chunks_exact(4) { - match pixel_format { - cidre::cv::PixelFormat::_32_BGRA => { - rgba_data.extend_from_slice(&[chunk[2], chunk[1], chunk[0], chunk[3]]) - } - cidre::cv::PixelFormat::_32_RGBA => rgba_data.extend_from_slice(chunk), - cidre::cv::PixelFormat::_32_ARGB => { - rgba_data.extend_from_slice(&[chunk[1], chunk[2], chunk[3], chunk[0]]) - } - cidre::cv::PixelFormat::_32_ABGR => { - rgba_data.extend_from_slice(&[chunk[3], chunk[2], chunk[1], chunk[0]]) - } - _ => unreachable!(), - } - } - } - - Some(rgba_data) -} - -#[derive(Copy, Clone)] -enum Nv12Range { - Video, - _Full, -} - -fn convert_nv12_pixel_buffer( - lock: &PixelBufferLock<'_>, - width: usize, - height: usize, - range: Nv12Range, -) -> Option> { - let y_base = lock.base_address_of_plane(0); - let uv_base = lock.base_address_of_plane(1); - if y_base.is_null() || uv_base.is_null() { - warn!("NV12 plane base address was null"); - return None; - } - - let y_stride = lock.bytes_per_row_of_plane(0); - let uv_stride = lock.bytes_per_row_of_plane(1); - if y_stride == 0 || uv_stride == 0 { - warn!(y_stride, uv_stride, "NV12 plane bytes per row was zero"); - return None; - } - - let y_plane_height = lock.height_of_plane(0); - let uv_plane_height = lock.height_of_plane(1); - if y_plane_height < height || uv_plane_height < height.div_ceil(2) { - warn!( - y_plane_height, - uv_plane_height, - expected_y = height, - expected_uv = height.div_ceil(2), - "NV12 plane height smaller than expected", - ); - return None; - } - - let y_plane = unsafe { std::slice::from_raw_parts(y_base, y_stride * y_plane_height) }; - let uv_plane = unsafe { std::slice::from_raw_parts(uv_base, uv_stride * uv_plane_height) }; - - let mut rgba_data = vec![0u8; width * height * 4]; - - for y_idx in 0..height { - let y_row_start = y_idx * y_stride; - if y_row_start + width > y_plane.len() { - warn!( - y_row_start, - width, - y_plane_len = y_plane.len(), - "Y row exceeded plane length during conversion", - ); - return None; - } - let y_row = &y_plane[y_row_start..y_row_start + width]; - - let uv_row_start = (y_idx / 2) * uv_stride; - if uv_row_start + width > uv_plane.len() { - warn!( - uv_row_start, - width, - uv_plane_len = uv_plane.len(), - "UV row exceeded plane length during conversion", - ); - return None; - } - let uv_row = &uv_plane[uv_row_start..uv_row_start + width]; - - for (x, y_val) in y_row.iter().enumerate().take(width) { - let uv_index = (x / 2) * 2; - if uv_index + 1 >= uv_row.len() { - warn!( - uv_index, - uv_row_len = uv_row.len(), - "UV index out of bounds during conversion", - ); - return None; - } - - let cb = uv_row[uv_index]; - let cr = uv_row[uv_index + 1]; - let (r, g, b) = ycbcr_to_rgb(*y_val, cb, cr, range); - let out = (y_idx * width + x) * 4; - rgba_data[out] = r; - rgba_data[out + 1] = g; - rgba_data[out + 2] = b; - rgba_data[out + 3] = 255; - } - } - - Some(rgba_data) -} - -fn ycbcr_to_rgb(y: u8, cb: u8, cr: u8, range: Nv12Range) -> (u8, u8, u8) { - let y = y as f32; - let cb = cb as f32 - 128.0; - let cr = cr as f32 - 128.0; - - let (y_value, scale) = match range { - Nv12Range::Video => ((y - 16.0).max(0.0), 1.164383_f32), - Nv12Range::_Full => (y, 1.0_f32), - }; - - let r = scale * y_value + 1.596027_f32 * cr; - let g = scale * y_value - 0.391762_f32 * cb - 0.812968_f32 * cr; - let b = scale * y_value + 2.017232_f32 * cb; - - (clamp_channel(r), clamp_channel(g), clamp_channel(b)) -} - -fn clamp_channel(value: f32) -> u8 { - value.clamp(0.0, 255.0) as u8 -} - -struct PixelBufferLock<'a> { - buffer: &'a mut cidre::cv::PixelBuf, - flags: cidre::cv::pixel_buffer::LockFlags, -} - -impl<'a> PixelBufferLock<'a> { - fn new( - buffer: &'a mut cidre::cv::PixelBuf, - flags: cidre::cv::pixel_buffer::LockFlags, - ) -> cidre::os::Result { - unsafe { buffer.lock_base_addr(flags) }.result()?; - Ok(Self { buffer, flags }) - } - - fn base_address(&self) -> *const u8 { - unsafe { cv_pixel_buffer_get_base_address(self.buffer) as *const u8 } - } - - fn bytes_per_row(&self) -> usize { - unsafe { cv_pixel_buffer_get_bytes_per_row(self.buffer) } - } - - fn base_address_of_plane(&self, plane_index: usize) -> *const u8 { - unsafe { cv_pixel_buffer_get_base_address_of_plane(self.buffer, plane_index) as *const u8 } - } - - fn bytes_per_row_of_plane(&self, plane_index: usize) -> usize { - unsafe { cv_pixel_buffer_get_bytes_per_row_of_plane(self.buffer, plane_index) } - } - - fn height_of_plane(&self, plane_index: usize) -> usize { - unsafe { cv_pixel_buffer_get_height_of_plane(self.buffer, plane_index) } - } -} - -impl Drop for PixelBufferLock<'_> { - fn drop(&mut self) { - unsafe { - let _ = self.buffer.unlock_lock_base_addr(self.flags); - } - } -} - -unsafe fn cv_pixel_buffer_get_base_address(buffer: &cidre::cv::PixelBuf) -> *mut std::ffi::c_void { - unsafe extern "C" { - fn CVPixelBufferGetBaseAddress(pixel_buffer: &cidre::cv::PixelBuf) - -> *mut std::ffi::c_void; - } - - unsafe { CVPixelBufferGetBaseAddress(buffer) } -} - -unsafe fn cv_pixel_buffer_get_bytes_per_row(buffer: &cidre::cv::PixelBuf) -> usize { - unsafe extern "C" { - fn CVPixelBufferGetBytesPerRow(pixel_buffer: &cidre::cv::PixelBuf) -> usize; - } - - unsafe { CVPixelBufferGetBytesPerRow(buffer) } -} - -unsafe fn cv_pixel_buffer_get_base_address_of_plane( - buffer: &cidre::cv::PixelBuf, - plane_index: usize, -) -> *mut std::ffi::c_void { - unsafe extern "C" { - fn CVPixelBufferGetBaseAddressOfPlane( - pixel_buffer: &cidre::cv::PixelBuf, - plane_index: usize, - ) -> *mut std::ffi::c_void; - } - - unsafe { CVPixelBufferGetBaseAddressOfPlane(buffer, plane_index) } -} - -unsafe fn cv_pixel_buffer_get_bytes_per_row_of_plane( - buffer: &cidre::cv::PixelBuf, - plane_index: usize, -) -> usize { - unsafe extern "C" { - fn CVPixelBufferGetBytesPerRowOfPlane( - pixel_buffer: &cidre::cv::PixelBuf, - plane_index: usize, - ) -> usize; - } - - unsafe { CVPixelBufferGetBytesPerRowOfPlane(buffer, plane_index) } -} - -unsafe fn cv_pixel_buffer_get_height_of_plane( - buffer: &cidre::cv::PixelBuf, - plane_index: usize, -) -> usize { - unsafe extern "C" { - fn CVPixelBufferGetHeightOfPlane( - pixel_buffer: &cidre::cv::PixelBuf, - plane_index: usize, - ) -> usize; - } - - unsafe { CVPixelBufferGetHeightOfPlane(buffer, plane_index) } -} diff --git a/apps/desktop/src-tauri/src/thumbnails/mod.rs b/apps/desktop/src-tauri/src/thumbnails/mod.rs deleted file mode 100644 index 085ed5e2509..00000000000 --- a/apps/desktop/src-tauri/src/thumbnails/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -use cap_recording::sources::screen_capture::{list_displays, list_windows}; -use serde::{Deserialize, Serialize}; -use specta::Type; -use tracing::*; - -#[cfg(windows)] -mod windows; -#[cfg(windows)] -pub use windows::*; - -#[cfg(target_os = "macos")] -mod mac; -#[cfg(target_os = "macos")] -pub use mac::*; - -#[cfg(target_os = "linux")] -mod linux; -#[cfg(target_os = "linux")] -pub use linux::*; - -const THUMBNAIL_WIDTH: u32 = 320; -const THUMBNAIL_HEIGHT: u32 = 180; - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct CaptureDisplayWithThumbnail { - pub id: scap_targets::DisplayId, - pub name: String, - pub refresh_rate: u32, - pub thumbnail: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct CaptureWindowWithThumbnail { - pub id: scap_targets::WindowId, - pub owner_name: String, - pub name: String, - pub bounds: scap_targets::bounds::LogicalBounds, - pub refresh_rate: u32, - pub thumbnail: Option, - pub app_icon: Option, - pub bundle_identifier: Option, -} - -pub fn normalize_thumbnail_dimensions(image: &image::RgbaImage) -> image::RgbaImage { - let width = image.width(); - let height = image.height(); - - if width == THUMBNAIL_WIDTH && height == THUMBNAIL_HEIGHT { - return image.clone(); - } - - if width == 0 || height == 0 { - return image::RgbaImage::from_pixel( - THUMBNAIL_WIDTH, - THUMBNAIL_HEIGHT, - image::Rgba([0, 0, 0, 0]), - ); - } - - let scale = (THUMBNAIL_WIDTH as f32 / width as f32) - .min(THUMBNAIL_HEIGHT as f32 / height as f32) - .max(f32::MIN_POSITIVE); - - let scaled_width = (width as f32 * scale) - .round() - .clamp(1.0, THUMBNAIL_WIDTH as f32) as u32; - let scaled_height = (height as f32 * scale) - .round() - .clamp(1.0, THUMBNAIL_HEIGHT as f32) as u32; - - let resized = image::imageops::resize( - image, - scaled_width.max(1), - scaled_height.max(1), - image::imageops::FilterType::Lanczos3, - ); - - let mut canvas = - image::RgbaImage::from_pixel(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, image::Rgba([0, 0, 0, 0])); - - let offset_x = (THUMBNAIL_WIDTH - scaled_width) / 2; - let offset_y = (THUMBNAIL_HEIGHT - scaled_height) / 2; - - image::imageops::overlay(&mut canvas, &resized, offset_x as i64, offset_y as i64); - - canvas -} - -pub async fn collect_displays_with_thumbnails() -> Result, String> -{ - let displays = list_displays(); - - let mut results = Vec::new(); - for (capture_display, display) in displays { - let thumbnail = capture_display_thumbnail(&display).await; - results.push(CaptureDisplayWithThumbnail { - id: capture_display.id, - name: capture_display.name, - refresh_rate: capture_display.refresh_rate, - thumbnail, - }); - } - - Ok(results) -} - -pub async fn collect_windows_with_thumbnails() -> Result, String> { - let windows = list_windows(); - - let mut results = Vec::new(); - for (capture_window, window) in windows { - let thumbnail = capture_window_thumbnail(&window).await; - let app_icon = window.app_icon().and_then(|bytes| { - if bytes.is_empty() { - None - } else { - Some(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - bytes, - )) - } - }); - - results.push(CaptureWindowWithThumbnail { - id: capture_window.id, - name: capture_window.name, - owner_name: capture_window.owner_name, - bounds: capture_window.bounds, - refresh_rate: capture_window.refresh_rate, - thumbnail, - app_icon, - bundle_identifier: capture_window.bundle_identifier, - }); - } - - Ok(results) -} diff --git a/apps/desktop/src-tauri/src/thumbnails/windows.rs b/apps/desktop/src-tauri/src/thumbnails/windows.rs deleted file mode 100644 index dde0fae69aa..00000000000 --- a/apps/desktop/src-tauri/src/thumbnails/windows.rs +++ /dev/null @@ -1,287 +0,0 @@ -use super::*; - -pub async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option { - use image::{ColorType, ImageEncoder, codecs::png::PngEncoder}; - use scap_direct3d::{Capturer, Settings}; - use std::io::Cursor; - - let item = display.raw_handle().try_as_capture_item().ok()?; - - let (tx, rx) = std::sync::mpsc::channel(); - - let mut settings = Settings { - is_cursor_capture_enabled: Some(false), - pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm, - ..Default::default() - }; - - if let Ok(true) = Settings::can_is_border_required() { - settings.is_border_required = Some(false); - } - - let mut capturer = Capturer::new( - item, - settings.clone(), - move |frame| { - let _ = tx.send(frame); - Ok(()) - }, - || Ok(()), - None, - ) - .ok()?; - - capturer.start().ok()?; - - let frame = rx.recv_timeout(std::time::Duration::from_secs(2)).ok()?; - let _ = capturer.stop(); - - let width = frame.width(); - let height = frame.height(); - - if width == 0 || height == 0 { - return None; - } - - let frame_buffer = frame.as_buffer().ok()?; - let data = frame_buffer.data(); - let stride = frame_buffer.stride() as usize; - - let width_usize = width as usize; - let height_usize = height as usize; - - let Some(row_bytes) = width_usize.checked_mul(4) else { - warn!( - frame_width = width, - "Windows display thumbnail row size overflowed" - ); - return None; - }; - - if stride < row_bytes { - warn!( - frame_width = width, - frame_height = height, - stride, - expected_row_bytes = row_bytes, - "Windows display thumbnail stride smaller than row size" - ); - return None; - } - - let rows_before_last = height_usize.saturating_sub(1); - let Some(last_row_start) = rows_before_last.checked_mul(stride) else { - warn!( - frame_width = width, - frame_height = height, - stride, - "Windows display thumbnail row offset overflowed" - ); - return None; - }; - - let Some(required_len) = last_row_start.checked_add(row_bytes) else { - warn!( - frame_width = width, - frame_height = height, - stride, - required_row_bytes = row_bytes, - "Windows display thumbnail required length overflowed" - ); - return None; - }; - - if data.len() < required_len { - warn!( - frame_width = width, - frame_height = height, - stride, - frame_data_len = data.len(), - expected_len = required_len, - "Windows display thumbnail frame buffer missing pixel data" - ); - return None; - } - - let Some(rgba_capacity) = height_usize.checked_mul(row_bytes) else { - warn!( - frame_width = width, - frame_height = height, - total_row_bytes = row_bytes, - "Windows display thumbnail RGBA capacity overflowed" - ); - return None; - }; - - let mut rgba_data = Vec::with_capacity(rgba_capacity); - for y in 0..height_usize { - let row_start = y * stride; - let row_end = row_start + row_bytes; - rgba_data.extend_from_slice(&data[row_start..row_end]); - } - - let Some(img) = image::RgbaImage::from_raw(width, height, rgba_data) else { - warn!("Windows display thumbnail failed to construct RGBA image"); - return None; - }; - let thumbnail = normalize_thumbnail_dimensions(&img); - - let mut png_data = Cursor::new(Vec::new()); - let encoder = PngEncoder::new(&mut png_data); - encoder - .write_image( - thumbnail.as_raw(), - thumbnail.width(), - thumbnail.height(), - ColorType::Rgba8.into(), - ) - .ok()?; - - Some(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - png_data.into_inner(), - )) -} - -pub async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option { - use image::{ColorType, ImageEncoder, codecs::png::PngEncoder}; - use scap_direct3d::{Capturer, Settings}; - use std::io::Cursor; - - let item = window.raw_handle().try_as_capture_item().ok()?; - - let (tx, rx) = std::sync::mpsc::channel(); - - let mut settings = Settings { - is_cursor_capture_enabled: Some(false), - pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm, - ..Default::default() - }; - - if let Ok(true) = Settings::can_is_border_required() { - settings.is_border_required = Some(false); - } - - let mut capturer = Capturer::new( - item, - settings.clone(), - move |frame| { - let _ = tx.send(frame); - Ok(()) - }, - || Ok(()), - None, - ) - .ok()?; - - capturer.start().ok()?; - - let frame = rx.recv_timeout(std::time::Duration::from_secs(2)).ok()?; - let _ = capturer.stop(); - - let width = frame.width(); - let height = frame.height(); - - if width == 0 || height == 0 { - return None; - } - - let frame_buffer = frame.as_buffer().ok()?; - let data = frame_buffer.data(); - let stride = frame_buffer.stride() as usize; - - let width_usize = width as usize; - let height_usize = height as usize; - - let Some(row_bytes) = width_usize.checked_mul(4) else { - warn!( - frame_width = width, - "Windows window thumbnail row size overflowed" - ); - return None; - }; - - if stride < row_bytes { - warn!( - frame_width = width, - frame_height = height, - stride, - expected_row_bytes = row_bytes, - "Windows window thumbnail stride smaller than row size" - ); - return None; - } - - let rows_before_last = height_usize.saturating_sub(1); - let Some(last_row_start) = rows_before_last.checked_mul(stride) else { - warn!( - frame_width = width, - frame_height = height, - stride, - "Windows window thumbnail row offset overflowed" - ); - return None; - }; - - let Some(required_len) = last_row_start.checked_add(row_bytes) else { - warn!( - frame_width = width, - frame_height = height, - stride, - required_row_bytes = row_bytes, - "Windows window thumbnail required length overflowed" - ); - return None; - }; - - if data.len() < required_len { - warn!( - frame_width = width, - frame_height = height, - stride, - frame_data_len = data.len(), - expected_len = required_len, - "Windows window thumbnail frame buffer missing pixel data" - ); - return None; - } - - let Some(rgba_capacity) = height_usize.checked_mul(row_bytes) else { - warn!( - frame_width = width, - frame_height = height, - total_row_bytes = row_bytes, - "Windows window thumbnail RGBA capacity overflowed" - ); - return None; - }; - - let mut rgba_data = Vec::with_capacity(rgba_capacity); - for y in 0..height_usize { - let row_start = y * stride; - let row_end = row_start + row_bytes; - rgba_data.extend_from_slice(&data[row_start..row_end]); - } - - let Some(img) = image::RgbaImage::from_raw(width, height, rgba_data) else { - warn!("Windows window thumbnail failed to construct RGBA image"); - return None; - }; - let thumbnail = normalize_thumbnail_dimensions(&img); - - let mut png_data = Cursor::new(Vec::new()); - let encoder = PngEncoder::new(&mut png_data); - encoder - .write_image( - thumbnail.as_raw(), - thumbnail.width(), - thumbnail.height(), - ColorType::Rgba8.into(), - ) - .ok()?; - - Some(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - png_data.into_inner(), - )) -} diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs deleted file mode 100644 index 562ac852c24..00000000000 --- a/apps/desktop/src-tauri/src/tray.rs +++ /dev/null @@ -1,1236 +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 { - Instant, - Screenshot, - Studio, - Stop, -} - -#[cfg(target_os = "linux")] -impl LinuxTrayIcon { - fn name(self) -> &'static str { - match self { - 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::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, -} - -#[derive(Default)] -struct PreviousItemsCache { - items: Vec, - revision: u64, - loaded: bool, -} - -impl PreviousItemsCache { - fn invalidate(&mut self) { - self.revision = self.revision.wrapping_add(1); - } - - fn finish_loading(&mut self, revision: u64, items: Vec) -> bool { - if self.revision != revision { - return false; - } - self.items = items; - self.loaded = true; - self.invalidate(); - true - } -} - -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 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, - }) -} - -fn load_all_previous_items(app: &AppHandle, load_thumbnails: bool) -> Vec { - let mut paths = 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() { - paths.push(entry.path()); - } - } - - 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") { - paths.push(path); - } - } - } - - newest_valid_items(paths, MAX_PREVIOUS_ITEMS, |path| { - load_single_item(path, &screenshots_dir, load_thumbnails) - }) -} - -fn newest_valid_items( - paths: Vec, - limit: usize, - mut load: impl FnMut(&PathBuf) -> Option, -) -> Vec { - let mut candidates = paths - .into_iter() - .filter_map(|path| { - let metadata = path.metadata().ok()?; - metadata.is_dir().then(|| { - let created = metadata - .created() - .unwrap_or_else(|_| std::time::SystemTime::now()); - (path, created) - }) - }) - .collect::>(); - candidates.sort_by_key(|(_, created)| std::cmp::Reverse(*created)); - candidates - .into_iter() - .filter_map(|(path, _)| load(&path)) - .take(limit) - .collect() -} - -fn load_previous_items_in_background(app: AppHandle, cache: Arc>) { - tokio::spawn(async move { - if !crate::startup::wait_for_window(&app).await { - return; - } - let worker_app = app.clone(); - let worker_cache = cache.clone(); - let result = tokio::task::spawn_blocking(move || { - while !crate::app_is_exiting(&worker_app) { - let revision = worker_cache.lock().unwrap().revision; - let items = load_all_previous_items(&worker_app, true); - if worker_cache.lock().unwrap().finish_loading(revision, items) { - return; - } - } - }) - .await; - if let Err(error) = result { - tracing::warn!(%error, "Failed to load tray history"); - } - if !crate::app_is_exiting(&app) { - refresh_tray_menu(&app, &cache); - } - }); -} - -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", - if cache.loaded { - "No recent items" - } else { - "Loading 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); - cache_guard.invalidate(); -} - -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 cache = Arc::new(Mutex::new(PreviousItemsCache::default())); - - 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(); - tauri::async_runtime::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 { button_state, .. } = event { - if is_recording.load(Ordering::Relaxed) { - if button_state != tauri::tray::MouseButtonState::Down { - return; - } - 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}"); - } - - 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 { - cache_clone.lock().unwrap().invalidate(); - load_previous_items_in_background(app_handle.clone(), cache_clone.clone()); - } - } - }); - - crate::RecordingDeleted::listen_any(&app, { - let app = app.clone(); - let cache = cache.clone(); - move |event| { - { - let mut cache = cache.lock().unwrap(); - cache.items.retain(|item| item.path != event.payload.path); - cache.invalidate(); - } - refresh_tray_menu(&app, &cache); - load_previous_items_in_background(app.clone(), cache.clone()); - } - }); - - load_previous_items_in_background(app.clone(), cache.clone()); - - Ok(()) -} - -#[cfg(target_os = "linux")] -pub(crate) fn clean_stop_icon() -> Result { - let image = image::load_from_memory(include_bytes!("../icons/tray-stop-icon.png")) - .map_err(|error| error.to_string())? - .resize_exact(32, 32, image::imageops::FilterType::Triangle) - .into_rgba8(); - cap_utils::linux_recording_stop::StopTrayIcon::from_rgba(32, 32, image.as_raw()) -} - -#[cfg(test)] -mod startup_history_tests { - use super::*; - - #[test] - fn history_load_reads_only_enough_valid_metadata_for_the_menu() { - let directory = tempfile::tempdir().unwrap(); - let paths = (0..100) - .map(|index| { - let path = directory.path().join(format!("{index}.cap")); - std::fs::create_dir(&path).unwrap(); - path - }) - .collect::>(); - let mut reads = 0; - let loaded = newest_valid_items(paths.clone(), 6, |_| { - reads += 1; - (reads > 2).then_some(reads) - }); - assert_eq!(reads, 8); - assert_eq!(loaded, vec![3, 4, 5, 6, 7, 8]); - - let mut reference = paths.clone(); - reference - .sort_by_key(|path| std::cmp::Reverse(path.metadata().unwrap().created().unwrap())); - reference.truncate(6); - assert_eq!( - newest_valid_items(paths, 6, |path| Some(path.clone())), - reference - ); - } - - #[test] - fn a_late_history_scan_cannot_overwrite_new_items_or_moved_folders() { - let mut cache = PreviousItemsCache::default(); - let old_revision = cache.revision; - cache.invalidate(); - assert!(!cache.finish_loading(old_revision, Vec::new())); - assert!(!cache.loaded); - assert!(cache.finish_loading(cache.revision, Vec::new())); - assert!(cache.loaded); - assert!(!cache.finish_loading(old_revision, Vec::new())); - } -} - -#[cfg(target_os = "linux")] -pub(crate) async fn set_clean_stop_mode( - app: &AppHandle, - generation: u32, - active: bool, -) -> Result<(), String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let result = if crate::clean_capture::wayland_generation(&handle) != Some(generation) { - Err("Recording tray transition was superseded".to_string()) - } else if let Some(tray) = handle.tray_by_id("tray") { - tray.set_visible(!active).map_err(|error| error.to_string()) - } else { - Ok(()) - }; - let _ = tx.send(result); - }) - .map_err(|error| error.to_string())?; - tokio::time::timeout(std::time::Duration::from_secs(2), rx) - .await - .map_err(|_| "Recording tray transition timed out".to_string())? - .map_err(|_| "Recording tray transition was lost".to_string())? -} diff --git a/apps/desktop/src-tauri/src/update_project_names.rs b/apps/desktop/src-tauri/src/update_project_names.rs deleted file mode 100644 index 8d008617064..00000000000 --- a/apps/desktop/src-tauri/src/update_project_names.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::{ - collections::HashSet, - path::{Path, PathBuf}, - sync::Arc, -}; - -use cap_project::RecordingMeta; -use futures::StreamExt; -use tauri::AppHandle; -use tauri_plugin_store::StoreExt; -use tokio::{fs, sync::Mutex}; - -use crate::recordings_path; - -const STORE_KEY: &str = "uuid_projects_migrated"; - -pub fn migrate_if_needed(app: &AppHandle) -> Result<(), String> { - let store = app - .store("store") - .map_err(|e| format!("Failed to access store: {e}"))?; - - if store - .get(STORE_KEY) - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - return Ok(()); - } - - let app_handle = app.clone(); - tokio::spawn(async move { - if let Err(err) = migrate(&app_handle).await { - tracing::error!("Updating project names failed: {err}"); - } - - if let Ok(store) = app_handle.store("store") { - store.set(STORE_KEY, true); - if let Err(e) = store.save() { - tracing::error!("Failed to save store after migration: {}", e); - } - } - }); - - Ok(()) -} - -use std::time::Instant; - -/// Performs a one-time migration of all UUID-named projects to pretty name-based naming. -pub async fn migrate(app: &AppHandle) -> Result<(), String> { - let recordings_dir = recordings_path(app); - if !fs::try_exists(&recordings_dir) - .await - .map_err(|e| format!("Failed to check recordings directory: {e}"))? - { - return Ok(()); - } - - let uuid_projects = collect_uuid_projects(&recordings_dir).await?; - if uuid_projects.is_empty() { - tracing::debug!("No UUID-named projects found to migrate"); - return Ok(()); - } - - tracing::info!( - "Found {} UUID-named projects to migrate", - uuid_projects.len() - ); - - let total_found = uuid_projects.len(); - let concurrency_limit = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(4) - .clamp(2, 16) - .min(total_found); - tracing::debug!("Using concurrency limit of {}", concurrency_limit); - - let wall_start = Instant::now(); - let in_flight_bases = Arc::new(Mutex::new(HashSet::new())); - - // (project_name, result, duration) - let migration_results = futures::stream::iter(uuid_projects) - .map(|project_path| { - let in_flight = in_flight_bases.clone(); - async move { - let project_name = project_path - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| project_path.display().to_string()); - - let start = Instant::now(); - let res = migrate_single_project(project_path, in_flight).await; - let dur = start.elapsed(); - - (project_name, res, dur) - } - }) - .buffer_unordered(concurrency_limit) - .collect::>() - .await; - - let wall_elapsed = wall_start.elapsed(); - - let mut migrated = 0usize; - let mut skipped = 0usize; - let mut failed = 0usize; - - let mut total_ms: u128 = 0; - let mut per_project: Vec<(String, std::time::Duration)> = - Vec::with_capacity(migration_results.len()); - - for (name, result, dur) in migration_results.into_iter() { - match result { - Ok(ProjectMigrationResult::Migrated) => migrated += 1, - Ok(ProjectMigrationResult::Skipped) => skipped += 1, - Err(_) => failed += 1, - } - total_ms += dur.as_millis(); - per_project.push((name, dur)); - } - - let avg_ms = if total_found > 0 { - (total_ms as f64) / (total_found as f64) - } else { - 0.0 - }; - - per_project.sort_by_key(|b| std::cmp::Reverse(b.1)); - - tracing::info!( - total_found = total_found, - migrated = migrated, - skipped = skipped, - failed = failed, - wall_ms = wall_elapsed.as_millis(), - avg_per_project_ms = ?avg_ms, - "Migration complete" - ); - - // Log top slowest N (choose 5 or less) - let top_n = 5.min(per_project.len()); - if top_n > 0 { - tracing::info!("Top {} slowest project migrations:", top_n); - for (name, dur) in per_project.into_iter().take(top_n) { - tracing::info!(project = %name, ms = dur.as_millis()); - } - } - - Ok(()) -} - -async fn collect_uuid_projects(recordings_dir: &Path) -> Result, String> { - let mut uuid_projects = Vec::new(); - let mut entries = fs::read_dir(recordings_dir) - .await - .map_err(|e| format!("Failed to read recordings directory: {e}"))?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|e| format!("Failed to read directory entry: {e}"))? - { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let Some(filename) = path.file_name().and_then(|s| s.to_str()) else { - continue; - }; - - if filename.ends_with(".cap") && fast_is_project_filename_uuid(filename) { - uuid_projects.push(path); - } - } - - Ok(uuid_projects) -} - -#[derive(Debug)] -enum ProjectMigrationResult { - Migrated, - Skipped, -} - -async fn migrate_single_project( - path: PathBuf, - in_flight_basis: Arc>>, -) -> Result { - let filename = path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("unknown"); - - let meta = match RecordingMeta::load_for_project(&path) { - Ok(meta) => meta, - Err(e) => { - tracing::warn!("Failed to load metadata for {}: {}", filename, e); - return Err(format!("Failed to load metadata: {e}")); - } - }; - - // Lock on the base sanitized name to prevent concurrent migrations with same target - let base_name = sanitize_filename::sanitize(meta.pretty_name.replace(":", ".")); - { - let mut in_flight = in_flight_basis.lock().await; - let mut wait_count = 0; - while !in_flight.insert(base_name.clone()) { - wait_count += 1; - if wait_count == 1 { - tracing::debug!( - "Project {} waiting for concurrent migration of base name \"{}\"", - filename, - base_name - ); - } - drop(in_flight); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - in_flight = in_flight_basis.lock().await; - } - if wait_count > 0 { - tracing::debug!( - "Project {} acquired lock for \"{}\" after {} waits", - filename, - base_name, - wait_count - ); - } - } - - let result = migrate_project_filename_async(&path, &meta).await; - - in_flight_basis.lock().await.remove(&base_name); - - match result { - Ok(new_path) => { - if new_path != path { - let new_name = new_path.file_name().unwrap().to_string_lossy(); - tracing::info!("Updated name: \"{}\" -> \"{}\"", filename, new_name); - Ok(ProjectMigrationResult::Migrated) - } else { - Ok(ProjectMigrationResult::Skipped) - } - } - Err(e) => { - tracing::error!("Failed to migrate {}: {}", filename, e); - Err(e) - } - } -} - -/// Migrates a project filename from UUID to sanitized pretty name -async fn migrate_project_filename_async( - project_path: &Path, - meta: &RecordingMeta, -) -> Result { - let sanitized = sanitize_filename::sanitize(meta.pretty_name.replace(":", ".")); - - let filename = if sanitized.ends_with(".cap") { - sanitized - } else { - format!("{sanitized}.cap") - }; - - let parent_dir = project_path - .parent() - .ok_or("Project path has no parent directory")?; - - let unique_filename = cap_utils::ensure_unique_filename(&filename, parent_dir) - .map_err(|e| format!("Failed to ensure unique filename: {e}"))?; - - let final_path = parent_dir.join(&unique_filename); - - fs::rename(project_path, &final_path) - .await - .map_err(|e| format!("Failed to rename project directory: {e}"))?; - - Ok(final_path) -} - -pub fn fast_is_project_filename_uuid(filename: &str) -> bool { - if filename.len() != 40 || !filename.ends_with(".cap") { - return false; - } - - let uuid_part = &filename[..36]; - - if uuid_part.as_bytes()[8] != b'-' - || uuid_part.as_bytes()[13] != b'-' - || uuid_part.as_bytes()[18] != b'-' - || uuid_part.as_bytes()[23] != b'-' - { - return false; - } - - uuid_part.chars().all(|c| c.is_ascii_hexdigit() || c == '-') -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_project_filename_uuid() { - // Valid UUID - assert!(fast_is_project_filename_uuid( - "a1b2c3d4-e5f6-7890-abcd-ef1234567890.cap" - )); - assert!(fast_is_project_filename_uuid( - "00000000-0000-0000-0000-000000000000.cap" - )); - - // Invalid cases - assert!(!fast_is_project_filename_uuid("my-project-name.cap")); - assert!(!fast_is_project_filename_uuid( - "a1b2c3d4-e5f6-7890-abcd-ef1234567890" - )); - assert!(!fast_is_project_filename_uuid( - "a1b2c3d4-e5f6-7890-abcd-ef1234567890.txt" - )); - assert!(!fast_is_project_filename_uuid( - "g1b2c3d4-e5f6-7890-abcd-ef1234567890.cap" - )); - } -} diff --git a/apps/desktop/src-tauri/src/updates.rs b/apps/desktop/src-tauri/src/updates.rs deleted file mode 100644 index da4ccf8d8d3..00000000000 --- a/apps/desktop/src-tauri/src/updates.rs +++ /dev/null @@ -1,423 +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::{ - exit_shutdown::{ExitBlocked, UpdateInstallGuard, UpdateInstallState}, - 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); -const UPDATE_BUSY_ERROR: &str = - "Finish your recording, export, or upload before updating or restarting Cap."; - -#[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>, - install: Mutex<()>, - install_phase: UpdateInstallState, - notify: Notify, -} - -impl UpdatesState { - pub(crate) fn is_installing(&self) -> bool { - self.install_phase.is_installing() - } -} - -pub(crate) fn recording_start_blocked(app: &AppHandle) -> bool { - app.try_state::() - .is_none_or(|state| state.install_phase.blocks_recording()) -} - -fn begin_install<'a>( - app: &AppHandle, - state: &'a UpdatesState, -) -> Result, String> { - crate::with_idle_app(app, true, || { - state - .install_phase - .begin() - .ok_or(ExitBlocked::UpdateInstalling) - }) - .and_then(std::convert::identity) - .map_err(|reason| reason.message().to_string()) -} - -fn current_channel(app: &AppHandle) -> UpdateChannel { - GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|s| s.update_channel) - .unwrap_or_default() -} - -fn updater_target() -> Result { - let arch = if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - "x86_64" - }; - - #[cfg(target_os = "linux")] - { - cap_utils::linux_package::updater_target(arch) - } - #[cfg(not(target_os = "linux"))] - { - let platform = if cfg!(target_os = "macos") { - "darwin" - } else { - "windows" - }; - Ok(format!("{platform}-{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()) -} - -fn is_busy(app: &AppHandle) -> bool { - crate::with_idle_app(app, true, || ()).is_err() -} - -#[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 _install = state.install.lock().await; - - if is_busy(&app) { - return Err(UPDATE_BUSY_ERROR.to_string()); - } - - 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?; - - let install_admission = begin_install(&app, &state)?; - info!("Installing update {}", pending.version); - pending.update.install(bytes).map_err(|e| e.to_string())?; - install_admission.complete(cfg!(windows)); - - 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) { - 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 _install = state.install.lock().await; - if is_busy(&app) { - delay = BUSY_RETRY_DELAY; - continue; - } - 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. - let install_admission = match begin_install(&app, &state) { - Ok(admission) => admission, - Err(_) => { - 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; - } - install_admission.complete(false); - - 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/upload.rs b/apps/desktop/src-tauri/src/upload.rs deleted file mode 100644 index be19c9d61db..00000000000 --- a/apps/desktop/src-tauri/src/upload.rs +++ /dev/null @@ -1,5511 +0,0 @@ -// credit @filleduchaos - -use crate::{ - UploadProgress, VideoUploadInfo, - api::{self, PresignedS3PutRequest, PresignedS3PutRequestMethod, S3VideoMeta, UploadedPart}, - http_client::{HttpClient, RetryableHttpClient}, - telemetry::{AnalyticsEvent, async_capture_event}, - web_api::{AuthedApiError, ManagerExt}, -}; -use async_stream::{stream, try_stream}; -use bytes::Bytes; -use cap_project::{RecordingMeta, S3UploadMeta, UploadMeta}; -use cap_recording::{ - upload_resume::UploadLock, - upload_verification::{UploadVerification, VerifiedUploadReceipt}, -}; -use cap_utils::spawn_actor; -use ffmpeg::ffi::AV_TIME_BASE; -use flume::Receiver; -use futures::future::join; -use futures::{Stream, StreamExt, TryStreamExt, stream}; -use image::{ImageReader, codecs::jpeg::JpegEncoder}; -use reqwest::StatusCode; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::{ - collections::HashMap, - io, - path::{Path, PathBuf}, - pin::pin, - sync::{ - Arc, Mutex, PoisonError, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, -}; -use tauri::{AppHandle, Manager, ipc::Channel}; -use tauri_specta::Event; -use tokio::{ - fs::File, - io::{AsyncReadExt, BufReader}, - task::{self, JoinHandle}, - time::{self, Instant, timeout}, -}; -use tokio_util::io::ReaderStream; -use tracing::{Span, debug, error, info, info_span, instrument, trace, warn}; - -pub(crate) mod lifecycle; -pub(crate) mod preparation; -pub(crate) mod recovery_age; -pub(crate) mod resume; -use tracing_futures::Instrument; - -pub(crate) fn acquire_upload_lock(recording_dir: &Path) -> Result { - let lock = UploadLock::acquire(recording_dir).map_err(|error| error.to_string())?; - match std::fs::symlink_metadata(recording_dir.join("instant-upload.json")) { - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(lock), - Ok(_) => Err("This recording's upload is managed by Cap GPUI; local files retained".into()), - Err(error) => Err(format!("Could not verify recording upload ownership: {error}").into()), - } -} - -async fn await_upload_verification( - app: &AppHandle, - video_id: &str, - verification: &UploadVerification, - session: &lifecycle::Session, -) -> Result { - session.set_verification(verification)?; - let started = Instant::now(); - while started.elapsed() < Duration::from_secs(10 * 60) { - session.check()?; - let result = tokio::select! { - result = api::verify_recording_complete(app, video_id, verification) => result, - _ = session.cancelled() => return Err("Upload verification cancelled; local files retained".into()), - }; - match result { - Ok(Some(receipt)) => { - session.record_receipt(verification, &receipt)?; - return Ok(receipt); - } - Err(error @ AuthedApiError::ReuploadRequired) => { - session.clear_verification()?; - return Err(error); - } - Ok(None) => {} - Err(error @ AuthedApiError::InvalidAuthentication) => return Err(error), - Err(error) => warn!(%error, "Recording verification pending; local files retained"), - } - tokio::select! { _ = tokio::time::sleep(Duration::from_secs(5)) => {}, _ = session.cancelled() => return Err("Upload verification cancelled; local files retained".into()) } - } - Err(AuthedApiError::VerificationPending) -} - -fn validate_uploaded_segment_inventory( - manifest: &SegmentUploadManifest, - expected: &[cap_enc_ffmpeg::segmented_stream::SegmentCompletedEvent], -) -> Result<(), AuthedApiError> { - use cap_enc_ffmpeg::segmented_stream::SegmentMediaType; - for media_type in [SegmentMediaType::Video, SegmentMediaType::Audio] { - let mut indices = expected - .iter() - .filter(|event| !event.is_init && event.media_type == media_type) - .map(|event| event.index) - .collect::>(); - indices.sort_unstable(); - let expected_init = expected - .iter() - .any(|event| event.is_init && event.media_type == media_type); - let (actual_init, actual_segments) = match media_type { - SegmentMediaType::Video => (manifest.video_init_uploaded, &manifest.video_segments), - SegmentMediaType::Audio => (manifest.audio_init_uploaded, &manifest.audio_segments), - }; - if actual_init != expected_init - || !actual_segments.iter().map(|entry| entry.index).eq(indices) - { - return Err("Uploaded segments do not cover the final recording inventory".into()); - } - } - Ok(()) -} - -pub struct UploadedItem { - pub link: String, - pub id: String, - pub object_identity: Option, - // #[allow(unused)] - // pub config: S3UploadMeta, -} - -#[derive(Clone, Serialize, Type, tauri_specta::Event)] -pub struct UploadProgressEvent { - video_id: String, - uploaded: String, - total: String, -} - -const MIN_CHUNK_SIZE: u64 = 5 * 1024 * 1024; -const MAX_CHUNK_SIZE: u64 = 15 * 1024 * 1024; -const NETWORK_RECOVERY_TIMEOUT: Duration = Duration::from_secs(5 * 60); -const CONNECTIVITY_PROBE_INITIAL_DELAY: Duration = Duration::from_secs(2); -const CONNECTIVITY_PROBE_MAX_DELAY: Duration = Duration::from_secs(30); - -static ACTIVE_UPLOADS: AtomicUsize = AtomicUsize::new(0); - -struct ActiveUploadGuard<'a>(&'a AtomicUsize); - -impl<'a> ActiveUploadGuard<'a> { - fn new(active: &'a AtomicUsize) -> Self { - active.fetch_add(1, Ordering::AcqRel); - Self(active) - } -} - -impl Drop for ActiveUploadGuard<'_> { - fn drop(&mut self) { - self.0.fetch_sub(1, Ordering::AcqRel); - } -} - -pub(crate) fn upload_session_active() -> bool { - ACTIVE_UPLOADS.load(Ordering::Acquire) > 0 -} - -fn is_google_drive_resumable_url(url: &str) -> bool { - let Ok(url) = reqwest::Url::parse(url) else { - return false; - }; - url.host_str().is_some_and(|host| { - (host == "googleapis.com" || host.ends_with(".googleapis.com")) - && url.path().starts_with("/upload/drive/") - }) -} - -fn is_google_drive_upload(provider: Option<&str>, upload_id: &str) -> bool { - provider == Some("googleDrive") || is_google_drive_resumable_url(upload_id) -} - -fn with_drive_content_range( - request: reqwest::RequestBuilder, - url: &str, - offset: u64, - size: u64, - total_size: u64, -) -> reqwest::RequestBuilder { - if !is_google_drive_resumable_url(url) || size == 0 { - return request; - } - - let end = offset.saturating_add(size).saturating_sub(1); - request.header( - "Content-Range", - format!("bytes {offset}-{end}/{total_size}"), - ) -} - -fn is_upload_response_accepted( - url: &str, - status: StatusCode, - offset: u64, - size: u64, - total_size: u64, -) -> bool { - status.is_success() - || (is_google_drive_resumable_url(url) - && status == StatusCode::PERMANENT_REDIRECT - && offset.saturating_add(size) < total_size) -} - -fn content_type_for_upload_subpath(subpath: &str) -> &'static str { - if subpath.ends_with(".json") { - "application/json" - } else if subpath.ends_with(".mp4") || subpath.ends_with(".m4s") { - "video/mp4" - } else if subpath.ends_with(".jpg") || subpath.ends_with(".jpeg") { - "image/jpeg" - } else if subpath.ends_with(".png") { - "image/png" - } else if subpath.ends_with(".aac") { - "audio/aac" - } else if subpath.ends_with(".webm") { - "audio/webm" - } else if subpath.ends_with(".m3u8") { - "application/x-mpegURL" - } else { - "application/octet-stream" - } -} - -pub fn reusable_video_id( - sharing: Option<&cap_project::SharingMeta>, - upload: Option<&cap_project::UploadMeta>, -) -> Option { - sharing - .map(|sharing| sharing.id.clone()) - .or_else(|| match upload { - Some(cap_project::UploadMeta::SinglePartUpload { video_id, .. }) - | Some(cap_project::UploadMeta::SegmentUpload { video_id, .. }) - | Some(cap_project::UploadMeta::MultipartUpload { video_id, .. }) => { - Some(video_id.clone()) - } - _ => None, - }) -} - -enum ReplacementAttempt { - Completed(T), - RestartRequired, -} - -async fn with_replacement_restart( - replace_existing: bool, - mut attempt: F, -) -> Result -where - F: FnMut(bool) -> Fut, - Fut: std::future::Future, AuthedApiError>>, -{ - lifecycle::cancellable(async {}).await?; - match attempt(false).await? { - ReplacementAttempt::Completed(value) => return Ok(value), - ReplacementAttempt::RestartRequired if replace_existing => {} - ReplacementAttempt::RestartRequired => { - return Err("Unexpected replacement restart response; local files retained".into()); - } - } - lifecycle::cancellable(async {}).await?; - match attempt(true).await? { - ReplacementAttempt::Completed(value) => Ok(value), - ReplacementAttempt::RestartRequired => { - Err("Replacement restart was rejected again; local files retained".into()) - } - } -} - -fn validate_replacement_restart_session( - restarted: bool, - upload_id: &str, -) -> Result<(), AuthedApiError> { - if restarted && !upload_id.starts_with("cap-reupload.") { - return Err( - "Replacement restart requires a signed upload session; local files retained".into(), - ); - } - Ok(()) -} - -#[derive(PartialEq, Eq)] -struct LegacyReplacementChunk { - offset: u64, - size: usize, - digest: md5::Digest, -} - -struct LegacyReplacementInput { - path: PathBuf, - size: u64, - modified: std::time::SystemTime, - chunks: Mutex>, -} - -impl LegacyReplacementInput { - async fn identity( - path: PathBuf, - ) -> Result<(PathBuf, u64, std::time::SystemTime), AuthedApiError> { - lifecycle::file_io(move || { - let metadata = std::fs::symlink_metadata(&path)?; - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - if metadata.file_attributes() & 0x400 != 0 { - return Err(io::Error::other("Replacement input is a reparse point")); - } - } - if !metadata.is_file() || metadata.file_type().is_symlink() || metadata.len() == 0 { - return Err(io::Error::other( - "Replacement input must be a nonempty regular file", - )); - } - Ok((path.canonicalize()?, metadata.len(), metadata.modified()?)) - }) - .await - .map_err(|error| error.to_string().into()) - } - - async fn capture(path: &Path) -> Result { - let (path, size, modified) = Self::identity(path.to_path_buf()).await?; - Ok(Self { - path, - size, - modified, - chunks: Mutex::new(HashMap::new()), - }) - } - - async fn verify(&self, path: &Path) -> Result<(), AuthedApiError> { - let (path, size, modified) = Self::identity(path.to_path_buf()).await?; - if path != self.path || size != self.size || modified != self.modified { - return Err("Replacement input changed during upload; local files retained".into()); - } - Ok(()) - } - - fn inspect_chunk(&self, chunk: &Chunk, record: bool) -> io::Result<()> { - if chunk.total_size != self.size - || chunk.chunk.is_empty() - || chunk.part_number == 0 - || chunk - .offset - .checked_add(chunk.chunk.len() as u64) - .is_none_or(|end| end > self.size) - { - return Err(io::Error::other("Replacement upload byte range changed")); - } - let receipt = LegacyReplacementChunk { - offset: chunk.offset, - size: chunk.chunk.len(), - digest: md5::compute(&chunk.chunk), - }; - let mut chunks = self.chunks.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(expected) = chunks.get(&chunk.part_number) { - if expected != &receipt { - return Err(io::Error::other("Replacement upload bytes changed")); - } - } else if record { - let _ = chunks.insert(chunk.part_number, receipt); - } else { - return Err(io::Error::other( - "Replacement upload has an unrecognized byte range", - )); - } - Ok(()) - } - - fn verify_parts(&self, parts: &[UploadedPart]) -> Result<(), AuthedApiError> { - let chunks = self.chunks.lock().unwrap_or_else(PoisonError::into_inner); - let mut offset = 0_u64; - if parts.len() != chunks.len() { - return Err("Replacement upload parts are incomplete".into()); - } - for (index, part) in parts.iter().enumerate() { - let Some(chunk) = chunks.get(&part.part_number) else { - return Err("Replacement upload part was not verified".into()); - }; - if part.part_number as usize != index + 1 - || chunk.offset != offset - || chunk.size != part.size - || part.total_size != self.size - { - return Err("Replacement upload part inventory changed".into()); - } - offset = offset - .checked_add(part.size as u64) - .ok_or("Replacement upload size overflow")?; - } - if offset != self.size { - return Err("Replacement upload does not cover the original file".into()); - } - Ok(()) - } -} - -fn legacy_replacement_session(replace_existing: bool, upload_id: &str) -> bool { - replace_existing && !upload_id.starts_with("cap-reupload.") -} - -#[instrument(skip(app, channel, file_path, screenshot_path))] -pub async fn upload_video( - app: &AppHandle, - video_id: String, - file_path: PathBuf, - screenshot_path: PathBuf, - replace_existing: bool, - channel: Option>, -) -> Result { - let _active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - info!("Uploading video {video_id}..."); - - let start = Instant::now(); - let initial_upload = lifecycle::cancellable(api::upload_multipart_initiate( - app, - &video_id, - replace_existing, - )) - .await??; - let legacy_input = std::sync::OnceLock::>::new(); - let video_fut = { - let file_path = &file_path; - let video_id = &video_id; - let channel = &channel; - let legacy_input = &legacy_input; - let initial_upload = &initial_upload; - with_replacement_restart(replace_existing, move |restarted| async move { - if let Some(input) = legacy_input.get() { - input.verify(file_path).await?; - } - let upload = if restarted { - lifecycle::cancellable(api::upload_multipart_initiate( - app, - video_id, - replace_existing, - )) - .await?? - } else { - api::MultipartUploadInitiateResponse { - upload_id: initial_upload.upload_id.clone(), - provider: initial_upload.provider.clone(), - } - }; - validate_replacement_restart_session(restarted, &upload.upload_id)?; - if legacy_replacement_session(replace_existing, &upload.upload_id) - && legacy_input.get().is_none() - { - legacy_input - .set(Arc::new(LegacyReplacementInput::capture(file_path).await?)) - .map_err(|_| "Replacement input identity was already established")?; - } - let input = legacy_input.get(); - let is_drive_upload = - is_google_drive_upload(upload.provider.as_deref(), &upload.upload_id); - let upload_id = upload.upload_id; - let failed_chunks: Arc>> = Arc::new(Mutex::new(Vec::new())); - - let stream = progress( - app.clone(), - video_id.clone(), - multipart_uploader( - app.clone(), - video_id.clone(), - upload_id.clone(), - is_drive_upload, - from_pending_file_to_chunks(file_path.clone(), None).map({ - let input = input.cloned(); - move |chunk| { - let chunk = chunk?; - if let Some(input) = &input { - input.inspect_chunk(&chunk, !restarted)?; - } - Ok(chunk) - } - }), - failed_chunks.clone(), - ), - ); - - let stream = if let Some(channel) = channel.clone() { - tauri_channel_progress(channel, stream).boxed() - } else { - stream.boxed() - }; - - let mut parts = stream.try_collect::>().await?; - - let failed = - std::mem::take(&mut *failed_chunks.lock().unwrap_or_else(PoisonError::into_inner)); - if !failed.is_empty() { - info!( - count = failed.len(), - "Retrying {} failed chunk(s) after main upload pass", - failed.len() - ); - let retry_parts = retry_failed_chunks_with_input( - app, - video_id, - &upload_id, - file_path, - failed, - input.map(Arc::as_ref), - ) - .await?; - parts.extend(retry_parts); - } - - let mut deduplicated_parts = HashMap::new(); - for part in parts { - deduplicated_parts.insert(part.part_number, part); - } - parts = deduplicated_parts.into_values().collect::>(); - parts.sort_by_key(|part| part.part_number); - - let metadata = build_video_meta(file_path) - .map_err(|e| error!("Failed to get video metadata: {e}")) - .ok(); - - if let Some(input) = input { - input.verify_parts(&parts)?; - input.verify(file_path).await?; - } - let completed_identity = match api::upload_multipart_complete_outcome( - app, - video_id, - &upload_id, - &parts, - metadata.clone(), - replace_existing, - ) - .await? - { - api::MultipartCompletion::Completed(identity) => identity, - api::MultipartCompletion::ReplacementRestartRequired if input.is_some() => { - return Ok(ReplacementAttempt::RestartRequired); - } - api::MultipartCompletion::ReplacementRestartRequired => { - return Err( - "Signed replacement upload was rejected; local files retained".into(), - ); - } - }; - let object_identity = if is_drive_upload { - parts - .iter() - .rev() - .find_map(|part| part.object_identity.clone()) - } else { - completed_identity - }; - Ok(ReplacementAttempt::Completed((metadata, object_identity))) - }) - }; - - // TODO: We don't report progress on image upload - let bytes = compress_image(screenshot_path).await?; - let thumbnail_fut = singlepart_uploader( - app.clone(), - PresignedS3PutRequest { - video_id: video_id.clone(), - subpath: "screenshot/screen-capture.jpg".to_string(), - method: PresignedS3PutRequestMethod::Put, - meta: None, - }, - bytes.len() as u64, - stream::once(async move { Ok::<_, std::io::Error>(bytes::Bytes::from(bytes)) }), - ); - - let (video_result, thumbnail_result): (Result<_, AuthedApiError>, Result<_, AuthedApiError>) = - tokio::join!(video_fut, thumbnail_fut); - - async_capture_event( - app, - match &video_result { - Ok((meta, _)) => AnalyticsEvent::MultipartUploadComplete { - duration: start.elapsed(), - length: meta - .as_ref() - .map(|v| Duration::from_secs(v.duration_in_secs as u64)) - .unwrap_or_default(), - size: std::fs::metadata(file_path) - .map(|m| ((m.len() as f64) / 1_000_000.0) as u64) - .unwrap_or_default(), - }, - Err(err) => AnalyticsEvent::MultipartUploadFailed { - duration: start.elapsed(), - error: err.to_string(), - }, - }, - ); - - let (_, object_identity) = video_result?; - thumbnail_result?; - lifecycle::cancellable(async {}).await?; - emit_upload_complete(app, &video_id); - - Ok(UploadedItem { - link: app.make_app_url(format!("/s/{video_id}")).await, - id: video_id, - object_identity, - }) -} - -/// Open a file and construct a stream to it. -async fn file_reader_stream(path: impl AsRef) -> Result<(ReaderStream, u64), String> { - let file = File::open(path) - .await - .map_err(|e| format!("Failed to open file: {e}"))?; - - let metadata = file - .metadata() - .await - .map_err(|e| format!("Failed to get file metadata: {e}"))?; - - Ok((ReaderStream::new(file), metadata.len())) -} - -fn screenshot_upload_subpath(content_type: &str) -> &'static str { - if content_type.eq_ignore_ascii_case("image/png") { - "screenshot/screen-capture.png" - } else { - "screenshot/screen-capture.jpg" - } -} - -fn screenshot_content_type_from_path(path: &Path) -> &'static str { - match path - .extension() - .and_then(|extension| extension.to_str()) - .map(str::to_ascii_lowercase) - .as_deref() - { - Some("png") => "image/png", - _ => "image/jpeg", - } -} - -pub async fn upload_screenshot_bytes( - app: &AppHandle, - image_bytes: Vec, - content_type: &str, - video_id: Option, - organization_id: Option, -) -> Result { - let _active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - let created = create_or_get_video(app, true, video_id, None, None, organization_id).await?; - let link = created.link(app).await; - let s3_config = created.config; - let subpath = screenshot_upload_subpath(content_type); - let total_size = image_bytes.len() as u64; - - singlepart_uploader( - app.clone(), - PresignedS3PutRequest { - video_id: s3_config.id.clone(), - subpath: subpath.to_string(), - method: PresignedS3PutRequestMethod::Put, - meta: None, - }, - total_size, - stream::once(async move { Ok::<_, std::io::Error>(Bytes::from(image_bytes)) }), - ) - .await?; - - Ok(UploadedItem { - link, - id: s3_config.id, - object_identity: None, - }) -} - -pub async fn upload_screenshot_file( - app: &AppHandle, - file_path: PathBuf, - video_id: Option, - organization_id: Option, -) -> Result { - let _active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - let content_type = screenshot_content_type_from_path(&file_path); - let created = create_or_get_video(app, true, video_id, None, None, organization_id).await?; - let link = created.link(app).await; - let s3_config = created.config; - let subpath = screenshot_upload_subpath(content_type); - let (stream, total_size) = file_reader_stream(file_path).await?; - - singlepart_uploader( - app.clone(), - PresignedS3PutRequest { - video_id: s3_config.id.clone(), - subpath: subpath.to_string(), - method: PresignedS3PutRequestMethod::Put, - meta: None, - }, - total_size, - stream, - ) - .await?; - - Ok(UploadedItem { - link, - id: s3_config.id, - object_identity: None, - }) -} - -#[instrument(skip(app))] -pub async fn create_or_get_video( - app: &AppHandle, - is_screenshot: bool, - video_id: Option, - name: Option, - meta: Option, - organization_id: Option, -) -> Result { - create_or_get_video_with_mode( - app, - is_screenshot, - video_id, - name, - meta, - organization_id, - "desktopMP4", - ) - .await -} - -#[instrument(skip(app))] -pub async fn create_or_get_video_with_mode( - app: &AppHandle, - is_screenshot: bool, - video_id: Option, - name: Option, - meta: Option, - organization_id: Option, - recording_mode: &str, -) -> Result { - let mut s3_config_url = if let Some(id) = video_id { - let mut url = - format!("/api/desktop/video/create?recordingMode={recording_mode}&videoId={id}"); - if is_screenshot { - url.push_str("&isScreenshot=true"); - } - url - } else if is_screenshot { - format!("/api/desktop/video/create?recordingMode={recording_mode}&isScreenshot=true") - } else { - format!("/api/desktop/video/create?recordingMode={recording_mode}") - }; - - if let Some(name) = name { - s3_config_url.push_str(&format!("&name={name}")); - } - - if let Some(meta) = meta { - s3_config_url.push_str(&format!("&durationInSecs={}", meta.duration_in_secs)); - s3_config_url.push_str(&format!("&width={}", meta.width)); - s3_config_url.push_str(&format!("&height={}", meta.height)); - if let Some(fps) = meta.fps { - s3_config_url.push_str(&format!("&fps={fps}")); - } - } - - if let Some(org_id) = organization_id { - s3_config_url.push_str(&format!("&orgId={org_id}")); - } - - let response = app - .authed_api_request(s3_config_url, |client, url| client.get(url)) - .await?; - - if response.status() != StatusCode::OK { - #[derive(Deserialize, Clone, Debug)] - pub struct CreateErrorResponse { - error: String, - } - - let status = response.status(); - let body = response.text().await; - - if let Some(error) = body - .as_ref() - .ok() - .and_then(|body| serde_json::from_str::(body).ok()) - && status == StatusCode::FORBIDDEN - && error.error == "upgrade_required" - { - return Err(AuthedApiError::UpgradeRequired); - } - - return Err(format!("create_or_get_video/error/{status}: {body:?}").into()); - } - - let response_text = response - .text() - .await - .map_err(|e| format!("Failed to read response body: {e}"))?; - - parse_created_video(&response_text).map_err(|e| { - format!("Failed to deserialize response: {e}. Response body: {response_text}").into() - }) -} - -pub struct CreatedVideo { - pub config: S3UploadMeta, - pub share_url: Option, -} - -impl CreatedVideo { - pub async fn link(&self, app: &AppHandle) -> String { - match &self.share_url { - Some(share_url) => share_url.clone(), - None => app.make_app_url(format!("/s/{}", self.config.id)).await, - } - } -} - -fn parse_created_video(body: &str) -> Result { - #[derive(Deserialize)] - struct CreateVideoResponse { - id: String, - #[serde(default)] - share_url: Option, - } - - let response = serde_json::from_str::(body)?; - let share_url = response - .share_url - .filter(|share_url| is_share_url_for(share_url, &response.id)); - Ok(CreatedVideo { - config: S3UploadMeta { id: response.id }, - share_url, - }) -} - -fn is_share_url_for(share_url: &str, video_id: &str) -> bool { - reqwest::Url::parse(share_url).is_ok_and(|url| { - url.scheme() == "https" - && url.host_str().is_some_and(|host| !host.is_empty()) - && url.username().is_empty() - && url.password().is_none() - && url.port().is_none() - && url.path() == format!("/s/{video_id}") - && url.query().is_none() - && url.fragment().is_none() - }) -} - -#[instrument] -pub fn build_video_meta(path: &PathBuf) -> Result { - let input = - ffmpeg::format::input(path).map_err(|e| format!("Failed to read input file: {e}"))?; - let video_stream = input - .streams() - .best(ffmpeg::media::Type::Video) - .ok_or_else(|| "Failed to find appropriate video stream in file".to_string())?; - - let video_codec = ffmpeg::codec::context::Context::from_parameters(video_stream.parameters()) - .map_err(|e| format!("Unable to read video codec information: {e}"))?; - let video = video_codec - .decoder() - .video() - .map_err(|e| format!("Unable to get video decoder: {e}"))?; - - Ok(S3VideoMeta { - duration_in_secs: input.duration() as f64 / AV_TIME_BASE as f64, - width: video.width(), - height: video.height(), - fps: video - .frame_rate() - .map(|v| v.numerator() as f32 / v.denominator() as f32), - }) -} - -#[instrument] -pub async fn compress_image(path: PathBuf) -> Result, String> { - task::spawn_blocking(move || { - let img = ImageReader::open(&path) - .map_err(|e| format!("Failed to open image: {e}"))? - .decode() - .map_err(|e| format!("Failed to decode image: {e}"))?; - - let resized_img = img.resize( - img.width() / 2, - img.height() / 2, - image::imageops::FilterType::Nearest, - ); - - let mut buffer = Vec::new(); - let mut encoder = JpegEncoder::new_with_quality(&mut buffer, 30); - encoder - .encode( - resized_img.as_bytes(), - resized_img.width(), - resized_img.height(), - resized_img.color().into(), - ) - .map_err(|e| format!("Failed to compress image: {e}"))?; - - Ok(buffer) - }) - .await - .map_err(|e| format!("Failed to compress image: {e}"))? -} - -pub struct InstantMultipartUpload { - #[cfg(not(target_os = "linux"))] - pub(crate) session: Arc, - #[cfg(not(target_os = "linux"))] - pub handle: tokio::task::JoinHandle>, -} - -impl InstantMultipartUpload { - /// starts a progressive (multipart) upload that runs until recording stops - /// and the file has stabilized (no additional data is being written). - #[cfg(not(target_os = "linux"))] - pub(crate) fn spawn( - app: AppHandle, - file_path: PathBuf, - pre_created_video: VideoUploadInfo, - session: Arc, - realtime_upload_done: Option>, - required_audio: bool, - ) -> Self { - let recording_dir = session.directory.clone(); - let worker_session = session.clone(); - let handle = spawn_actor(async move { - let start = Instant::now(); - let result = worker_session - .run(async { - worker_session.acquire()?; - crate::web_api::inherit_upload_context( - worker_session.context(), - Self::run( - app.clone(), - file_path.clone(), - pre_created_video, - recording_dir, - realtime_upload_done, - required_audio, - worker_session.clone(), - ), - ) - .await - }) - .await; - - async_capture_event( - &app, - match &result { - Ok(meta) => AnalyticsEvent::MultipartUploadComplete { - duration: start.elapsed(), - length: meta - .as_ref() - .map(|v| Duration::from_secs(v.duration_in_secs as u64)) - .unwrap_or_default(), - size: std::fs::metadata(file_path) - .map(|m| ((m.len() as f64) / 1_000_000.0) as u64) - .unwrap_or_default(), - }, - Err(err) => AnalyticsEvent::MultipartUploadFailed { - duration: start.elapsed(), - error: err.to_string(), - }, - }, - ); - - result.map(|_| ()) - }); - Self { - session, - #[cfg(not(target_os = "linux"))] - handle, - } - } - - pub(crate) async fn run( - app: AppHandle, - file_path: PathBuf, - pre_created_video: VideoUploadInfo, - recording_dir: PathBuf, - realtime_video_done: Option>, - required_audio: bool, - session: Arc, - ) -> Result, AuthedApiError> { - session.check()?; - if let Some(verification) = session.cached_verification()? { - await_upload_verification(&app, &pre_created_video.id, &verification, &session).await?; - return Ok(Some(build_video_meta(&file_path)?)); - } - let active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - let video_id = pre_created_video.id.clone(); - debug!("Initiating multipart upload for {video_id}..."); - - session.persist_upload(UploadMeta::MultipartUpload { - video_id: video_id.clone(), - file_path: file_path.clone(), - pre_created_video: pre_created_video.clone(), - recording_dir: recording_dir.clone(), - })?; - - let upload = api::upload_multipart_initiate(&app, &video_id, false).await?; - let is_drive_upload = is_google_drive_upload(upload.provider.as_deref(), &upload.upload_id); - let upload_id = upload.upload_id; - - let failed_chunks: Arc>> = Arc::new(Mutex::new(Vec::new())); - - let mut parts = progress( - app.clone(), - video_id.clone(), - multipart_uploader( - app.clone(), - video_id.clone(), - upload_id.clone(), - is_drive_upload, - from_pending_file_to_chunks(file_path.clone(), realtime_video_done), - failed_chunks.clone(), - ), - ) - .try_collect::>() - .await?; - - let failed = - std::mem::take(&mut *failed_chunks.lock().unwrap_or_else(PoisonError::into_inner)); - if !failed.is_empty() { - info!( - count = failed.len(), - "Retrying {} failed chunk(s) after main upload pass", - failed.len() - ); - let retry_parts = - retry_failed_chunks(&app, &video_id, &upload_id, &file_path, failed).await?; - parts.extend(retry_parts); - } - - let mut deduplicated_parts = HashMap::new(); - for part in parts { - deduplicated_parts.insert(part.part_number, part); - } - parts = deduplicated_parts.into_values().collect::>(); - parts.sort_by_key(|part| part.part_number); - - // Multipart parts already describe the original bytes. Repairing the file here - // would validate different bytes from those being completed remotely. - let metadata = build_video_meta(&file_path).map_err(|error| { - format!( - "Uploaded recording could not be validated; original retained for recovery: {error}" - ) - })?; - let file_size = std::fs::metadata(&file_path) - .map_err(|error| error.to_string())? - .len(); - let duration = metadata.duration_in_secs; - let metadata = Some(metadata); - session.wait_ready().await?; - let completed_identity = api::upload_multipart_complete( - &app, - &video_id, - &upload_id, - &parts, - metadata.clone(), - false, - ) - .await?; - let object_identity = if is_drive_upload { - parts - .iter() - .rev() - .find_map(|part| part.object_identity.clone()) - } else { - completed_identity - }; - let verification = UploadVerification::mp4( - file_size, - duration, - required_audio, - object_identity.ok_or( - "Server did not return the uploaded object identity; local recording retained", - )?, - )?; - // Every byte is on the server now; verification is server-side and resumes on - // the next launch via the persisted intent, so it must not block app exit. - drop(active_upload); - await_upload_verification(&app, &video_id, &verification, &session).await?; - info!("Multipart upload complete for {video_id}."); - - emit_upload_complete(&app, &video_id); - - Ok(metadata) - } -} - -pub struct SegmentUploader { - pub(crate) session: Arc, - pub handle: tokio::task::JoinHandle>, -} - -type SegmentUploadTasks = - futures::stream::FuturesUnordered>>; - -struct SegmentUploadState { - uploaded_video_segments: std::collections::HashMap, - uploaded_audio_segments: std::collections::HashMap, - video_init_uploaded: bool, - audio_init_uploaded: bool, - failed_segments: Vec, - worker_error: Option, - total_bytes_uploaded: u64, -} - -#[derive(Clone)] -struct FailedSegmentInfo { - subpath: String, - file_path: PathBuf, - is_init: bool, - media_type: cap_enc_ffmpeg::segmented_stream::SegmentMediaType, - index: u32, - duration: f64, - expected_size: u64, -} - -impl SegmentUploadState { - fn new() -> Self { - Self { - uploaded_video_segments: std::collections::HashMap::new(), - uploaded_audio_segments: std::collections::HashMap::new(), - video_init_uploaded: false, - audio_init_uploaded: false, - failed_segments: Vec::new(), - worker_error: None, - total_bytes_uploaded: 0, - } - } - - fn to_manifest(&self) -> SegmentUploadManifest { - let mut video_segments: Vec = self - .uploaded_video_segments - .iter() - .map(|(&index, &duration)| SegmentManifestEntry { index, duration }) - .collect(); - video_segments.sort_by_key(|s| s.index); - let mut audio_segments: Vec = self - .uploaded_audio_segments - .iter() - .map(|(&index, &duration)| SegmentManifestEntry { index, duration }) - .collect(); - audio_segments.sort_by_key(|s| s.index); - - SegmentUploadManifest { - version: 2, - video_init_uploaded: self.video_init_uploaded, - audio_init_uploaded: self.audio_init_uploaded, - video_segments, - audio_segments, - is_complete: false, - } - } - - fn to_complete_manifest(&self) -> SegmentUploadManifest { - let mut manifest = self.to_manifest(); - manifest.is_complete = true; - manifest - } - - fn record_worker_failure(&mut self, error: String) { - warn!(%error, "Segment upload worker failed"); - if self.worker_error.is_none() { - self.worker_error = Some(error); - } - } - - fn record_task_result( - &mut self, - result: Result, tokio::task::JoinError>, - ) { - let error = match result { - Ok(Ok(())) => return, - Ok(Err(error)) => error.to_string(), - Err(error) => format!("Segment upload task did not complete: {error}"), - }; - self.record_worker_failure(error); - } - - fn completed_manifest(&self) -> Result { - if let Some(error) = &self.worker_error { - return Err(error.clone().into()); - } - if !self.failed_segments.is_empty() { - return Err(format!( - "Segment upload is incomplete: {} required segment uploads failed", - self.failed_segments.len() - ) - .into()); - } - let manifest = self.to_complete_manifest(); - if !manifest.has_video_content() { - return Err("Segment upload completed without video segments".into()); - } - Ok(manifest) - } -} - -async fn publish_completed_segments( - state: &Mutex, - publish: F, -) -> Result -where - F: FnOnce(SegmentUploadManifest) -> Fut, - Fut: std::future::Future>, -{ - let manifest = state - .lock() - .unwrap_or_else(|error| error.into_inner()) - .completed_manifest()?; - publish(manifest).await -} - -async fn drain_segment_upload_tasks( - state: &Mutex, - tasks: &mut SegmentUploadTasks, -) { - while let Some(result) = tasks.next().await { - state - .lock() - .unwrap_or_else(|error| error.into_inner()) - .record_task_result(result); - } -} - -async fn cancel_segment_prefetch_tasks( - tasks: &mut futures::stream::FuturesUnordered>, -) { - for task in tasks.iter() { - task.abort(); - } - while let Some(result) = tasks.next().await { - if let Err(error) = result - && !error.is_cancelled() - { - warn!(%error, "Optional segment URL prefetch failed"); - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct SegmentManifestEntry { - index: u32, - duration: f64, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct SegmentUploadManifest { - version: u32, - video_init_uploaded: bool, - audio_init_uploaded: bool, - video_segments: Vec, - audio_segments: Vec, - is_complete: bool, -} - -impl SegmentUploadManifest { - fn has_video_content(&self) -> bool { - self.video_init_uploaded && !self.video_segments.is_empty() - } -} - -struct PresignedUrlCache { - urls: tokio::sync::Mutex>, -} - -impl PresignedUrlCache { - fn new() -> Self { - Self { - urls: tokio::sync::Mutex::new(HashMap::new()), - } - } - - async fn prefetch(&self, app: &AppHandle, video_id: &str, segment_count: u32) { - let mut subpaths = Vec::with_capacity((segment_count as usize) * 2 + 3); - subpaths.push("segments/video/init.mp4".to_string()); - subpaths.push("segments/audio/init.mp4".to_string()); - subpaths.push("segments/manifest.json".to_string()); - for i in 1..=segment_count { - subpaths.push(format!("segments/video/segment_{i:03}.m4s")); - subpaths.push(format!("segments/audio/segment_{i:03}.m4s")); - } - - match api::upload_signed_batch(app, video_id, &subpaths).await { - Ok(urls) => { - let mut cache = self.urls.lock().await; - let count = urls.len(); - for (subpath, url) in urls { - cache.insert(subpath, url); - } - info!(count, "Pre-fetched presigned URLs for segments"); - } - Err(e) => { - warn!("Failed to batch-prefetch presigned URLs: {e}"); - } - } - } - - async fn get_or_fetch( - &self, - app: &AppHandle, - video_id: &str, - subpath: &str, - ) -> Result { - { - let mut cache = self.urls.lock().await; - if let Some(url) = cache.remove(subpath) { - return Ok(url); - } - } - - api::upload_signed( - app, - api::PresignedS3PutRequest { - video_id: video_id.to_string(), - subpath: subpath.to_string(), - method: api::PresignedS3PutRequestMethod::Put, - meta: None, - }, - ) - .await - .map(|target| target.url) - } - - async fn extend_prefetch(&self, app: &AppHandle, video_id: &str, from: u32, count: u32) { - let mut subpaths = Vec::with_capacity((count as usize) * 2); - for i in from..from + count { - subpaths.push(format!("segments/video/segment_{i:03}.m4s")); - subpaths.push(format!("segments/audio/segment_{i:03}.m4s")); - } - - match api::upload_signed_batch(app, video_id, &subpaths).await { - Ok(urls) => { - let mut cache = self.urls.lock().await; - let count = urls.len(); - for (subpath, url) in urls { - cache.insert(subpath, url); - } - info!(count, from, "Extended presigned URL cache"); - } - Err(e) => { - warn!("Failed to extend presigned URL cache: {e}"); - } - } - } -} - -impl SegmentUploader { - #[cfg(not(target_os = "linux"))] - pub(crate) fn spawn( - app: AppHandle, - segment_rx: std::sync::mpsc::Receiver< - cap_enc_ffmpeg::segmented_stream::SegmentCompletedEvent, - >, - recording_done: Option>, - session: Arc, - pre_created_video: VideoUploadInfo, - required_audio: bool, - ) -> Self { - let recording_dir = session.directory.clone(); - let worker_session = session.clone(); - Self { - session, - handle: spawn_actor(async move { - let start = Instant::now(); - let result = worker_session - .run(async { - worker_session.acquire()?; - crate::web_api::inherit_upload_context( - worker_session.context(), - Self::run( - app.clone(), - segment_rx, - recording_done, - recording_dir.clone(), - pre_created_video, - required_audio, - worker_session.clone(), - ), - ) - .await - }) - .await; - - async_capture_event( - &app, - match &result { - Ok(total_bytes) => AnalyticsEvent::MultipartUploadComplete { - duration: start.elapsed(), - length: start.elapsed(), - size: total_bytes / (1024 * 1024), - }, - Err(err) => AnalyticsEvent::MultipartUploadFailed { - duration: start.elapsed(), - error: err.to_string(), - }, - }, - ); - - result.map(|_| ()) - }), - } - } - - async fn read_segment_data( - file_path: &Path, - subpath: &str, - expected_size: u64, - ) -> Result { - const FILE_WAIT_TIMEOUT: Duration = Duration::from_secs(10); - const DATA_WAIT_TIMEOUT: Duration = Duration::from_secs(30); - - Self::read_segment_data_with_timeouts( - file_path, - subpath, - expected_size, - FILE_WAIT_TIMEOUT, - DATA_WAIT_TIMEOUT, - ) - .await - } - - async fn read_segment_data_with_timeouts( - file_path: &Path, - subpath: &str, - expected_size: u64, - file_wait_timeout: Duration, - data_wait_timeout: Duration, - ) -> Result { - if expected_size == 0 { - return Err(format!("segment_upload/{subpath}: expected segment size is zero").into()); - } - - let start = Instant::now(); - let actual_path = loop { - if file_path.exists() { - break file_path.to_path_buf(); - } - if start.elapsed() >= file_wait_timeout { - return Err(format!( - "segment_upload/timeout/{subpath}: file not found after {:?} ({})", - file_wait_timeout, - file_path.display() - ) - .into()); - } - lifecycle::cancellable(tokio::time::sleep(Duration::from_millis(50))).await?; - }; - - let start = Instant::now(); - let file_data = loop { - let path = actual_path.clone(); - let data = lifecycle::file_io(move || std::fs::read(path)) - .await - .map_err(|e| { - format!( - "segment_upload/read/{subpath}: {e} ({})", - actual_path.display() - ) - })?; - - if data.len() as u64 == expected_size { - break data; - } - - if data.len() as u64 > expected_size || start.elapsed() >= data_wait_timeout { - return Err(format!( - "segment_upload/{subpath}: expected {expected_size} bytes, found {}", - data.len() - ) - .into()); - } - - lifecycle::cancellable(tokio::time::sleep(Duration::from_millis(50))).await?; - }; - - Ok(Bytes::from(file_data)) - } - - async fn put_segment_to_s3( - app: &AppHandle, - video_id: &str, - subpath: &str, - file_bytes: Bytes, - url_cache: &PresignedUrlCache, - prefetched_url: Option, - ) -> Result { - const MAX_RETRIES: u32 = 3; - let file_size = file_bytes.len(); - let mut cached_url = prefetched_url; - - for attempt in 0..MAX_RETRIES { - let presigned_url = match cached_url.take() { - Some(url) => url, - None => url_cache.get_or_fetch(app, video_id, subpath).await?, - }; - - let client = app - .state::() - .as_ref() - .map_err(|err| format!("segment_upload/client: {err:?}"))? - .clone(); - - let request = client - .put(&presigned_url) - .header("Content-Length", file_size) - .header("Content-Type", content_type_for_upload_subpath(subpath)) - .timeout(Duration::from_secs(5 * 60)) - .body(file_bytes.clone()); - let send_result = lifecycle::cancellable( - with_drive_content_range( - request, - &presigned_url, - 0, - file_size as u64, - file_size as u64, - ) - .send(), - ) - .await? - .map_err(reqwest::Error::without_url); - - match send_result { - Ok(resp) if resp.status().is_success() => { - return Ok(file_size as u64); - } - Ok(resp) => { - let status = resp.status(); - if attempt < MAX_RETRIES - 1 { - warn!( - attempt = attempt + 1, - subpath, - status = %status, - "Segment upload failed, retrying" - ); - tokio::time::sleep(Duration::from_millis(500 * (1 << attempt) as u64)) - .await; - continue; - } - return Err(format!("segment_upload/{subpath}/error: {status}").into()); - } - Err(err) if is_reqwest_network_error(&err) => { - if attempt < MAX_RETRIES - 1 { - warn!( - attempt = attempt + 1, - subpath, - error = %err, - "Segment upload network error, retrying" - ); - if !wait_for_network_recovery(app, video_id).await { - return Err( - format!("segment_upload/{subpath}/network_timeout: {err}").into() - ); - } - continue; - } - return Err(format!("segment_upload/{subpath}/network_error: {err}").into()); - } - Err(err) => { - return Err(format!("segment_upload/{subpath}/error: {err}").into()); - } - } - } - - Err(format!("segment_upload/{subpath}/exhausted_retries").into()) - } - - async fn upload_manifest( - app: &AppHandle, - video_id: &str, - manifest: &SegmentUploadManifest, - url_cache: &PresignedUrlCache, - ) -> Result<(), AuthedApiError> { - let json = serde_json::to_string_pretty(manifest) - .map_err(|e| format!("segment_upload/manifest/serialize: {e}"))?; - - let presigned_url = url_cache - .get_or_fetch(app, video_id, "segments/manifest.json") - .await?; - - let client = app - .state::() - .as_ref() - .map_err(|err| format!("segment_upload/manifest/client: {err:?}"))? - .clone(); - - let content_length = json.len() as u64; - let request = client - .put(&presigned_url) - .header("Content-Length", content_length) - .header("Content-Type", "application/json") - .timeout(Duration::from_secs(60)) - .body(json); - let resp = lifecycle::cancellable( - with_drive_content_range(request, &presigned_url, 0, content_length, content_length) - .send(), - ) - .await? - .map_err(reqwest::Error::without_url) - .map_err(|err| format!("segment_upload/manifest/error: {err}"))?; - - if !resp.status().is_success() { - return Err(format!("segment_upload/manifest/error: {}", resp.status()).into()); - } - - Ok(()) - } - - async fn run( - app: AppHandle, - segment_rx: std::sync::mpsc::Receiver< - cap_enc_ffmpeg::segmented_stream::SegmentCompletedEvent, - >, - recording_done: Option>, - recording_dir: PathBuf, - pre_created_video: VideoUploadInfo, - required_audio: bool, - session: Arc, - ) -> Result { - use cap_enc_ffmpeg::segmented_stream::SegmentMediaType; - let video_id = pre_created_video.id.clone(); - - session.check()?; - - if let Some(verification) = session.cached_verification()? { - await_upload_verification(&app, &video_id, &verification, &session).await?; - return Ok(0); - } - let active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - info!("Starting segment uploader for {video_id}"); - - session.persist_upload(UploadMeta::SegmentUpload { - video_id: video_id.clone(), - pre_created_video: pre_created_video.clone(), - recording_dir: recording_dir.clone(), - })?; - - let state = Arc::new(Mutex::new(SegmentUploadState::new())); - let preparation = preparation::start( - app.clone(), - video_id.clone(), - state.clone(), - session.clone(), - ); - let semaphore = Arc::new(tokio::sync::Semaphore::new(6)); - let read_semaphore = Arc::new(tokio::sync::Semaphore::new(12)); - let consecutive_failures = Arc::new(std::sync::atomic::AtomicU32::new(0)); - let mut in_flight = SegmentUploadTasks::new(); - let mut prefetch_tasks = futures::stream::FuturesUnordered::new(); - - let url_cache = Arc::new(PresignedUrlCache::new()); - { - let cache = url_cache.clone(); - let app = app.clone(); - let vid = video_id.clone(); - let context = session.context(); - prefetch_tasks.push(tokio::spawn(crate::web_api::inherit_upload_context( - context, - async move { - cache.prefetch(&app, &vid, 20).await; - }, - ))); - } - - let manifest_dirty = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let manifest_shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let manifest_notify = Arc::new(tokio::sync::Notify::new()); - let manifest_handle = { - let app = app.clone(); - let video_id = video_id.clone(); - let state = state.clone(); - let dirty = manifest_dirty.clone(); - let shutdown = manifest_shutdown.clone(); - let notify = manifest_notify.clone(); - let url_cache = url_cache.clone(); - let context = session.context(); - tokio::spawn(crate::web_api::inherit_upload_context( - context, - async move { - let mut interval = tokio::time::interval(Duration::from_secs(1)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = interval.tick() => {} - _ = notify.notified() => {} - } - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - let manifest = state - .lock() - .unwrap_or_else(|e| e.into_inner()) - .to_manifest(); - if let Err(e) = - Self::upload_manifest(&app, &video_id, &manifest, &url_cache).await - { - warn!("Periodic manifest upload failed: {e}"); - } - } - if shutdown.load(std::sync::atomic::Ordering::Acquire) { - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - let manifest = state - .lock() - .unwrap_or_else(|e| e.into_inner()) - .to_manifest(); - Self::upload_manifest(&app, &video_id, &manifest, &url_cache) - .await - .ok(); - } - break; - } - } - }, - )) - }; - - let (async_segment_tx, mut async_segment_rx) = tokio::sync::mpsc::unbounded_channel::< - cap_enc_ffmpeg::segmented_stream::SegmentCompletedEvent, - >(); - - let bridge_handle = { - let async_segment_tx = async_segment_tx; - let recording_done = recording_done.clone(); - let session = session.clone(); - std::thread::Builder::new() - .name("segment-rx-bridge".to_string()) - .spawn(move || { - while session.check().is_ok() && !async_segment_tx.is_closed() { - match segment_rx.recv_timeout(std::time::Duration::from_millis(20)) { - Ok(event) => { - if async_segment_tx.send(event).is_err() { - break; - } - } - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - if let Some(ref done_rx) = recording_done - && done_rx.try_recv().is_ok() - { - while let Ok(event) = segment_rx.try_recv() { - if async_segment_tx.send(event).is_err() { - return; - } - } - break; - } - } - Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { - while let Ok(event) = segment_rx.try_recv() { - if async_segment_tx.send(event).is_err() { - return; - } - } - break; - } - } - } - }) - .map_err(|e| format!("Failed to spawn bridge thread: {e}")) - }; - - let bridge_handle = match bridge_handle { - Ok(handle) => handle, - Err(error) => { - manifest_shutdown.store(true, std::sync::atomic::Ordering::Release); - manifest_notify.notify_one(); - let _ = manifest_handle.await; - cancel_segment_prefetch_tasks(&mut prefetch_tasks).await; - return Err(error.into()); - } - }; - - use futures::StreamExt as _; - - const CONSECUTIVE_FAILURE_LIMIT: u32 = 5; - - loop { - tokio::select! { - biased; - _ = session.cancelled() => { async_segment_rx.close(); break; } - Some(join_result) = in_flight.next(), if !in_flight.is_empty() => { - state.lock().unwrap_or_else(|error| error.into_inner()).record_task_result(join_result); - } - maybe_event = async_segment_rx.recv() => { - let Some(event) = maybe_event else { - break; - }; - - let subpath = match (event.is_init, event.media_type) { - (true, SegmentMediaType::Video) => "segments/video/init.mp4".to_string(), - (true, SegmentMediaType::Audio) => "segments/audio/init.mp4".to_string(), - (false, SegmentMediaType::Video) => { - format!("segments/video/segment_{:03}.m4s", event.index) - } - (false, SegmentMediaType::Audio) => { - format!("segments/audio/segment_{:03}.m4s", event.index) - } - }; - - let app_clone = app.clone(); - let video_id_clone = video_id.clone(); - let state_clone = state.clone(); - let failures_clone = consecutive_failures.clone(); - let semaphore_clone = semaphore.clone(); - let read_semaphore_clone = read_semaphore.clone(); - let url_cache_clone = url_cache.clone(); - let manifest_dirty_clone = manifest_dirty.clone(); - let manifest_notify_clone = manifest_notify.clone(); - - let file_path = event.path.clone(); - let is_init = event.is_init; - let media_type = event.media_type; - let index = event.index; - let duration = event.duration; - let expected_size = event.file_size; - let subpath_for_fail = subpath.clone(); - - let context = session.context(); - let task_session = session.clone(); - let handle = tokio::spawn(crate::web_api::inherit_upload_context(context, async move { - task_session.check()?; - let _read_permit = read_semaphore_clone - .acquire() - .await - .map_err(|e| format!("segment_upload/read_semaphore: {e}"))?; - - let (file_result, url_result) = tokio::join!( - Self::read_segment_data(&file_path, &subpath, expected_size), - url_cache_clone.get_or_fetch( - &app_clone, - &video_id_clone, - &subpath - ), - ); - - let file_data = match file_result { - Ok(data) => data, - Err(e) => { - let prev = failures_clone - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - warn!( - subpath, - error = %e, - consecutive_failures = prev + 1, - "Failed to read segment, queued for retry" - ); - let mut s = - state_clone.lock().unwrap_or_else(|e| e.into_inner()); - s.failed_segments.push(FailedSegmentInfo { - subpath: subpath_for_fail, - file_path, - is_init, - media_type, - index, - duration, - expected_size, - }); - return Ok(()); - } - }; - - let prefetched_url = match url_result { - Ok(url) => Some(url), - Err(e) => { - warn!(subpath, error = %e, "Pre-fetch URL failed, will fetch on demand"); - None - } - }; - - drop(_read_permit); - - let permit = semaphore_clone - .acquire_owned() - .await - .map_err(|e| format!("segment_upload/semaphore: {e}"))?; - - let result = Self::put_segment_to_s3( - &app_clone, - &video_id_clone, - &subpath, - file_data, - &url_cache_clone, - prefetched_url, - ) - .await; - - drop(permit); - - match result { - Err(e) => { - let prev = failures_clone - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - warn!( - subpath, - error = %e, - consecutive_failures = prev + 1, - "Failed to upload segment, queued for retry" - ); - let mut s = - state_clone.lock().unwrap_or_else(|e| e.into_inner()); - s.failed_segments.push(FailedSegmentInfo { - subpath: subpath_for_fail, - file_path, - is_init, - media_type, - index, - duration, - expected_size, - }); - } - Ok(bytes) => { - failures_clone - .store(0, std::sync::atomic::Ordering::Relaxed); - let mut s = - state_clone.lock().unwrap_or_else(|e| e.into_inner()); - s.total_bytes_uploaded += bytes; - if is_init { - match media_type { - SegmentMediaType::Video => { - s.video_init_uploaded = true - } - SegmentMediaType::Audio => { - s.audio_init_uploaded = true - } - } - } else { - match media_type { - SegmentMediaType::Video => { - s.uploaded_video_segments - .insert(index, duration); - } - SegmentMediaType::Audio => { - s.uploaded_audio_segments - .insert(index, duration); - } - } - } - manifest_dirty_clone - .store(true, std::sync::atomic::Ordering::Release); - manifest_notify_clone.notify_one(); - info!(subpath, "Segment uploaded successfully"); - } - } - - Ok(()) - })); - - in_flight.push(handle); - - if !event.is_init - && event.media_type == SegmentMediaType::Video - && event.index > 0 - && event.index % 15 == 0 - { - let cache = url_cache.clone(); - let app_ref = app.clone(); - let vid_ref = video_id.clone(); - let from = event.index + 1; - let context = session.context(); - prefetch_tasks.push(tokio::spawn(crate::web_api::inherit_upload_context(context, async move { - cache.extend_prefetch(&app_ref, &vid_ref, from, 20).await; - }))); - } - - if consecutive_failures.load(std::sync::atomic::Ordering::Relaxed) >= CONSECUTIVE_FAILURE_LIMIT { - warn!("Consecutive upload failures reached threshold, attempting network recovery"); - if !wait_for_network_recovery(&app, &video_id).await { - warn!("Network recovery failed during segment upload"); - } - consecutive_failures.store(0, std::sync::atomic::Ordering::Relaxed); - } - } - } - } - - drain_segment_upload_tasks(&state, &mut in_flight).await; - preparation.stop().await; - - if bridge_handle.join().is_err() { - state - .lock() - .unwrap_or_else(|error| error.into_inner()) - .record_worker_failure("Segment upload bridge did not complete".to_string()); - } - - manifest_shutdown.store(true, std::sync::atomic::Ordering::Release); - manifest_dirty.store(true, std::sync::atomic::Ordering::Release); - manifest_notify.notify_one(); - let manifest_result = manifest_handle.await; - state - .lock() - .unwrap_or_else(|error| error.into_inner()) - .record_task_result(manifest_result.map(|()| Ok(()))); - cancel_segment_prefetch_tasks(&mut prefetch_tasks).await; - - session.check()?; - let failed = { - let s = state.lock().unwrap_or_else(|e| e.into_inner()); - s.failed_segments.clone() - }; - - if !failed.is_empty() { - info!( - count = failed.len(), - "Retrying failed segment uploads before completing" - ); - for seg in failed { - session.check()?; - let file_data = - Self::read_segment_data(&seg.file_path, &seg.subpath, seg.expected_size).await; - - let file_data = match file_data { - Ok(data) => data, - Err(e) => { - warn!( - subpath = seg.subpath, - error = %e, - "Failed segment retry read failed, segment will be missing" - ); - continue; - } - }; - - match Self::put_segment_to_s3( - &app, - &video_id, - &seg.subpath, - file_data, - &url_cache, - None, - ) - .await - { - Ok(bytes) => { - let mut s = state.lock().unwrap_or_else(|e| e.into_inner()); - s.total_bytes_uploaded += bytes; - if seg.is_init { - match seg.media_type { - SegmentMediaType::Video => s.video_init_uploaded = true, - SegmentMediaType::Audio => s.audio_init_uploaded = true, - } - } else { - match seg.media_type { - SegmentMediaType::Video => { - s.uploaded_video_segments.insert(seg.index, seg.duration); - } - SegmentMediaType::Audio => { - s.uploaded_audio_segments.insert(seg.index, seg.duration); - } - } - } - s.failed_segments.retain(|f| f.subpath != seg.subpath); - info!(subpath = seg.subpath, "Failed segment retry succeeded"); - } - Err(e) => { - warn!( - subpath = seg.subpath, - error = %e, - "Failed segment retry also failed, segment will be missing" - ); - } - } - } - } - - session.wait_ready().await?; - let completion_state = state.as_ref(); - let expected_events = resume::collect_segment_events(&recording_dir, required_audio)?; - validate_uploaded_segment_inventory( - &completion_state - .lock() - .unwrap_or_else(|error| error.into_inner()) - .completed_manifest()?, - &expected_events, - )?; - publish_completed_segments(completion_state, |final_manifest| async move { - let final_json = serde_json::to_vec_pretty(&final_manifest)?; - let verification = UploadVerification::segments(&final_json, required_audio); - { - let mut manifest_err: Option = None; - for attempt in 0..3u32 { - match Self::upload_manifest(&app, &video_id, &final_manifest, &url_cache).await - { - Ok(()) => { - manifest_err = None; - break; - } - Err(e) => { - warn!( - attempt = attempt + 1, - "Failed to upload final manifest: {e}" - ); - manifest_err = Some(e); - if attempt < 2 { - tokio::time::sleep(Duration::from_millis( - 1000 * (1 << attempt) as u64, - )) - .await; - } - } - } - } - if let Some(e) = manifest_err { - error!("All attempts to upload final manifest failed for {video_id}"); - - // Leave the UploadMeta::SegmentUpload written at upload start in - // place so resume_uploads retries this recording on next launch. - emit_upload_complete(&app, &video_id); - - return Err(format!("segment_upload/final_manifest: {e}").into()); - } - } - - // All segments and the final manifest are on the server; the remaining wait is - // server-side finalization polled every 5s, which can take minutes and resumes - // on the next launch via the persisted verification, so stop blocking app exit. - drop(active_upload); - await_upload_verification(&app, &video_id, &verification, &session).await?; - emit_upload_complete(&app, &video_id); - - let total_bytes = completion_state - .lock() - .unwrap_or_else(|e| e.into_inner()) - .total_bytes_uploaded; - - info!(total_bytes, "Segment upload complete for {video_id}"); - - Ok(total_bytes) - }) - .await - } -} - -pub struct Chunk { - total_size: u64, - part_number: u32, - offset: u64, - chunk: Bytes, -} - -struct FailedChunkInfo { - part_number: u32, - offset: u64, - chunk_size: usize, - total_size: u64, - error: String, -} - -/// Creates a stream that reads chunks from a file, yielding [Chunk]'s. -#[allow(unused)] -#[instrument] -pub fn from_file_to_chunks(path: PathBuf) -> impl Stream> { - try_stream! { - let file = File::open(path).await?; - let total_size = file.metadata().await?.len(); - let mut file = BufReader::new(file); - - let mut buf = vec![0u8; MAX_CHUNK_SIZE as usize]; - let mut part_number = 0; - let mut current_offset: u64 = 0; - loop { - part_number += 1; - let n = file.read(&mut buf).await?; - if n == 0 { break; } - yield Chunk { - total_size, - part_number, - offset: current_offset, - chunk: Bytes::copy_from_slice(&buf[..n]), - }; - current_offset += n as u64; - } - } - .instrument(Span::current()) -} - -/// Creates a stream that reads chunks from a potentially growing file, yielding [Chunk]'s. -/// The first chunk of the file is yielded last to allow for header rewriting after recording completion. -/// This uploader will continually poll the filesystem and wait for the file to stop uploading before flushing the rest. -struct UploadFile { - file: Mutex, - _guard: Option, -} - -async fn open_upload_file(path: PathBuf) -> io::Result> { - let guard = lifecycle::UploadScope::current().map(|scope| scope.read_guard()); - lifecycle::file_io(move || { - Ok(Arc::new(UploadFile { - file: Mutex::new(std::fs::File::open(path)?), - _guard: guard, - })) - }) - .await -} - -async fn upload_file_size(file: Arc) -> io::Result { - lifecycle::file_io(move || { - file.file - .lock() - .unwrap_or_else(PoisonError::into_inner) - .metadata() - .map(|metadata| metadata.len()) - }) - .await -} - -async fn upload_file_chunk(file: Arc, offset: u64, count: usize) -> io::Result { - lifecycle::file_io(move || { - use std::io::{Read, Seek}; - let mut file = file.file.lock().unwrap_or_else(PoisonError::into_inner); - file.seek(std::io::SeekFrom::Start(offset))?; - let mut bytes = vec![0; count]; - file.read_exact(&mut bytes)?; - Ok(Bytes::from(bytes)) - }) - .await -} - -#[instrument(skip(realtime_upload_done))] -pub fn from_pending_file_to_chunks( - path: PathBuf, - realtime_upload_done: Option>, -) -> impl Stream> { - try_stream! { - let file = timeout(Duration::from_secs(20), async { - loop { - if let Ok(file) = open_upload_file(path.clone()).await { return Ok::<_, io::Error>(file); } - lifecycle::cancellable(tokio::time::sleep(Duration::from_millis(100))).await.map_err(io::Error::other)?; - } - }).await.map_err(|_| io::Error::other("Failed to open recording upload file"))??; - let mut part_number = 1; - let mut offset = 0u64; - let mut realtime_is_done = realtime_upload_done.as_ref().map(|_| false); - let mut recording_completed_cleanly = false; - let mut first_chunk_size = None; - loop { - lifecycle::cancellable(async {}).await.map_err(io::Error::other)?; - if !realtime_is_done.unwrap_or(true) && let Some(receiver) = &realtime_upload_done { - match receiver.try_recv() { - Ok(()) => { realtime_is_done = Some(true); recording_completed_cleanly = true; } - Err(flume::TryRecvError::Empty) => {}, - Err(flume::TryRecvError::Disconnected) => { realtime_is_done = Some(true); } - } - } - let file_size = upload_file_size(file.clone()).await?; - let available = file_size.saturating_sub(offset); - let should_read = match realtime_is_done { - Some(done) => available >= MIN_CHUNK_SIZE || (done && available > 0), - None => available > 0, - }; - if should_read { - let count = available.min(MAX_CHUNK_SIZE) as usize; - let chunk = upload_file_chunk(file.clone(), offset, count).await?; - if offset == 0 { first_chunk_size = Some(count); } - yield Chunk { total_size: file_size, part_number, offset, chunk }; - part_number += 1; - offset = offset.saturating_add(count as u64); - } else if available == 0 && realtime_is_done.unwrap_or(true) { - if let Some(count) = first_chunk_size && realtime_upload_done.is_some() && recording_completed_cleanly { - let chunk = upload_file_chunk(file.clone(), 0, count).await?; - yield Chunk { total_size: file_size, part_number: 1, offset: 0, chunk }; - } - break; - } else { - lifecycle::cancellable(tokio::time::sleep(Duration::from_millis(100))).await.map_err(io::Error::other)?; - } - } - }.instrument(Span::current()) -} - -#[instrument(skip(app, stream, upload_id, failed_chunks))] -fn multipart_uploader( - app: AppHandle, - video_id: String, - upload_id: String, - is_drive_upload: bool, - stream: impl Stream> + Send + 'static, - failed_chunks: Arc>>, -) -> impl Stream> + 'static { - const MAX_CONCURRENT_UPLOADS: usize = 5; - - debug!("Initializing multipart uploader for video {video_id:?}"); - let start = Instant::now(); - let video_id2 = video_id.clone(); - - stream::once(async move { - let use_md5_hashes = app.is_server_url_custom().await; - let max_concurrent_uploads = if is_drive_upload { - 1 - } else { - MAX_CONCURRENT_UPLOADS - }; - let first_chunk_presigned_url = Arc::new(Mutex::new(None::<(String, Instant)>)); - - stream::unfold( - (Box::pin(stream), 1), - move |(mut stream, expected_part_number)| { - let app = app.clone(); - let video_id = video_id.clone(); - let upload_id = upload_id.clone(); - let first_chunk_presigned_url = first_chunk_presigned_url.clone(); - let failed_chunks = failed_chunks.clone(); - - async move { - let (Some(item), presigned_url) = join(stream.next(), async { - if use_md5_hashes { - return Ok(None); - } - - api::upload_multipart_presign_part( - &app, - &video_id, - &upload_id, - expected_part_number, - None, - ) - .await - .map(Some) - }) - .await - else { - return None; - }; - - let part_number = item - .as_ref() - .map(|c| c.part_number.to_string()) - .unwrap_or_else(|_| "--".into()); - - Some(( - async move { - let Chunk { - total_size, - part_number, - chunk, - offset, - } = match item { - Ok(c) => c, - Err(err) => { - return Some(Err(AuthedApiError::Other(format!( - "uploader/part/{expected_part_number:?}/fs: {err:?}" - )))); - } - }; - - let chunk_size = chunk.len(); - - let upload_result = async move { - trace!( - "Uploading chunk {part_number} ({chunk_size} bytes) for video {video_id:?}", - ); - - let md5_sum = - use_md5_hashes.then(|| base64::encode(md5::compute(&chunk).0)); - let presigned_url = if let Some(url) = presigned_url? - && part_number == expected_part_number - { - url - } else if part_number == 1 - && !use_md5_hashes - && let Some((url, expiry)) = first_chunk_presigned_url - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone() - && expiry.elapsed() < Duration::from_secs(60 * 50) - { - url - } else { - api::upload_multipart_presign_part( - &app, - &video_id, - &upload_id, - part_number, - md5_sum.as_deref(), - ) - .await? - }; - - if part_number == 1 { - *first_chunk_presigned_url - .lock() - .unwrap_or_else(PoisonError::into_inner) = - Some((presigned_url.clone(), Instant::now())); - } - - let size = chunk.len(); - let chunk_for_retry = chunk.clone(); - let client = app - .state::() - .as_ref() - .map_err(|err| { - format!("uploader/part/{part_number}/client: {err:?}") - })? - .clone(); - - let req = client - .put(&presigned_url) - .header("Content-Length", size) - .timeout(Duration::from_secs(5 * 60)) - .body(chunk); - let mut req = with_drive_content_range( - req, - &presigned_url, - offset, - size as u64, - total_size, - ); - - if let Some(md5_sum) = &md5_sum { - req = req.header("Content-MD5", md5_sum); - } - - let send_result = lifecycle::cancellable(req.send().instrument(info_span!("s3_put", size = size))).await?.map_err(reqwest::Error::without_url); - - let resp = match send_result { - Ok(resp) => resp, - Err(err) if is_reqwest_network_error(&err) => { - info!( - part_number, - error = %err, - "Chunk upload failed due to network error, waiting for connectivity" - ); - if !wait_for_network_recovery(&app, &video_id).await { - return Err(format!( - "uploader/part/{part_number}/network_timeout: {err:?}" - ) - .into()); - } - let retry_url = api::upload_multipart_presign_part( - &app, - &video_id, - &upload_id, - part_number, - md5_sum.as_deref(), - ) - .await?; - let retry_req = client - .put(&retry_url) - .header("Content-Length", size) - .timeout(Duration::from_secs(5 * 60)) - .body(chunk_for_retry); - let mut retry_req = with_drive_content_range( - retry_req, - &retry_url, - offset, - size as u64, - total_size, - ); - if let Some(md5_sum) = &md5_sum { - retry_req = - retry_req.header("Content-MD5", md5_sum); - } - lifecycle::cancellable(retry_req.send().instrument(info_span!("s3_put_after_network_recovery", size = size))).await? - .map_err(reqwest::Error::without_url) - .map_err(|err| { - format!( - "uploader/part/{part_number}/network_retry_error: {err:?}" - ) - })? - } - Err(err) => { - return Err(format!( - "uploader/part/{part_number}/error: {err:?}" - ) - .into()); - } - }; - - let etag = resp - .headers() - .get("ETag") - .as_ref() - .and_then(|etag| etag.to_str().ok()) - .map(|v| v.trim_matches('"').to_string()); - - if !is_upload_response_accepted(&presigned_url, resp.status(), offset, size as u64, total_size) { - return Err(format!("uploader/part/{part_number}/error: {}", resp.status()).into()); - } - - trace!("Completed upload of part {part_number}"); - - let etag = match etag { - Some(etag) => etag, - None if is_google_drive_resumable_url(&presigned_url) => { - format!("drive-{part_number}") - } - None => { - return Err(format!( - "uploader/part/{part_number}/missing_etag" - ) - .into()); - } - }; - - let object_identity = completed_drive_upload_identity(resp, is_google_drive_resumable_url(&presigned_url), total_size).await; - Ok::<_, AuthedApiError>(UploadedPart { - object_identity, - etag, - part_number, - size, - total_size, - }) - } - .await; - - match upload_result { - Ok(part) => Some(Ok(part)), - Err(err) => { - warn!( - part_number = part_number, - offset = offset, - chunk_size = chunk_size, - error = %err, - "Chunk upload failed, will retry after remaining chunks" - ); - failed_chunks - .lock() - .unwrap_or_else(PoisonError::into_inner) - .push(FailedChunkInfo { - part_number, - offset, - chunk_size, - total_size, - error: err.to_string(), - }); - None - } - } - } - .instrument(info_span!("upload_part", part_number = part_number)), - (stream, expected_part_number + 1), - )) - } - }, - ) - .buffered(max_concurrent_uploads) - .filter_map(|item| async { item }) - .boxed() - }) - .chain(stream::once(async move { - debug!( - "Completed multipart upload for {video_id2:?} in {:?}", - start.elapsed() - ); - - stream::empty().boxed() - })) - .flatten() - .instrument(Span::current()) -} - -async fn read_exact_upload_chunk( - file_path: &Path, - offset: u64, - expected_size: usize, -) -> io::Result { - if expected_size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Upload chunk must not be empty", - )); - } - let path = file_path.to_path_buf(); - lifecycle::file_io(move || { - use std::io::{Read, Seek}; - let mut file = std::fs::File::open(path)?; - file.seek(std::io::SeekFrom::Start(offset))?; - let mut bytes = vec![0; expected_size]; - file.read_exact(&mut bytes)?; - Ok(Bytes::from(bytes)) - }) - .await -} - -#[instrument(skip(app, failed_chunks))] -async fn retry_failed_chunks( - app: &AppHandle, - video_id: &str, - upload_id: &str, - file_path: &Path, - failed_chunks: Vec, -) -> Result, AuthedApiError> { - retry_failed_chunks_with_input(app, video_id, upload_id, file_path, failed_chunks, None).await -} - -async fn retry_failed_chunks_with_input( - app: &AppHandle, - video_id: &str, - upload_id: &str, - file_path: &Path, - failed_chunks: Vec, - input: Option<&LegacyReplacementInput>, -) -> Result, AuthedApiError> { - let use_md5_hashes = app.is_server_url_custom().await; - let mut retry_parts = Vec::new(); - - for failed in &failed_chunks { - info!( - part_number = failed.part_number, - offset = failed.offset, - chunk_size = failed.chunk_size, - original_error = %failed.error, - "Retrying failed chunk upload" - ); - - let chunk = read_exact_upload_chunk(file_path, failed.offset, failed.chunk_size) - .await - .map_err(|error| format!("retry/part/{}/read: {error}", failed.part_number))?; - - if let Some(input) = input { - input - .inspect_chunk( - &Chunk { - total_size: failed.total_size, - part_number: failed.part_number, - offset: failed.offset, - chunk: chunk.clone(), - }, - false, - ) - .map_err(|error| error.to_string())?; - } - - let md5_sum = use_md5_hashes.then(|| base64::encode(md5::compute(&chunk).0)); - - let presigned_url = api::upload_multipart_presign_part( - app, - video_id, - upload_id, - failed.part_number, - md5_sum.as_deref(), - ) - .await?; - - let size = chunk.len(); - let chunk_for_retry = chunk.clone(); - let client = app - .state::() - .as_ref() - .map_err(|err| format!("retry/part/{}/client: {err:?}", failed.part_number))? - .clone(); - - let req = client - .put(&presigned_url) - .header("Content-Length", size) - .timeout(Duration::from_secs(5 * 60)) - .body(chunk); - let mut req = with_drive_content_range( - req, - &presigned_url, - failed.offset, - size as u64, - failed.total_size, - ); - - if let Some(md5_sum) = &md5_sum { - req = req.header("Content-MD5", md5_sum); - } - - let send_result = lifecycle::cancellable(req.send().instrument(info_span!( - "s3_put_retry", - part_number = failed.part_number, - size = size - ))) - .await? - .map_err(reqwest::Error::without_url); - - let resp = match send_result { - Ok(resp) => resp, - Err(err) if is_reqwest_network_error(&err) => { - info!( - part_number = failed.part_number, - error = %err, - "Retry chunk upload failed due to network error, waiting for connectivity" - ); - if !wait_for_network_recovery(app, video_id).await { - return Err(format!( - "retry/part/{}/network_timeout: {err:?}", - failed.part_number - ) - .into()); - } - let retry_url = api::upload_multipart_presign_part( - app, - video_id, - upload_id, - failed.part_number, - md5_sum.as_deref(), - ) - .await?; - let retry_req = client - .put(&retry_url) - .header("Content-Length", size) - .timeout(Duration::from_secs(5 * 60)) - .body(chunk_for_retry); - let mut retry_req = with_drive_content_range( - retry_req, - &retry_url, - failed.offset, - size as u64, - failed.total_size, - ); - if let Some(md5_sum) = &md5_sum { - retry_req = retry_req.header("Content-MD5", md5_sum); - } - lifecycle::cancellable(retry_req.send().instrument(info_span!( - "s3_put_retry_after_network_recovery", - part_number = failed.part_number, - size = size - ))) - .await? - .map_err(reqwest::Error::without_url) - .map_err(|err| { - format!( - "retry/part/{}/network_retry_error: {err:?}", - failed.part_number - ) - })? - } - Err(err) => { - return Err(format!("retry/part/{}/error: {err:?}", failed.part_number).into()); - } - }; - - let etag = resp - .headers() - .get("ETag") - .as_ref() - .and_then(|etag| etag.to_str().ok()) - .map(|v| v.trim_matches('"').to_string()); - - if !is_upload_response_accepted( - &presigned_url, - resp.status(), - failed.offset, - size as u64, - failed.total_size, - ) { - return Err( - format!("retry/part/{}/error: {}", failed.part_number, resp.status()).into(), - ); - } - - info!( - part_number = failed.part_number, - "Successfully retried chunk upload" - ); - - let etag = match etag { - Some(etag) => etag, - None if is_google_drive_resumable_url(&presigned_url) => { - format!("drive-{}", failed.part_number) - } - None => { - return Err(format!("retry/part/{}/missing_etag", failed.part_number).into()); - } - }; - - let object_identity = completed_drive_upload_identity( - resp, - is_google_drive_resumable_url(&presigned_url), - failed.total_size, - ) - .await; - retry_parts.push(UploadedPart { - object_identity, - etag, - part_number: failed.part_number, - size, - total_size: failed.total_size, - }); - } - - Ok(retry_parts) -} - -async fn completed_drive_upload_identity( - response: reqwest::Response, - is_drive: bool, - expected_size: u64, -) -> Option { - if !is_drive || !response.status().is_success() { - return None; - } - lifecycle::cancellable(response.json::()) - .await - .ok()? - .ok() - .and_then(|value| { - cap_recording::upload_verification::completed_drive_object_identity( - &value, - expected_size, - ) - }) -} - -fn is_reqwest_network_error(err: &reqwest::Error) -> bool { - err.is_connect() || err.is_timeout() || err.status().is_none() -} - -async fn probe_connectivity(app: &AppHandle) -> bool { - let url = app.make_app_url("/").await; - app.state::() - .head(&url) - .timeout(Duration::from_secs(5)) - .send() - .await - .is_ok() -} - -async fn wait_for_network_recovery(app: &AppHandle, video_id: &str) -> bool { - let start = Instant::now(); - let mut delay = CONNECTIVITY_PROBE_INITIAL_DELAY; - - warn!( - video_id, - "Network loss detected, pausing uploads until connectivity is restored" - ); - - loop { - if start.elapsed() > NETWORK_RECOVERY_TIMEOUT { - error!( - video_id, - elapsed_secs = start.elapsed().as_secs(), - "Network recovery timeout exceeded (5 minutes), giving up" - ); - return false; - } - - if lifecycle::cancellable(tokio::time::sleep(delay)) - .await - .is_err() - { - return false; - } - - if lifecycle::cancellable(probe_connectivity(app)) - .await - .unwrap_or(false) - { - info!( - video_id, - waited_secs = start.elapsed().as_secs(), - "Network connectivity restored, resuming uploads" - ); - return true; - } - - delay = (delay * 2).min(CONNECTIVITY_PROBE_MAX_DELAY); - } -} - -#[instrument(skip(app, stream))] -pub async fn singlepart_uploader( - app: AppHandle, - request: PresignedS3PutRequest, - total_size: u64, - stream: impl Stream> + Send + 'static, -) -> Result<(), AuthedApiError> { - let upload_target = api::upload_signed(&app, request).await?; - let presigned_url = upload_target.url; - - let mut request = app - .state::() - .as_ref() - .map_err(|err| format!("singlepart_uploader/client: {err:?}"))? - .put(&presigned_url) - .header("Content-Length", total_size) - .body(reqwest::Body::wrap_stream(stream)); - - for (key, value) in upload_target.headers { - if key.eq_ignore_ascii_case("content-length") { - continue; - } - request = request.header(key, value); - } - - let resp = lifecycle::cancellable( - with_drive_content_range(request, &presigned_url, 0, total_size, total_size).send(), - ) - .await? - .map_err(reqwest::Error::without_url) - .map_err(|err| format!("singlepart_uploader/error: {err:?}"))?; - - match !resp.status().is_success() { - true => Err(format!("singlepart_uploader/error: {}", resp.status())), - false => Ok(()), - }?; - - Ok(()) -} - -pub trait UploadedChunk { - /// total size of the file - fn total(&self) -> u64; - - /// size of the current chunk - fn size(&self) -> u64; -} - -impl UploadedChunk for UploadedPart { - fn total(&self) -> u64 { - self.total_size - } - - fn size(&self) -> u64 { - self.size as u64 - } -} - -impl UploadedChunk for Chunk { - fn total(&self) -> u64 { - self.total_size - } - - fn size(&self) -> u64 { - self.chunk.len() as u64 - } -} - -impl UploadedChunk for (u64, Bytes) { - fn total(&self) -> u64 { - self.0 - } - - fn size(&self) -> u64 { - self.1.len() as u64 - } -} - -/// Monitor the stream to report the upload progress -fn progress( - app: AppHandle, - video_id: String, - stream: impl Stream>, -) -> impl Stream> { - let mut uploaded = 0u64; - let mut pending_task: Option> = None; - let mut reemit_task: Option> = None; - - stream! { - let mut stream = pin!(stream); - - while let Some(chunk) = stream.next().await { - if let Ok(chunk) = &chunk { - uploaded += chunk.size(); - let total = chunk.total(); - - if let Some(handle) = pending_task.take() { - handle.abort(); - } - - if let Some(handle) = reemit_task.take() { - handle.abort(); - } - - let should_send_immediately = uploaded >= total; - - if should_send_immediately { - let app_clone = app.clone(); - let video_id_clone = video_id.clone(); - tokio::spawn(crate::web_api::inherit_upload_context(crate::web_api::UploadRequestContext::current(), async move { - api::desktop_video_progress(&app_clone, &video_id_clone, uploaded, total).await.ok(); - })); - } else { - let app_clone = app.clone(); - let video_id_clone = video_id.clone(); - pending_task = Some(tokio::spawn(crate::web_api::inherit_upload_context(crate::web_api::UploadRequestContext::current(), async move { - tokio::time::sleep(Duration::from_secs(2)).await; - api::desktop_video_progress(&app_clone, &video_id_clone, uploaded, total).await.ok(); - }))); - - let app_reemit = app.clone(); - let video_id_reemit = video_id.clone(); - let uploaded_reemit = uploaded; - let total_reemit = total; - reemit_task = Some(tokio::spawn(async move { - let mut interval = time::interval(Duration::from_millis(700)); - interval.tick().await; - - loop { - interval.tick().await; - UploadProgressEvent { - video_id: video_id_reemit.clone(), - uploaded: uploaded_reemit.to_string(), - total: total_reemit.to_string(), - } - .emit(&app_reemit) - .ok(); - } - })); - } - - UploadProgressEvent { - video_id: video_id.clone(), - uploaded: uploaded.to_string(), - total: total.to_string(), - } - .emit(&app) - .ok(); - } - - yield chunk; - } - - if let Some(handle) = reemit_task.take() { - handle.abort(); - } - } -} - -pub fn emit_upload_complete(app: &AppHandle, video_id: &str) { - UploadProgressEvent { - video_id: video_id.to_string(), - uploaded: "0".into(), - total: "0".into(), - } - .emit(app) - .ok(); -} - -/// Track the upload progress into a Tauri channel -fn tauri_channel_progress( - channel: Channel, - stream: impl Stream>, -) -> impl Stream> { - let mut uploaded = 0u64; - - stream! { - let mut stream = pin!(stream); - - while let Some(chunk) = stream.next().await { - if let Ok(chunk) = &chunk { - uploaded += chunk.size(); - - channel.send(UploadProgress { - progress: uploaded as f64 / chunk.total() as f64 - }) - .ok(); - } - - yield chunk; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use std::sync::atomic::{AtomicU32, Ordering}; - - #[test] - fn created_video_uses_a_custom_domain_share_link() { - let created = parse_created_video( - r#"{"id":"abc123","share_url":"https://videos.team.test/s/abc123","user_id":"u"}"#, - ) - .unwrap(); - assert_eq!(created.config.id, "abc123"); - assert_eq!( - created.share_url.as_deref(), - Some("https://videos.team.test/s/abc123") - ); - } - - #[test] - fn created_video_without_a_share_link_keeps_the_default() { - let created = parse_created_video(r#"{"id":"abc123","user_id":"u"}"#).unwrap(); - assert_eq!(created.config.id, "abc123"); - assert_eq!(created.share_url, None); - - let created = parse_created_video(r#"{"id":"abc123","share_url":null}"#).unwrap(); - assert_eq!(created.share_url, None); - } - - #[test] - fn created_video_ignores_share_links_for_other_pages() { - for share_url in [ - "", - "not a url", - "http://videos.team.test/s/abc123", - "https://videos.team.test/s/other", - "https://videos.team.test/s/abc123/extra", - "https://videos.team.test/c/abc123", - "https://videos.team.test/s/abc123?x=1", - "https://videos.team.test/s/abc123#x", - "https://user:pass@videos.team.test/s/abc123", - "https://videos.team.test:8443/s/abc123", - "javascript:alert(1)//s/abc123", - ] { - let body = serde_json::json!({ "id": "abc123", "share_url": share_url }).to_string(); - let created = parse_created_video(&body).unwrap(); - assert_eq!(created.share_url, None, "{share_url}"); - } - } - - #[tokio::test] - async fn restart_requires_a_signed_session_before_any_further_upload_work() { - for upload_id in ["legacy-again", "cap-reupload.signed"] { - let uploads = AtomicUsize::new(0); - let uploads = &uploads; - let result = with_replacement_restart(true, |restarted| async move { - validate_replacement_restart_session( - restarted, - if restarted { upload_id } else { "legacy-first" }, - )?; - uploads.fetch_add(1, Ordering::AcqRel); - Ok(if restarted { - ReplacementAttempt::Completed(()) - } else { - ReplacementAttempt::RestartRequired - }) - }) - .await; - assert_eq!(result.is_ok(), upload_id.starts_with("cap-reupload.")); - assert_eq!( - uploads.load(Ordering::Acquire), - if result.is_ok() { 2 } else { 1 } - ); - } - } - - #[tokio::test] - async fn legacy_replacement_restarts_the_entire_attempt_once() { - let stages = Mutex::new(Vec::new()); - let identity = with_replacement_restart(true, |restarted| { - let mut stages = stages.lock().unwrap(); - let session = if restarted { "fresh" } else { "legacy" }; - stages.extend([ - (session, "initiate"), - (session, "parts"), - (session, "complete"), - ]); - std::future::ready(Ok(if restarted { - ReplacementAttempt::Completed("fresh-object") - } else { - ReplacementAttempt::RestartRequired - })) - }) - .await - .unwrap(); - assert_eq!(identity, "fresh-object"); - assert_eq!( - *stages.lock().unwrap(), - vec![ - ("legacy", "initiate"), - ("legacy", "parts"), - ("legacy", "complete"), - ("fresh", "initiate"), - ("fresh", "parts"), - ("fresh", "complete"), - ] - ); - } - - #[tokio::test] - async fn replacement_restart_is_bounded_and_never_turns_failure_into_success() { - for (replace_existing, expected) in [(false, 1), (true, 2)] { - let calls = AtomicUsize::new(0); - let result = with_replacement_restart(replace_existing, |_| { - calls.fetch_add(1, Ordering::AcqRel); - std::future::ready(Ok::, AuthedApiError>( - ReplacementAttempt::RestartRequired, - )) - }) - .await; - assert!(result.is_err()); - assert_eq!(calls.load(Ordering::Acquire), expected); - } - for restart_first in [false, true] { - let calls = AtomicUsize::new(0); - let result = with_replacement_restart(true, |restarted| { - calls.fetch_add(1, Ordering::AcqRel); - std::future::ready(if restart_first && !restarted { - Ok(ReplacementAttempt::<()>::RestartRequired) - } else { - Err(AuthedApiError::InvalidAuthentication) - }) - }) - .await; - assert!(matches!(result, Err(AuthedApiError::InvalidAuthentication))); - assert_eq!( - calls.load(Ordering::Acquire), - if restart_first { 2 } else { 1 } - ); - } - } - - #[tokio::test] - async fn successful_or_dropped_attempt_cannot_start_a_replacement() { - let calls = AtomicUsize::new(0); - let result = with_replacement_restart(true, |_| { - calls.fetch_add(1, Ordering::AcqRel); - std::future::ready(Ok(ReplacementAttempt::Completed(None::))) - }) - .await - .unwrap(); - assert_eq!(result, None); - assert_eq!(calls.load(Ordering::Acquire), 1); - let future = with_replacement_restart(true, |_| { - calls.fetch_add(1, Ordering::AcqRel); - std::future::pending::, AuthedApiError>>() - }); - let mut future = Box::pin(future); - assert!(matches!( - futures::poll!(&mut future), - std::task::Poll::Pending - )); - drop(future); - assert_eq!(calls.load(Ordering::Acquire), 2); - } - - #[test] - fn only_raw_replacement_sessions_need_byte_receipts() { - for replace_existing in [false, true] { - assert!(!legacy_replacement_session( - replace_existing, - "cap-reupload.signed-token" - )); - } - for upload_id in [ - "raw-s3-id", - "https://www.googleapis.com/upload/raw-drive-session", - ] { - assert!(!legacy_replacement_session(false, upload_id)); - assert!(legacy_replacement_session(true, upload_id)); - } - } - - fn replacement_input() -> LegacyReplacementInput { - LegacyReplacementInput { - path: PathBuf::from("unused"), - size: 4, - modified: std::time::SystemTime::UNIX_EPOCH, - chunks: Mutex::new(HashMap::new()), - } - } - - fn replacement_chunk(bytes: &'static [u8]) -> Chunk { - Chunk { - total_size: 4, - part_number: 1, - offset: 0, - chunk: Bytes::from_static(bytes), - } - } - - #[test] - fn restarted_and_failed_part_reads_must_match_original_bytes() { - let input = replacement_input(); - let original = replacement_chunk(b"test"); - assert!(input.inspect_chunk(&original, false).is_err()); - input.inspect_chunk(&original, true).unwrap(); - input.inspect_chunk(&original, false).unwrap(); - for changed in [ - replacement_chunk(b"best"), - Chunk { - offset: 1, - ..replacement_chunk(b"test") - }, - Chunk { - total_size: 5, - ..replacement_chunk(b"test") - }, - Chunk { - part_number: 2, - ..replacement_chunk(b"test") - }, - replacement_chunk(b""), - ] { - assert!(input.inspect_chunk(&changed, false).is_err()); - } - assert!( - input - .inspect_chunk(&replacement_chunk(b"best"), true) - .is_err() - ); - input.inspect_chunk(&original, false).unwrap(); - } - - #[test] - fn replacement_completion_requires_the_complete_verified_inventory() { - let input = replacement_input(); - input - .inspect_chunk(&replacement_chunk(b"test"), true) - .unwrap(); - assert!(input.verify_parts(&[]).is_err()); - let mut parts = vec![UploadedPart { - part_number: 1, - etag: "fresh-etag".into(), - size: 4, - total_size: 4, - object_identity: None, - }]; - input.verify_parts(&parts).unwrap(); - parts[0].size = 3; - assert!(input.verify_parts(&parts).is_err()); - parts[0].size = 4; - parts[0].total_size = 5; - assert!(input.verify_parts(&parts).is_err()); - parts[0].total_size = 4; - parts[0].part_number = 2; - assert!(input.verify_parts(&parts).is_err()); - } - - #[tokio::test] - async fn changed_local_input_blocks_fresh_initiation_after_restart_response() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("output.mp4"); - std::fs::write(&path, b"test").unwrap(); - let input = LegacyReplacementInput::capture(&path).await.unwrap(); - let initiations = AtomicUsize::new(0); - let result = with_replacement_restart(true, |restarted| { - let path = &path; - let input = &input; - let initiations = &initiations; - async move { - input.verify(path).await?; - initiations.fetch_add(1, Ordering::AcqRel); - if !restarted { - std::fs::write(path, b"changed").unwrap(); - return Ok(ReplacementAttempt::RestartRequired); - } - Ok(ReplacementAttempt::Completed(())) - } - }) - .await; - assert!(result.is_err()); - assert_eq!(initiations.load(Ordering::Acquire), 1); - assert_eq!(std::fs::read(&path).unwrap(), b"changed"); - } - - #[test] - fn reupload_keeps_sharing_identity_after_completion_or_failure() { - let sharing = cap_project::SharingMeta { - id: "existing-video".into(), - link: "https://cap.link/existing-video".into(), - content_hash: None, - }; - for upload in [ - None, - Some(cap_project::UploadMeta::Complete), - Some(cap_project::UploadMeta::Failed { - error: "offline".into(), - }), - ] { - assert_eq!( - reusable_video_id(Some(&sharing), upload.as_ref()).as_deref(), - Some("existing-video") - ); - } - } - - #[test] - fn retry_keeps_the_reserved_identity_until_sharing_is_saved() { - let upload = cap_project::UploadMeta::SinglePartUpload { - video_id: "reserved-video".into(), - file_path: "output.mp4".into(), - screenshot_path: "display.jpg".into(), - recording_dir: "recording.cap".into(), - }; - assert_eq!( - reusable_video_id(None, Some(&upload)).as_deref(), - Some("reserved-video") - ); - assert_eq!(reusable_video_id(None, None), None); - } - - #[test] - fn active_upload_guards_track_overlapping_sessions() { - let active = AtomicUsize::new(0); - let first = ActiveUploadGuard::new(&active); - let second = ActiveUploadGuard::new(&active); - - assert_eq!(active.load(Ordering::Acquire), 2); - drop(first); - assert_eq!(active.load(Ordering::Acquire), 1); - drop(second); - assert_eq!(active.load(Ordering::Acquire), 0); - } - - #[test] - fn dropping_an_upload_future_releases_its_session() { - let active = AtomicUsize::new(0); - let mut upload = Box::pin(async { - let _guard = ActiveUploadGuard::new(&active); - std::future::pending::<()>().await; - }); - - let mut context = std::task::Context::from_waker(std::task::Waker::noop()); - assert!(std::future::Future::poll(upload.as_mut(), &mut context).is_pending()); - assert_eq!(active.load(Ordering::Acquire), 1); - drop(upload); - assert_eq!(active.load(Ordering::Acquire), 0); - } - - #[test] - fn screenshot_upload_subpath_matches_content_type() { - assert_eq!( - screenshot_upload_subpath("image/png"), - "screenshot/screen-capture.png" - ); - assert_eq!( - screenshot_upload_subpath("image/jpeg"), - "screenshot/screen-capture.jpg" - ); - assert_eq!( - screenshot_content_type_from_path(Path::new("screen-capture.png")), - "image/png" - ); - assert_eq!( - screenshot_content_type_from_path(Path::new("screen-capture.jpg")), - "image/jpeg" - ); - } - - #[tokio::test] - async fn multipart_retry_reads_exact_file_range_without_padding() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("output.mp4"); - let data = b"init-video-fragment-final"; - std::fs::write(&path, data).unwrap(); - - let chunk = read_exact_upload_chunk(&path, 5, 14).await.unwrap(); - assert_eq!(chunk.as_ref(), &data[5..19]); - - for (offset, size) in [(5, data.len()), (data.len() as u64, 1)] { - let error = read_exact_upload_chunk(&path, offset, size) - .await - .unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof); - } - assert_eq!( - read_exact_upload_chunk(&path, 0, 0) - .await - .unwrap_err() - .kind(), - io::ErrorKind::InvalidInput - ); - - std::fs::write(&path, b"").unwrap(); - assert_eq!( - read_exact_upload_chunk(&path, 0, 1) - .await - .unwrap_err() - .kind(), - io::ErrorKind::UnexpectedEof - ); - } - - #[tokio::test] - async fn read_segment_data_returns_existing_file() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("segment_001.m4s"); - std::fs::write(&path, b"fake segment data 12345").unwrap(); - - let result = SegmentUploader::read_segment_data( - &path, - "segments/video/segment_001.m4s", - b"fake segment data 12345".len() as u64, - ) - .await; - - let data = result.unwrap(); - assert_eq!(data.as_ref(), b"fake segment data 12345"); - } - - #[tokio::test] - async fn read_segment_data_waits_for_file_to_appear() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("segment_002.m4s"); - - let path_clone = path.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(200)).await; - std::fs::write(&path_clone, b"delayed segment data").unwrap(); - }); - - let start = Instant::now(); - let result = SegmentUploader::read_segment_data( - &path, - "segments/video/segment_002.m4s", - b"delayed segment data".len() as u64, - ) - .await; - let elapsed = start.elapsed(); - - let data = result.unwrap(); - assert_eq!(data.as_ref(), b"delayed segment data"); - assert!( - elapsed >= Duration::from_millis(150), - "should have waited for file: {elapsed:?}" - ); - assert!( - elapsed < Duration::from_secs(2), - "should not have waited too long: {elapsed:?}" - ); - } - - #[tokio::test] - async fn read_segment_data_waits_for_expected_size() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("segment_003.m4s"); - - let mut file = std::fs::File::create(&path).unwrap(); - file.write_all(b"sm").unwrap(); - file.flush().unwrap(); - drop(file); - - let path_clone = path.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(200)).await; - std::fs::write(&path_clone, b"full segment data here!").unwrap(); - }); - - let result = SegmentUploader::read_segment_data( - &path, - "segments/video/segment_003.m4s", - b"full segment data here!".len() as u64, - ) - .await; - - let data = result.unwrap(); - assert_eq!(data.as_ref(), b"full segment data here!"); - } - - #[tokio::test] - async fn read_segment_data_rejects_empty_short_oversized_and_unknown_size() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("segment.m4s"); - for (bytes, expected_size) in [ - (b"".as_slice(), 5), - (b"tiny".as_slice(), 5), - (b"too long".as_slice(), 5), - (b"valid".as_slice(), 0), - ] { - std::fs::write(&path, bytes).unwrap(); - let result = SegmentUploader::read_segment_data_with_timeouts( - &path, - "segments/video/segment.m4s", - expected_size, - Duration::ZERO, - Duration::ZERO, - ) - .await; - assert!(result.is_err(), "accepted {bytes:?} for {expected_size}"); - } - } - - #[tokio::test] - async fn read_segment_data_rejects_missing_and_unreadable_input() { - let dir = tempfile::tempdir().unwrap(); - for path in [dir.path().join("missing.m4s"), dir.path().to_path_buf()] { - let result = SegmentUploader::read_segment_data_with_timeouts( - &path, - "segments/video/segment.m4s", - 5, - Duration::ZERO, - Duration::ZERO, - ) - .await; - assert!(result.is_err()); - } - } - - #[tokio::test] - async fn read_segment_data_polls_at_50ms_not_200ms() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("segment_fast.m4s"); - - let path_clone = path.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(120)).await; - std::fs::write(&path_clone, b"fast poll data").unwrap(); - }); - - let start = Instant::now(); - let result = SegmentUploader::read_segment_data( - &path, - "segments/video/segment_fast.m4s", - b"fast poll data".len() as u64, - ) - .await; - let elapsed = start.elapsed(); - - assert!(result.is_ok()); - assert!( - elapsed < Duration::from_millis(300), - "50ms polling should detect file within ~170ms, got {elapsed:?}" - ); - } - - #[tokio::test] - async fn url_cache_returns_prefetched_url() { - let cache = PresignedUrlCache::new(); - { - let mut urls = cache.urls.lock().await; - urls.insert( - "segments/video/segment_001.m4s".to_string(), - "https://s3.example.com/presigned/seg1".to_string(), - ); - urls.insert( - "segments/video/segment_002.m4s".to_string(), - "https://s3.example.com/presigned/seg2".to_string(), - ); - } - - let mut urls = cache.urls.lock().await; - let url1 = urls.remove("segments/video/segment_001.m4s"); - let url2 = urls.remove("segments/video/segment_002.m4s"); - let url3 = urls.remove("segments/video/segment_003.m4s"); - drop(urls); - - assert_eq!( - url1.as_deref(), - Some("https://s3.example.com/presigned/seg1") - ); - assert_eq!( - url2.as_deref(), - Some("https://s3.example.com/presigned/seg2") - ); - assert!(url3.is_none(), "uncached key should return None"); - } - - #[tokio::test] - async fn url_cache_consumes_entry_on_use() { - let cache = PresignedUrlCache::new(); - { - let mut urls = cache.urls.lock().await; - urls.insert( - "segments/video/init.mp4".to_string(), - "https://s3.example.com/presigned/init".to_string(), - ); - } - - let mut urls = cache.urls.lock().await; - let first = urls.remove("segments/video/init.mp4"); - let second = urls.remove("segments/video/init.mp4"); - drop(urls); - - assert!(first.is_some(), "first access should hit cache"); - assert!(second.is_none(), "second access should miss (consumed)"); - } - - #[tokio::test] - async fn semaphore_allows_six_concurrent_uploads() { - let semaphore = Arc::new(tokio::sync::Semaphore::new(6)); - let active = Arc::new(AtomicU32::new(0)); - let peak = Arc::new(AtomicU32::new(0)); - let completed = Arc::new(AtomicU32::new(0)); - - let mut handles = Vec::new(); - for _ in 0..12 { - let sem = semaphore.clone(); - let active = active.clone(); - let peak = peak.clone(); - let completed = completed.clone(); - handles.push(tokio::spawn(async move { - let _permit = sem.acquire().await.unwrap(); - let current = active.fetch_add(1, Ordering::SeqCst) + 1; - peak.fetch_max(current, Ordering::SeqCst); - tokio::time::sleep(Duration::from_millis(50)).await; - active.fetch_sub(1, Ordering::SeqCst); - completed.fetch_add(1, Ordering::SeqCst); - })); - } - - for h in handles { - h.await.unwrap(); - } - - assert_eq!(completed.load(Ordering::SeqCst), 12); - assert_eq!( - peak.load(Ordering::SeqCst), - 6, - "peak concurrency should be 6" - ); - } - - #[tokio::test] - async fn file_read_runs_outside_semaphore() { - let semaphore = Arc::new(tokio::sync::Semaphore::new(2)); - let dir = tempfile::tempdir().unwrap(); - - let read_start_times = Arc::new(tokio::sync::Mutex::new(Vec::::new())); - let overall_start = Instant::now(); - - let mut handles = Vec::new(); - for i in 0..6u32 { - let path = dir.path().join(format!("seg_{i:03}.m4s")); - std::fs::write(&path, format!("data for segment {i}")).unwrap(); - - let sem = semaphore.clone(); - let times = read_start_times.clone(); - handles.push(tokio::spawn(async move { - { - let mut t = times.lock().await; - t.push(Instant::now()); - } - let _data = tokio::fs::read(&path).await.unwrap(); - - let _permit = sem.acquire().await.unwrap(); - tokio::time::sleep(Duration::from_millis(100)).await; - })); - } - - for h in handles { - h.await.unwrap(); - } - - let times = read_start_times.lock().await; - let max_read_delay = times - .iter() - .map(|t| t.duration_since(overall_start)) - .max() - .unwrap(); - - assert!( - max_read_delay < Duration::from_millis(50), - "all file reads should start nearly immediately (not blocked by semaphore), max delay was {max_read_delay:?}" - ); - } - - #[tokio::test] - async fn manifest_debounce_reduces_upload_count() { - let dirty = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let notify = Arc::new(tokio::sync::Notify::new()); - let upload_count = Arc::new(AtomicU32::new(0)); - - let manifest_handle = { - let dirty = dirty.clone(); - let shutdown = shutdown.clone(); - let notify = notify.clone(); - let upload_count = upload_count.clone(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_millis(100)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = interval.tick() => {} - _ = notify.notified() => {} - } - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - upload_count.fetch_add(1, Ordering::SeqCst); - } - if shutdown.load(std::sync::atomic::Ordering::Acquire) { - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - upload_count.fetch_add(1, Ordering::SeqCst); - } - break; - } - } - }) - }; - - for _ in 0..20 { - dirty.store(true, std::sync::atomic::Ordering::Release); - tokio::time::sleep(Duration::from_millis(10)).await; - } - - tokio::time::sleep(Duration::from_millis(200)).await; - - shutdown.store(true, std::sync::atomic::Ordering::Release); - dirty.store(true, std::sync::atomic::Ordering::Release); - notify.notify_one(); - manifest_handle.await.unwrap(); - - let count = upload_count.load(Ordering::SeqCst); - assert!( - count < 10, - "debounce should batch 20 dirty flags into far fewer uploads, got {count}" - ); - assert!( - count >= 1, - "should have uploaded manifest at least once, got {count}" - ); - } - - #[tokio::test] - async fn manifest_notify_wakes_immediately_on_shutdown() { - let dirty = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let notify = Arc::new(tokio::sync::Notify::new()); - let upload_count = Arc::new(AtomicU32::new(0)); - - let manifest_handle = { - let dirty = dirty.clone(); - let shutdown = shutdown.clone(); - let notify = notify.clone(); - let upload_count = upload_count.clone(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(60)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = interval.tick() => {} - _ = notify.notified() => {} - } - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - upload_count.fetch_add(1, Ordering::SeqCst); - } - if shutdown.load(std::sync::atomic::Ordering::Acquire) { - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - upload_count.fetch_add(1, Ordering::SeqCst); - } - break; - } - } - }) - }; - - tokio::time::sleep(Duration::from_millis(50)).await; - - let start = Instant::now(); - shutdown.store(true, std::sync::atomic::Ordering::Release); - dirty.store(true, std::sync::atomic::Ordering::Release); - notify.notify_one(); - manifest_handle.await.unwrap(); - let elapsed = start.elapsed(); - - assert!( - elapsed < Duration::from_millis(100), - "notify should wake task immediately, not wait for interval. took {elapsed:?}" - ); - assert_eq!(upload_count.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn parallel_file_read_and_url_fetch() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("segment_001.m4s"); - std::fs::write(&path, vec![0u8; 4096]).unwrap(); - - let start = Instant::now(); - let (file_result, url_result) = tokio::join!( - async { - tokio::time::sleep(Duration::from_millis(100)).await; - tokio::fs::read(&path).await - }, - async { - tokio::time::sleep(Duration::from_millis(100)).await; - Ok::("https://s3.example.com/presigned".to_string()) - }, - ); - let elapsed = start.elapsed(); - - assert!(file_result.is_ok()); - assert!(url_result.is_ok()); - assert!( - elapsed < Duration::from_millis(180), - "parallel operations (100ms each) should complete in ~100ms, not ~200ms. took {elapsed:?}" - ); - } - - #[tokio::test] - async fn new_architecture_faster_than_old() { - let segment_count = 12u32; - - let old_start = Instant::now(); - { - let semaphore = Arc::new(tokio::sync::Semaphore::new(3)); - let completed = Arc::new(AtomicU32::new(0)); - let mut in_flight = futures::stream::FuturesUnordered::new(); - - for _ in 0..segment_count { - let permit = semaphore.clone().acquire_owned().await.unwrap(); - let completed = completed.clone(); - in_flight.push(tokio::spawn(async move { - let _permit = permit; - tokio::time::sleep(Duration::from_millis(10)).await; - tokio::time::sleep(Duration::from_millis(50)).await; - tokio::time::sleep(Duration::from_millis(30)).await; - completed.fetch_add(1, Ordering::SeqCst); - })); - } - - use futures::StreamExt; - while in_flight.next().await.is_some() {} - } - let old_duration = old_start.elapsed(); - - let new_start = Instant::now(); - { - let semaphore = Arc::new(tokio::sync::Semaphore::new(6)); - let completed = Arc::new(AtomicU32::new(0)); - let manifest_uploads = Arc::new(AtomicU32::new(0)); - let mut in_flight = futures::stream::FuturesUnordered::new(); - - for _ in 0..segment_count { - let semaphore = semaphore.clone(); - let completed = completed.clone(); - let manifest_uploads = manifest_uploads.clone(); - in_flight.push(tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(10)).await; - - let _permit = semaphore.acquire_owned().await.unwrap(); - tokio::time::sleep(Duration::from_millis(50)).await; - drop(_permit); - - completed.fetch_add(1, Ordering::SeqCst); - let _ = &manifest_uploads; - })); - } - - use futures::StreamExt; - while in_flight.next().await.is_some() {} - } - let new_duration = new_start.elapsed(); - - assert!( - new_duration < old_duration, - "new architecture ({new_duration:?}) should be faster than old ({old_duration:?})" - ); - - let speedup = - old_duration.as_secs_f64() / new_duration.as_secs_f64().max(f64::MIN_POSITIVE); - assert!( - speedup > 1.5, - "expected at least 1.5x speedup, got {speedup:.2}x (old={old_duration:?}, new={new_duration:?})" - ); - } - - fn upload_state_with_video() -> SegmentUploadState { - let mut state = SegmentUploadState::new(); - state.video_init_uploaded = true; - state.uploaded_video_segments.insert(1, 3.0); - state - } - - #[tokio::test] - async fn failed_required_segment_prevents_all_completion_side_effects() { - use cap_enc_ffmpeg::segmented_stream::SegmentMediaType; - - for (media_type, is_init) in [ - (SegmentMediaType::Video, true), - (SegmentMediaType::Video, false), - (SegmentMediaType::Audio, true), - (SegmentMediaType::Audio, false), - ] { - let mut state = upload_state_with_video(); - state.failed_segments.push(FailedSegmentInfo { - subpath: "required segment".to_string(), - file_path: PathBuf::from("required.m4s"), - is_init, - media_type, - index: 2, - duration: 3.0, - expected_size: 7, - }); - let state = Mutex::new(state); - let published = AtomicUsize::new(0); - let result = publish_completed_segments(&state, |_| async { - published.fetch_add(1, Ordering::SeqCst); - Ok(()) - }) - .await; - assert!(result.is_err()); - assert_eq!(published.load(Ordering::SeqCst), 0); - assert_eq!(state.lock().unwrap().failed_segments.len(), 1); - } - } - - #[tokio::test] - async fn successful_required_segment_retry_restores_completion() { - let mut state = upload_state_with_video(); - state.failed_segments.push(FailedSegmentInfo { - subpath: "segments/video/segment_002.m4s".to_string(), - file_path: PathBuf::from("segment_002.m4s"), - is_init: false, - media_type: cap_enc_ffmpeg::segmented_stream::SegmentMediaType::Video, - index: 2, - duration: 3.0, - expected_size: 7, - }); - assert!(state.completed_manifest().is_err()); - state.uploaded_video_segments.insert(2, 3.0); - state.failed_segments.clear(); - let published = AtomicUsize::new(0); - let result = publish_completed_segments(&Mutex::new(state), |manifest| { - assert!(manifest.is_complete); - assert_eq!(manifest.video_segments.len(), 2); - published.fetch_add(1, Ordering::SeqCst); - async { Ok(()) } - }) - .await; - assert!(result.is_ok()); - assert_eq!(published.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn missing_video_and_publication_errors_do_not_become_success() { - let result: Result<(), AuthedApiError> = - publish_completed_segments(&Mutex::new(SegmentUploadState::new()), |_| async { - panic!("an empty upload must not publish") - }) - .await; - assert!(result.is_err()); - - let result: Result<(), AuthedApiError> = - publish_completed_segments(&Mutex::new(upload_state_with_video()), |_| async { - Err("injected manifest failure".into()) - }) - .await; - assert!( - result - .unwrap_err() - .to_string() - .contains("injected manifest failure") - ); - } - - #[tokio::test] - async fn panicked_and_cancelled_upload_workers_block_completion() { - for cancel in [false, true] { - let task: tokio::task::JoinHandle> = if cancel { - let task = tokio::spawn(std::future::pending()); - task.abort(); - task - } else { - tokio::spawn(async { panic!("injected segment worker panic") }) - }; - let mut state = upload_state_with_video(); - state.record_task_result(task.await); - let result: Result<(), AuthedApiError> = - publish_completed_segments(&Mutex::new(state), |_| async { - panic!("a failed worker must not publish") - }) - .await; - assert!(result.is_err()); - } - } - - #[tokio::test] - async fn failed_upload_worker_does_not_skip_joining_other_owned_work() { - let state = Mutex::new(upload_state_with_video()); - let mut tasks = SegmentUploadTasks::new(); - let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); - let (release_tx, release_rx) = tokio::sync::oneshot::channel(); - tasks.push(tokio::spawn(async { - Err("injected upload failure".into()) - })); - tasks.push(tokio::spawn(async move { - entered_tx.send(()).unwrap(); - release_rx.await.unwrap(); - Ok(()) - })); - entered_rx.await.unwrap(); - let mut drain = Box::pin(drain_segment_upload_tasks(&state, &mut tasks)); - assert!(futures::poll!(&mut drain).is_pending()); - release_tx.send(()).unwrap(); - drain.await; - assert!(tasks.is_empty()); - let result: Result<(), AuthedApiError> = publish_completed_segments(&state, |_| async { - panic!("a failed upload must not publish after its workers are joined") - }) - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn optional_prefetch_panic_does_not_block_successful_media_completion() { - let mut tasks = futures::stream::FuturesUnordered::new(); - let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); - tasks.push(tokio::spawn(async move { - entered_tx.send(()).unwrap(); - panic!("injected optional URL prefetch panic"); - })); - entered_rx.await.unwrap(); - assert!(tasks.iter().all(tokio::task::JoinHandle::is_finished)); - cancel_segment_prefetch_tasks(&mut tasks).await; - assert!(tasks.is_empty()); - - let published = AtomicUsize::new(0); - let result = - publish_completed_segments(&Mutex::new(upload_state_with_video()), |_| async { - published.fetch_add(1, Ordering::SeqCst); - Ok(()) - }) - .await; - assert!(result.is_ok()); - assert_eq!(published.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn upload_state_manifest_generation() { - let state = SegmentUploadState::new(); - let manifest = state.to_manifest(); - assert!(!manifest.is_complete); - assert!(!manifest.video_init_uploaded); - assert!(!manifest.audio_init_uploaded); - assert!(manifest.video_segments.is_empty()); - assert!(manifest.audio_segments.is_empty()); - } - - #[tokio::test] - async fn upload_state_tracks_segments() { - let mut state = SegmentUploadState::new(); - state.video_init_uploaded = true; - state.audio_init_uploaded = true; - state.uploaded_video_segments.insert(1, 3.0); - state.uploaded_video_segments.insert(2, 3.0); - state.uploaded_audio_segments.insert(1, 3.0); - - let manifest = state.to_manifest(); - assert!(manifest.video_init_uploaded); - assert!(manifest.audio_init_uploaded); - assert_eq!(manifest.video_segments.len(), 2); - assert_eq!(manifest.audio_segments.len(), 1); - assert!(!manifest.is_complete); - - let complete = state.to_complete_manifest(); - assert!(complete.is_complete); - assert_eq!(complete.video_segments.len(), 2); - assert!(complete.has_video_content()); - } - - #[tokio::test] - async fn upload_state_without_video_segments_has_no_video_content() { - let mut state = SegmentUploadState::new(); - state.video_init_uploaded = true; - state.audio_init_uploaded = true; - - let complete = state.to_complete_manifest(); - assert!(complete.is_complete); - assert!(!complete.has_video_content()); - } - - #[tokio::test] - async fn end_to_end_pipeline_simulation() { - let segment_count = 10u32; - let semaphore = Arc::new(tokio::sync::Semaphore::new(6)); - let state = Arc::new(Mutex::new(SegmentUploadState::new())); - let manifest_dirty = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let manifest_shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let manifest_notify = Arc::new(tokio::sync::Notify::new()); - let manifest_upload_count = Arc::new(AtomicU32::new(0)); - - let manifest_handle = { - let dirty = manifest_dirty.clone(); - let shutdown = manifest_shutdown.clone(); - let notify = manifest_notify.clone(); - let count = manifest_upload_count.clone(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_millis(100)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = interval.tick() => {} - _ = notify.notified() => {} - } - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - tokio::time::sleep(Duration::from_millis(20)).await; - count.fetch_add(1, Ordering::SeqCst); - } - if shutdown.load(std::sync::atomic::Ordering::Acquire) { - if dirty.swap(false, std::sync::atomic::Ordering::AcqRel) { - count.fetch_add(1, Ordering::SeqCst); - } - break; - } - } - }) - }; - - let dir = tempfile::tempdir().unwrap(); - let start = Instant::now(); - let mut handles = Vec::new(); - - for i in 0..segment_count { - let path = dir.path().join(format!("segment_{i:03}.m4s")); - std::fs::write(&path, vec![0u8; 1024]).unwrap(); - - let sem = semaphore.clone(); - let st = state.clone(); - let dirty = manifest_dirty.clone(); - let mn = manifest_notify.clone(); - handles.push(tokio::spawn(async move { - let (_data, _url) = tokio::join!(tokio::fs::read(&path), async { - Ok::<_, ()>("https://example.com/presigned") - },); - - let _permit = sem.acquire_owned().await.unwrap(); - tokio::time::sleep(Duration::from_millis(50)).await; - drop(_permit); - - { - let mut s = st.lock().unwrap_or_else(|e| e.into_inner()); - s.uploaded_video_segments.insert(i, 3.0); - s.total_bytes_uploaded += 1024; - } - dirty.store(true, std::sync::atomic::Ordering::Release); - mn.notify_one(); - })); - } - - for h in handles { - h.await.unwrap(); - } - - let upload_duration = start.elapsed(); - - let shutdown_start = Instant::now(); - manifest_shutdown.store(true, std::sync::atomic::Ordering::Release); - manifest_dirty.store(true, std::sync::atomic::Ordering::Release); - manifest_notify.notify_one(); - manifest_handle.await.unwrap(); - let shutdown_duration = shutdown_start.elapsed(); - - let manifest_count = manifest_upload_count.load(Ordering::SeqCst); - let final_state = state.lock().unwrap(); - - assert_eq!( - final_state.uploaded_video_segments.len(), - segment_count as usize, - "all segments should be tracked" - ); - assert_eq!( - final_state.total_bytes_uploaded, - segment_count as u64 * 1024 - ); - assert!( - manifest_count < segment_count, - "manifest uploads ({manifest_count}) should be fewer than segment count ({segment_count})" - ); - assert!( - manifest_count >= 1, - "at least one manifest upload should happen" - ); - - let per_segment_ms = upload_duration.as_millis() as f64 / segment_count as f64; - assert!( - per_segment_ms < 40.0, - "with concurrency 6, per-segment time should be well under 50ms, got {per_segment_ms:.1}ms" - ); - - assert!( - shutdown_duration < Duration::from_millis(200), - "manifest shutdown should be near-instant via Notify, took {shutdown_duration:?}" - ); - } -} - -#[cfg(target_os = "linux")] -pub(crate) mod strict_instant { - use super::*; - use cap_enc_ffmpeg::segmented_stream::{SegmentCompletedEvent, SegmentMediaType}; - use futures::{FutureExt as _, stream::FuturesUnordered}; - use std::{future::Future, panic::AssertUnwindSafe, sync::atomic::AtomicBool}; - - #[derive(Clone)] - pub struct Control(Arc); - - struct ControlInner { - decision: tokio::sync::watch::Sender, - cleanup: tokio::sync::watch::Sender>, - } - - pub struct Permission { - control: Control, - granted: bool, - } - - impl Control { - pub fn new() -> (Self, Permission) { - let (decision, _) = tokio::sync::watch::channel(0); - let (cleanup, _) = tokio::sync::watch::channel(None); - let control = Self(Arc::new(ControlInner { decision, cleanup })); - ( - control.clone(), - Permission { - control, - granted: false, - }, - ) - } - - pub fn deny(&self) { - self.0.decision.send_replace(2); - } - - pub fn check(&self) -> Result<(), AuthedApiError> { - if *self.0.decision.borrow() == 2 { - Err("Instant upload authorization revoked".to_string().into()) - } else { - Ok(()) - } - } - - pub async fn step(&self, step: impl FnOnce() -> F) -> Result - where - F: Future>, - { - self.check()?; - let result = step().await; - self.check()?; - result - } - - async fn permission(&self) -> Result<(), AuthedApiError> { - let mut decision = self.0.decision.subscribe(); - loop { - let value = *decision.borrow_and_update(); - match value { - 1 => return self.check(), - 2 => return Err("Instant completion was denied".to_string().into()), - _ => decision - .changed() - .await - .map_err(|_| "Instant authorization owner lost")?, - } - } - } - - async fn cancelled(&self) { - let mut decision = self.0.decision.subscribe(); - loop { - if *decision.borrow_and_update() == 2 { - return; - } - if decision.changed().await.is_err() { - return; - } - } - } - - pub async fn joined(&self) -> bool { - let mut cleanup = self.0.cleanup.subscribe(); - loop { - let state = *cleanup.borrow_and_update(); - if let Some(joined) = state { - return joined; - } - if cleanup.changed().await.is_err() { - return false; - } - } - } - } - - impl Permission { - pub fn grant(mut self) -> Result<(), AuthedApiError> { - let changed = self.control.0.decision.send_if_modified(|state| { - if *state != 0 { - return false; - } - *state = 1; - true - }); - if !changed { - return Err("Instant completion permission is no longer valid" - .to_string() - .into()); - } - self.granted = true; - self.control.check() - } - } - - impl Drop for Permission { - fn drop(&mut self) { - if !self.granted { - self.control.deny(); - } - } - } - - struct WorkerGuard(Control); - impl Drop for WorkerGuard { - fn drop(&mut self) { - let missing = self.0.0.cleanup.borrow().is_none(); - if missing { - self.0.deny(); - let _ = self.0.0.cleanup.send_replace(Some(false)); - } - } - } - - struct Bridge { - events: tokio::sync::mpsc::Receiver, - stop: Arc, - thread: Option>, - } - - impl Bridge { - fn new( - source: std::sync::mpsc::Receiver, - ) -> Result { - let (sender, events) = tokio::sync::mpsc::channel(12); - let stop = Arc::new(AtomicBool::new(false)); - let stop_thread = stop.clone(); - let thread = std::thread::Builder::new() - .name("strict-instant-events".into()) - .spawn(move || { - while !stop_thread.load(Ordering::Acquire) { - match source.recv_timeout(Duration::from_millis(100)) { - Ok(event) => { - if sender.blocking_send(event).is_err() { - break; - } - } - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} - Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, - } - } - }) - .map_err(|error| format!("Starting owned Instant event bridge: {error}"))?; - Ok(Self { - events, - stop, - thread: Some(thread), - }) - } - - async fn finish(mut self) -> bool { - self.events.close(); - self.stop.store(true, Ordering::Release); - let thread = self.thread.take().unwrap(); - tokio::task::spawn_blocking(move || thread.join().is_ok()) - .await - .unwrap_or(false) - } - } - - impl Drop for Bridge { - fn drop(&mut self) { - self.events.close(); - self.stop.store(true, Ordering::Release); - if let Some(thread) = self.thread.take() { - drop(tokio::task::spawn_blocking(move || thread.join())); - } - } - } - - trait Transport: Send + Sync { - fn put( - &self, - path: &str, - bytes: Bytes, - ) -> impl Future> + Send; - fn complete( - &self, - manifest: &SegmentUploadManifest, - required_audio: bool, - ) -> impl Future> + Send; - } - - struct LiveTransport<'a> { - app: &'a AppHandle, - video_id: &'a str, - control: &'a Control, - session: Option<&'a lifecycle::Session>, - } - - impl Transport for LiveTransport<'_> { - async fn put(&self, path: &str, bytes: Bytes) -> Result<(), AuthedApiError> { - let mut failure = None; - for attempt in 0..3 { - if attempt > 0 { - self.control - .step(|| async { - tokio::time::sleep(Duration::from_millis(500u64 << (attempt - 1))) - .await; - Ok(()) - }) - .await?; - } - let target = self - .control - .step(|| { - api::upload_signed( - self.app, - PresignedS3PutRequest { - video_id: self.video_id.into(), - subpath: path.into(), - method: PresignedS3PutRequestMethod::Put, - meta: None, - }, - ) - }) - .await?; - let size = bytes.len() as u64; - let mut request = self - .app - .state::() - .put(&target.url) - .header("Content-Length", size) - .header("Content-Type", content_type_for_upload_subpath(path)) - .timeout(Duration::from_secs(5 * 60)) - .body(bytes.clone()); - for (name, value) in target.headers { - if !name.eq_ignore_ascii_case("content-length") { - request = request.header(name, value); - } - } - let result = self - .control - .step(|| async { - let response = - with_drive_content_range(request, &target.url, 0, size, size) - .send() - .await - .map_err(AuthedApiError::from)?; - if !response.status().is_success() { - return Err(format!("Instant PUT failed: {}", response.status()).into()); - } - Ok(()) - }) - .await; - match result { - Ok(()) => return Ok(()), - Err(error) => { - let network = matches!(&error, AuthedApiError::Timeout) - || matches!(&error, AuthedApiError::Request(error) if is_reqwest_network_error(error)); - failure = Some(error); - self.control.check()?; - if network && attempt < 2 { - network_recovery(self.app, self.control).await?; - } - } - } - self.control.check()?; - } - Err(failure.unwrap()) - } - - async fn complete( - &self, - manifest: &SegmentUploadManifest, - required_audio: bool, - ) -> Result<(), AuthedApiError> { - let session = self.session.ok_or("Upload ownership is missing")?; - session.wait_ready().await?; - let expected = resume::collect_segment_events(&session.directory, required_audio)?; - validate_uploaded_segment_inventory(manifest, &expected)?; - let verification = - UploadVerification::segments(&serde_json::to_vec(manifest)?, required_audio); - self.control - .step(|| await_upload_verification(self.app, self.video_id, &verification, session)) - .await?; - Ok(()) - } - } - - fn event_path(event: &SegmentCompletedEvent) -> String { - let media = match event.media_type { - SegmentMediaType::Video => "video", - SegmentMediaType::Audio => "audio", - }; - if event.is_init { - format!("segments/{media}/init.mp4") - } else { - format!("segments/{media}/segment_{:03}.m4s", event.index) - } - } - - async fn segment( - transport: &impl Transport, - control: &Control, - event: SegmentCompletedEvent, - ) -> Result { - control.check()?; - let path = event_path(&event); - let bytes = control - .step(|| SegmentUploader::read_segment_data(&event.path, &path, event.file_size)) - .await?; - if bytes.is_empty() || (event.file_size > 0 && (bytes.len() as u64) < event.file_size) { - return Err(format!("Required Instant segment is incomplete: {path}").into()); - } - control.step(|| transport.put(&path, bytes)).await?; - Ok(event) - } - - async fn segments( - transport: &impl Transport, - control: &Control, - events: &mut tokio::sync::mpsc::Receiver, - required_audio: bool, - ) -> Result<(), AuthedApiError> { - let active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - let mut uploads = FuturesUnordered::new(); - let mut state = SegmentUploadState::new(); - let mut closed = false; - let mut failure = None; - let mut manifest_at = Instant::now(); - while !closed || !uploads.is_empty() { - tokio::select! { - _ = control.cancelled(), if failure.is_none() => { failure = Some("Instant upload cancelled".to_string().into()); closed = true; events.close(); } - event = events.recv(), if !closed && uploads.len() < 6 => { - match event { Some(event) => uploads.push(segment(transport, control, event)), None => closed = true } - } - Some(result) = uploads.next(), if !uploads.is_empty() => { - match result { - Ok(event) => { - match (event.is_init, event.media_type) { - (true, SegmentMediaType::Video) => state.video_init_uploaded = true, - (true, SegmentMediaType::Audio) => state.audio_init_uploaded = true, - (false, SegmentMediaType::Video) => { let _ = state.uploaded_video_segments.insert(event.index, event.duration); } - (false, SegmentMediaType::Audio) => { let _ = state.uploaded_audio_segments.insert(event.index, event.duration); } - } - if failure.is_none() && manifest_at.elapsed() >= Duration::from_secs(1) { - let manifest = serde_json::to_vec(&state.to_manifest()).map_err(|error| error.to_string()); - let result = match manifest { Ok(bytes) => control.step(|| transport.put("segments/manifest.json", bytes.into())).await, Err(error) => Err(error.into()) }; - if let Err(error) = result { failure = Some(error); closed = true; events.close(); } - manifest_at = Instant::now(); - } - } - Err(error) => { failure = Some(error); closed = true; events.close(); } - } - } - } - if failure.is_some() { - control.deny(); - closed = true; - events.close(); - } - } - if let Some(error) = failure { - return Err(error); - } - control.permission().await?; - let manifest = state.to_complete_manifest(); - if !manifest.has_video_content() - || (required_audio - && (!state.audio_init_uploaded || state.uploaded_audio_segments.is_empty())) - { - return Err("Required Instant media track is missing".to_string().into()); - } - let bytes = serde_json::to_vec(&manifest).map_err(|error| error.to_string())?; - control - .step(|| transport.put("segments/manifest.json", bytes.into())) - .await?; - drop(active_upload); - control - .step(|| transport.complete(&manifest, required_audio)) - .await - } - - pub(crate) fn spawn( - app: AppHandle, - session: Arc, - video: VideoUploadInfo, - events: Option>, - control: Control, - required_audio: bool, - ) -> SegmentUploader { - let recording_dir = session.directory.clone(); - session.bind_control(control.clone()); - let worker_session = session.clone(); - SegmentUploader { - session, - handle: spawn_actor(async move { - let _owner = WorkerGuard(control.clone()); - let result = if let Err(error) = worker_session.acquire() { - control.0.cleanup.send_replace(Some(true)); - Err(error) - } else { - worker_session - .run(crate::web_api::inherit_upload_context( - worker_session.context(), - async { - if let Some(events) = events { - match Bridge::new(events) { - Ok(mut bridge) => { - let transport = LiveTransport { - app: &app, - video_id: &video.id, - control: &control, - session: Some(&worker_session), - }; - let result = AssertUnwindSafe(segments( - &transport, - &control, - &mut bridge.events, - required_audio, - )) - .catch_unwind() - .await; - let joined = bridge.finish().await; - worker_session.wait_for_reads().await; - let _ = control - .0 - .cleanup - .send_replace(Some(joined && result.is_ok())); - match result { - Ok(result) if joined => result, - _ => Err("Instant upload cleanup is unconfirmed" - .to_string() - .into()), - } - } - Err(error) => { - let _ = control.0.cleanup.send_replace(Some(true)); - Err(error) - } - } - } else { - let result = AssertUnwindSafe(multipart( - &app, - &recording_dir, - &video, - &control, - &worker_session, - required_audio, - )) - .catch_unwind() - .await; - worker_session.wait_for_reads().await; - let _ = control.0.cleanup.send_replace(Some(result.is_ok())); - result.unwrap_or_else(|_| { - Err("Instant multipart cleanup is unconfirmed" - .to_string() - .into()) - }) - } - }, - )) - .await - }; - if result.is_err() { - control.deny(); - } - result - }), - } - } - - async fn network_recovery(app: &AppHandle, control: &Control) -> Result<(), AuthedApiError> { - let started = Instant::now(); - let mut delay = CONNECTIVITY_PROBE_INITIAL_DELAY; - while started.elapsed() <= NETWORK_RECOVERY_TIMEOUT { - control - .step(|| async { - tokio::time::sleep(delay).await; - Ok(()) - }) - .await?; - if control - .step(|| async { Ok(probe_connectivity(app).await) }) - .await? - { - return Ok(()); - } - delay = (delay * 2).min(CONNECTIVITY_PROBE_MAX_DELAY); - } - Err("Instant upload network recovery timed out" - .to_string() - .into()) - } - - struct MultipartTarget<'a> { - app: &'a AppHandle, - video_id: &'a str, - upload_id: &'a str, - control: &'a Control, - use_md5: bool, - } - - async fn part( - target: &MultipartTarget<'_>, - chunk: Chunk, - ) -> Result { - let size = chunk.chunk.len(); - let md5_sum = target - .use_md5 - .then(|| base64::encode(md5::compute(&chunk.chunk).0)); - for attempt in 0..2 { - let url = target - .control - .step(|| { - api::upload_multipart_presign_part( - target.app, - target.video_id, - target.upload_id, - chunk.part_number, - md5_sum.as_deref(), - ) - }) - .await?; - let mut request = target - .app - .state::() - .put(&url) - .header("Content-Length", size) - .timeout(Duration::from_secs(5 * 60)) - .body(chunk.chunk.clone()); - if let Some(md5_sum) = &md5_sum { - request = request.header("Content-MD5", md5_sum); - } - let request = with_drive_content_range( - request, - &url, - chunk.offset, - size as u64, - chunk.total_size, - ); - let sent = target - .control - .step(|| async { Ok(request.send().await) }) - .await? - .map_err(reqwest::Error::without_url); - let response = match sent { - Ok(response) => response, - Err(error) if attempt == 0 && is_reqwest_network_error(&error) => { - network_recovery(target.app, target.control).await?; - continue; - } - Err(error) => return Err(error.to_string().into()), - }; - if !is_upload_response_accepted( - &url, - response.status(), - chunk.offset, - size as u64, - chunk.total_size, - ) { - return Err(format!("Instant multipart part failed: {}", response.status()).into()); - } - let etag = response - .headers() - .get("ETag") - .and_then(|value| value.to_str().ok()) - .map(|value| value.trim_matches('"').to_string()) - .or_else(|| { - is_google_drive_resumable_url(&url) - .then(|| format!("drive-{}", chunk.part_number)) - }) - .ok_or_else(|| { - format!( - "Instant multipart part {} is missing ETag", - chunk.part_number - ) - })?; - target.control.check()?; - let object_identity = completed_drive_upload_identity( - response, - is_google_drive_resumable_url(&url), - chunk.total_size, - ) - .await; - return Ok(UploadedPart { - object_identity, - part_number: chunk.part_number, - etag, - size, - total_size: chunk.total_size, - }); - } - Err("Instant multipart retries exhausted".to_string().into()) - } - - async fn multipart( - app: &AppHandle, - directory: &Path, - video: &VideoUploadInfo, - control: &Control, - session: &lifecycle::Session, - required_audio: bool, - ) -> Result<(), AuthedApiError> { - let active_upload = ActiveUploadGuard::new(&ACTIVE_UPLOADS); - let upload = control - .step(|| api::upload_multipart_initiate(app, &video.id, false)) - .await?; - let concurrency = if is_google_drive_upload(upload.provider.as_deref(), &upload.upload_id) { - 1 - } else { - 5 - }; - let target = MultipartTarget { - app, - video_id: &video.id, - upload_id: &upload.upload_id, - control, - use_md5: app.is_server_url_custom().await, - }; - let path = directory.join("content/output.mp4"); - let stream = strict_chunks(path.clone(), control.clone()); - let parts = transfer_parts(&path, stream, &target, control, concurrency).await?; - let metadata = tokio::task::spawn_blocking(move || build_video_meta(&path)) - .await - .map_err(|error| error.to_string())??; - session.wait_ready().await?; - let file_size = std::fs::metadata(directory.join("content/output.mp4")) - .map_err(|error| error.to_string())? - .len(); - let duration = metadata.duration_in_secs; - let completed_identity = control - .step(|| { - api::upload_multipart_complete( - app, - &video.id, - &upload.upload_id, - &parts, - Some(metadata), - false, - ) - }) - .await?; - let object_identity = - if is_google_drive_upload(upload.provider.as_deref(), &upload.upload_id) { - parts - .iter() - .rev() - .find_map(|part| part.object_identity.clone()) - } else { - completed_identity - }; - let verification = UploadVerification::mp4( - file_size, - duration, - required_audio, - object_identity.ok_or( - "Server did not return the uploaded object identity; local recording retained", - )?, - )?; - drop(active_upload); - control - .step(|| await_upload_verification(app, &video.id, &verification, session)) - .await?; - Ok(()) - } - - fn strict_chunks(path: PathBuf, control: Control) -> impl Stream> { - try_stream! { - let started = Instant::now(); - let file = loop { - control.check().map_err(io::Error::other)?; - let opened = open_upload_file(path.clone()).await; - control.check().map_err(io::Error::other)?; - match opened { - Ok(file) => break file, - Err(error) if started.elapsed() >= Duration::from_secs(20) => Err(error)?, - Err(_) => tokio::time::sleep(Duration::from_millis(100)).await, - } - }; - let mut offset = 0u64; - let mut number = 1; - let mut first_size = None; - loop { - control.check().map_err(io::Error::other)?; - let done = *control.0.decision.borrow() == 1; - let size = upload_file_size(file.clone()).await?; - control.check().map_err(io::Error::other)?; - let available = size.saturating_sub(offset); - if available >= MIN_CHUNK_SIZE || (done && available > 0) { - let count = available.min(MAX_CHUNK_SIZE) as usize; - let chunk = upload_file_chunk(file.clone(), offset, count).await?; - control.check().map_err(io::Error::other)?; - if offset == 0 { first_size = Some(count); } - yield Chunk { total_size: size, part_number: number, offset, chunk }; - offset += count as u64; number += 1; - } else if done && available == 0 { - if let Some(count) = first_size { - let chunk = upload_file_chunk(file.clone(), 0, count).await?; - control.check().map_err(io::Error::other)?; - yield Chunk { total_size: size, part_number: 1, offset: 0, chunk }; - } - break; - } else { - tokio::select! { _ = tokio::time::sleep(Duration::from_millis(100)) => {}, _ = control.cancelled() => {} } - } - } - } - } - - trait PartTransport: Send + Sync { - fn send( - &self, - chunk: Chunk, - ) -> impl Future> + Send; - } - impl PartTransport for MultipartTarget<'_> { - async fn send(&self, chunk: Chunk) -> Result { - part(self, chunk).await - } - } - - async fn transfer_parts( - path: &Path, - stream: impl Stream>, - transport: &impl PartTransport, - control: &Control, - concurrency: usize, - ) -> Result, AuthedApiError> { - let mut chunks = Box::pin(stream); - let mut pending = futures::stream::FuturesOrdered::new(); - let mut parts = Vec::new(); - let mut failed = Vec::new(); - let mut failure = None; - let mut closed = false; - while !closed || !pending.is_empty() { - tokio::select! { - _ = control.cancelled(), if failure.is_none() => { failure = Some("Instant multipart cancelled".to_string().into()); } - chunk = chunks.next(), if !closed && pending.len() < concurrency && failure.is_none() => { - match chunk { - Some(Ok(chunk)) => { - let info = FailedChunkInfo { part_number: chunk.part_number, offset: chunk.offset, chunk_size: chunk.chunk.len(), total_size: chunk.total_size, error: String::new() }; - let target = transport; - pending.push_back(async move { (info, control.step(|| target.send(chunk)).await) }); - } - Some(Err(error)) => { failure = Some(error.to_string().into()); closed = true; } - None => closed = true, - } - } - Some((info, result)) = pending.next(), if !pending.is_empty() => { - match result { Ok(part) => parts.push(part), Err(error) => { failed.push(info); if control.check().is_err() { failure = Some(error); } } } - } - } - if failure.is_some() { - control.deny(); - if !closed { - let _ = chunks.next().await; - } - closed = true; - } - } - drop(chunks); - if let Some(error) = failure { - return Err(error); - } - control.permission().await?; - for info in failed { - control.check()?; - let bytes = read_exact_upload_chunk(path, info.offset, info.chunk_size) - .await - .map_err(|error| error.to_string())?; - control.check()?; - parts.push( - control - .step(|| { - transport.send(Chunk { - part_number: info.part_number, - offset: info.offset, - total_size: info.total_size, - chunk: bytes, - }) - }) - .await?, - ); - } - let mut deduplicated = HashMap::new(); - for part in parts { - let _ = deduplicated.insert(part.part_number, part); - } - let mut parts: Vec<_> = deduplicated.into_values().collect(); - parts.sort_by_key(|part| part.part_number); - if parts.is_empty() - || parts - .iter() - .enumerate() - .any(|(index, part)| part.part_number as usize != index + 1 || part.size == 0) - { - return Err("Required Instant multipart data is incomplete" - .to_string() - .into()); - } - Ok(parts) - } - - pub async fn upload_thumbnail( - app: &AppHandle, - video_id: &str, - bytes: Vec, - control: &Control, - ) -> Result<(), AuthedApiError> { - let transport = LiveTransport { - app, - video_id, - control, - session: None, - }; - control - .step(|| transport.put("screenshot/screen-capture.jpg", bytes.into())) - .await - } - - #[cfg(test)] - mod tests { - use super::*; - - #[derive(Default)] - struct FakeTransport { - calls: Mutex>, - fail_audio: AtomicBool, - delay_complete: AtomicBool, - entered: tokio::sync::Notify, - released: tokio::sync::Notify, - put_without_guard: AtomicBool, - complete_with_guard: AtomicBool, - } - impl Transport for FakeTransport { - async fn put(&self, path: &str, _: Bytes) -> Result<(), AuthedApiError> { - self.calls.lock().unwrap().push(path.into()); - if !upload_session_active() { - self.put_without_guard.store(true, Ordering::Release); - } - if self.fail_audio.load(Ordering::Acquire) && path.starts_with("segments/audio/") { - return Err("required audio upload failed".into()); - } - Ok(()) - } - async fn complete( - &self, - _: &SegmentUploadManifest, - _: bool, - ) -> Result<(), AuthedApiError> { - self.calls.lock().unwrap().push("Complete".into()); - if upload_session_active() { - self.complete_with_guard.store(true, Ordering::Release); - } - if self.delay_complete.load(Ordering::Acquire) { - self.entered.notify_one(); - self.released.notified().await; - } - Ok(()) - } - } - fn serial() -> std::sync::MutexGuard<'static, ()> { - static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(()); - SERIAL.lock().unwrap_or_else(PoisonError::into_inner) - } - fn directory(tag: &str) -> PathBuf { - static NEXT: AtomicUsize = AtomicUsize::new(0); - let path = std::env::temp_dir().join(format!( - "cap-strict-upload-{tag}-{}-{}", - std::process::id(), - NEXT.fetch_add(1, Ordering::AcqRel) - )); - std::fs::create_dir_all(&path).unwrap(); - path - } - fn event(path: PathBuf, audio: bool, init: bool) -> SegmentCompletedEvent { - SegmentCompletedEvent { - path, - index: u32::from(!init), - duration: if init { 0.0 } else { 1.0 }, - file_size: 4, - is_init: init, - media_type: if audio { - SegmentMediaType::Audio - } else { - SegmentMediaType::Video - }, - } - } - fn events(path: &Path, audio: bool) -> tokio::sync::mpsc::Receiver { - let file = path.join("synthetic-bytes"); - std::fs::write(&file, b"test").unwrap(); - let (sender, receiver) = tokio::sync::mpsc::channel(12); - for init in [true, false] { - sender.try_send(event(file.clone(), false, init)).unwrap(); - if audio { - sender.try_send(event(file.clone(), true, init)).unwrap(); - } - } - receiver - } - #[tokio::test] - async fn strict_segment_eof_waits_for_owned_permission() { - let _serial = serial(); - let dir = directory("eof"); - let transport = FakeTransport::default(); - let (control, permission) = Control::new(); - let mut source = events(&dir, false); - let future = segments(&transport, &control, &mut source, false); - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - assert!( - !transport - .calls - .lock() - .unwrap() - .iter() - .any(|value| value == "Complete") - ); - permission.grant().unwrap(); - future.await.unwrap(); - assert!( - transport - .calls - .lock() - .unwrap() - .iter() - .any(|value| value == "Complete") - ); - std::fs::remove_dir_all(&dir).unwrap(); - } - #[tokio::test] - async fn strict_required_audio_failure_or_absence_never_completes() { - let _serial = serial(); - for present in [false, true] { - let dir = directory("audio"); - let transport = FakeTransport::default(); - transport.fail_audio.store(present, Ordering::Release); - let (control, permission) = Control::new(); - permission.grant().unwrap(); - let mut source = events(&dir, present); - assert!( - segments(&transport, &control, &mut source, true) - .await - .is_err() - ); - assert!( - !transport - .calls - .lock() - .unwrap() - .iter() - .any(|value| value == "Complete") - ); - std::fs::remove_dir_all(&dir).unwrap(); - } - } - #[tokio::test] - async fn strict_complete_inflight_revocation_rejects_late_success() { - let _serial = serial(); - let dir = directory("late-complete"); - let transport = FakeTransport::default(); - transport.delay_complete.store(true, Ordering::Release); - let (control, permission) = Control::new(); - permission.grant().unwrap(); - let mut source = events(&dir, false); - let future = segments(&transport, &control, &mut source, false); - tokio::pin!(future); - tokio::select! { _ = transport.entered.notified() => {}, result = &mut future => panic!("Unexpected completion: {result:?}") } - control.deny(); - transport.released.notify_one(); - assert!(future.await.is_err()); - assert_eq!( - transport - .calls - .lock() - .unwrap() - .iter() - .filter(|value| *value == "Complete") - .count(), - 1 - ); - std::fs::remove_dir_all(&dir).unwrap(); - } - #[tokio::test] - async fn strict_exit_guard_covers_transfer_but_not_verification() { - let _serial = serial(); - let dir = directory("exit-guard"); - let transport = FakeTransport::default(); - transport.delay_complete.store(true, Ordering::Release); - let (control, permission) = Control::new(); - permission.grant().unwrap(); - let mut source = events(&dir, false); - assert!(!upload_session_active()); - let future = segments(&transport, &control, &mut source, false); - tokio::pin!(future); - tokio::select! { _ = transport.entered.notified() => {}, result = &mut future => panic!("Unexpected completion: {result:?}") } - assert!(!transport.put_without_guard.load(Ordering::Acquire)); - assert!(!transport.complete_with_guard.load(Ordering::Acquire)); - assert!(!upload_session_active()); - transport.released.notify_one(); - future.await.unwrap(); - assert!(!upload_session_active()); - std::fs::remove_dir_all(&dir).unwrap(); - } - #[tokio::test] - async fn strict_permission_drop_and_old_generation_cannot_complete() { - let _serial = serial(); - let dir = directory("owner-drop"); - let transport = FakeTransport::default(); - let (old, old_permission) = Control::new(); - old.deny(); - assert!(old_permission.grant().is_err()); - let (control, permission) = Control::new(); - let mut source = events(&dir, false); - let future = segments(&transport, &control, &mut source, false); - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - drop(permission); - assert!(future.await.is_err()); - assert!( - !transport - .calls - .lock() - .unwrap() - .iter() - .any(|value| value == "Complete") - ); - std::fs::remove_dir_all(&dir).unwrap(); - } - #[tokio::test] - async fn owned_bridge_closes_full_queue_before_actual_thread_join() { - let (sender, receiver) = std::sync::mpsc::channel(); - for _ in 0..30 { - sender - .send(event(PathBuf::from("unused"), false, false)) - .unwrap(); - } - let bridge = Bridge::new(receiver).unwrap(); - tokio::time::sleep(Duration::from_millis(10)).await; - assert!( - tokio::time::timeout(Duration::from_secs(1), bridge.finish()) - .await - .unwrap() - ); - assert!( - sender - .send(event(PathBuf::from("unused"), false, false)) - .is_err() - ); - } - #[derive(Default)] - struct FakeParts { - entered: tokio::sync::Notify, - released: tokio::sync::Notify, - delayed: AtomicBool, - calls: AtomicUsize, - } - impl PartTransport for FakeParts { - async fn send(&self, chunk: Chunk) -> Result { - self.calls.fetch_add(1, Ordering::AcqRel); - if self.delayed.load(Ordering::Acquire) { - self.entered.notify_one(); - self.released.notified().await; - } - Ok(UploadedPart { - object_identity: None, - part_number: chunk.part_number, - etag: "synthetic".into(), - size: chunk.chunk.len(), - total_size: chunk.total_size, - }) - } - } - fn chunks() -> impl Stream> { - stream::iter([Ok(Chunk { - total_size: 4, - part_number: 1, - offset: 0, - chunk: Bytes::from_static(b"test"), - })]) - } - #[tokio::test] - async fn strict_multipart_eof_without_permission_does_not_finalize() { - let transport = FakeParts::default(); - let (control, permission) = Control::new(); - let future = transfer_parts(Path::new("unused"), chunks(), &transport, &control, 5); - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - permission.grant().unwrap(); - assert_eq!(future.await.unwrap().len(), 1); - } - #[tokio::test] - async fn strict_multipart_cancel_drains_inflight_part_before_error() { - let transport = FakeParts::default(); - transport.delayed.store(true, Ordering::Release); - let (control, permission) = Control::new(); - permission.grant().unwrap(); - let future = transfer_parts(Path::new("unused"), chunks(), &transport, &control, 5); - tokio::pin!(future); - tokio::select! { _ = transport.entered.notified() => {}, result = &mut future => panic!("Unexpected result: {result:?}") } - control.deny(); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - transport.released.notify_one(); - assert!(future.await.is_err()); - assert_eq!(transport.calls.load(Ordering::Acquire), 1); - } - #[tokio::test] - async fn strict_request_boundary_stops_following_request_after_late_response() { - let (control, permission) = Control::new(); - let called = AtomicBool::new(false); - let (reply, response) = tokio::sync::oneshot::channel(); - let future = async { - control - .step(|| async { - response.await.unwrap(); - Ok(()) - }) - .await?; - control - .step(|| async { - called.store(true, Ordering::Release); - Ok(()) - }) - .await - }; - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(1), &mut future) - .await - .is_err() - ); - drop(permission); - reply.send(()).unwrap(); - assert!(future.await.is_err()); - assert!(!called.load(Ordering::Acquire)); - } - #[tokio::test] - async fn lost_upload_worker_marks_cleanup_unconfirmed_and_wakes_waiter() { - let (control, _permission) = Control::new(); - let worker = control.clone(); - let task = tokio::spawn(async move { - let _guard = WorkerGuard(worker); - panic!("lost uploader"); - }); - assert!(task.await.is_err()); - assert!( - !tokio::time::timeout(Duration::from_secs(1), control.joined()) - .await - .unwrap() - ); - assert!(control.check().is_err()); - } - #[tokio::test] - async fn strict_reader_waits_for_permission_then_rewrites_first_part() { - let dir = directory("reader"); - let path = dir.join("bytes"); - std::fs::write(&path, b"test").unwrap(); - let (control, permission) = Control::new(); - let mut stream = Box::pin(strict_chunks(path, control)); - assert!( - tokio::time::timeout(Duration::from_millis(10), stream.next()) - .await - .is_err() - ); - permission.grant().unwrap(); - let first = stream.next().await.unwrap().unwrap(); - assert_eq!(first.part_number, 1); - assert_eq!(first.chunk.as_ref(), b"test"); - let rewritten = stream.next().await.unwrap().unwrap(); - assert_eq!(rewritten.part_number, 1); - assert_eq!(rewritten.chunk.as_ref(), b"test"); - assert!(stream.next().await.is_none()); - drop(stream); - std::fs::remove_dir_all(dir).unwrap(); - } - #[tokio::test] - async fn strict_reader_cancellation_is_error_not_successful_eof() { - let dir = directory("cancel-reader"); - let path = dir.join("bytes"); - std::fs::write(&path, b"test").unwrap(); - let (control, permission) = Control::new(); - let mut stream = Box::pin(strict_chunks(path, control)); - assert!( - tokio::time::timeout(Duration::from_millis(10), stream.next()) - .await - .is_err() - ); - drop(permission); - assert!( - tokio::time::timeout(Duration::from_secs(1), stream.next()) - .await - .unwrap() - .unwrap() - .is_err() - ); - drop(stream); - std::fs::remove_dir_all(dir).unwrap(); - } - #[tokio::test] - async fn strict_multipart_completion_step_retains_optional_success_errors_and_late_revocation() - { - let (control, permission) = Control::new(); - permission.grant().unwrap(); - let result: Result, AuthedApiError> = control - .step(|| async { Ok(Some("synthetic-location".to_string())) }) - .await; - assert_eq!(result.unwrap().as_deref(), Some("synthetic-location")); - assert!( - control - .step(|| async { - Err::, _>("required completion failed".into()) - }) - .await - .is_err() - ); - let (release, released) = tokio::sync::oneshot::channel(); - let future = control.step(|| async { - released.await.unwrap(); - Ok(Some("late-location".to_string())) - }); - tokio::pin!(future); - assert!( - tokio::time::timeout(Duration::from_millis(10), &mut future) - .await - .is_err() - ); - control.deny(); - release.send(()).unwrap(); - assert!(future.await.is_err()); - } - } -} diff --git a/apps/desktop/src-tauri/src/upload/lifecycle.rs b/apps/desktop/src-tauri/src/upload/lifecycle.rs deleted file mode 100644 index b6f5927dc64..00000000000 --- a/apps/desktop/src-tauri/src/upload/lifecycle.rs +++ /dev/null @@ -1,1725 +0,0 @@ -use super::*; -use crate::{auth::AuthStore, web_api::UploadRequestContext}; -use futures::FutureExt; -use std::{io::Write, sync::atomic::AtomicBool}; - -const FILE_NAME: &str = "tauri-upload.json"; - -#[derive(Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct Intent { - version: u32, - video_id: String, - server_url: String, - owner_id: Option, - required_audio: Option, - local_ready: bool, - cancelled: bool, - preserve_local: bool, - failures: u32, - #[serde(default)] - needs_authentication: bool, - retry_at: Option, - error: Option, - verification: Option, - receipt: Option, -} - -fn read(directory: &Path) -> Result, AuthedApiError> { - let path = directory.join(FILE_NAME); - let metadata = match path.symlink_metadata() { - Ok(metadata) => metadata, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), - Err(error) => return Err(error.to_string().into()), - }; - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - if metadata.file_attributes() & 0x400 != 0 { - return Err("Upload intent is a reparse point".into()); - } - } - if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.len() > 65536 { - return Err("Invalid recording upload intent".into()); - } - let intent: Intent = - serde_json::from_slice(&std::fs::read(path).map_err(|error| error.to_string())?)?; - if intent.version != 1 || intent.video_id.is_empty() || intent.server_url.is_empty() { - return Err("Unsupported recording upload intent".into()); - } - Ok(Some(intent)) -} - -fn write(directory: &Path, intent: &Intent) -> Result<(), AuthedApiError> { - read(directory)?; - let bytes = serde_json::to_vec_pretty(intent)?; - let temporary = directory.join(format!(".tauri-upload-{}.tmp", uuid::Uuid::new_v4())); - let result = (|| { - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&temporary)?; - file.write_all(&bytes)?; - file.sync_all()?; - drop(file); - std::fs::rename(&temporary, directory.join(FILE_NAME))?; - #[cfg(unix)] - std::fs::File::open(directory)?.sync_all()?; - Ok::<_, io::Error>(()) - })(); - if result.is_err() { - let _ = std::fs::remove_file(temporary); - } - result.map_err(|error| format!("Could not save upload intent: {error}").into()) -} - -pub(crate) async fn prepare( - app: &AppHandle, - directory: &Path, - video_id: &str, - required_audio: bool, -) -> Result, AuthedApiError> { - let server_url = app.make_app_url("").await.trim_end_matches('/').to_string(); - let owner_id = AuthStore::get(app) - .map_err(AuthedApiError::AuthStore)? - .and_then(|auth| auth.user_id); - let context = UploadRequestContext::new( - server_url.clone(), - owner_id - .clone() - .ok_or(AuthedApiError::InvalidAuthentication)?, - )?; - context.check(app).await?; - let lock = acquire_upload_lock(directory)?; - if read(directory)?.is_some() { - return Err("Recording already has an upload identity".into()); - } - write( - directory, - &Intent { - version: 1, - video_id: video_id.into(), - server_url, - owner_id, - required_audio: Some(required_audio), - local_ready: false, - cancelled: false, - preserve_local: false, - failures: 0, - needs_authentication: false, - retry_at: None, - error: None, - verification: None, - receipt: None, - }, - )?; - let session = Session::new(lock.project_path().to_path_buf(), video_id.to_string()); - session.adopt(lock)?; - Ok(session) -} - -#[derive(Default)] -struct ReadTracker { - active: AtomicUsize, - changed: tokio::sync::Notify, -} - -pub(crate) struct ReadGuard(Arc); - -impl Drop for ReadGuard { - fn drop(&mut self) { - self.0.active.fetch_sub(1, Ordering::AcqRel); - self.0.changed.notify_one(); - } -} - -#[derive(Clone)] -pub(crate) struct UploadScope { - cancelled: tokio::sync::watch::Receiver, - reads: Arc, -} - -tokio::task_local! { - static UPLOAD_SCOPE: UploadScope; -} - -impl UploadScope { - pub(crate) fn current() -> Option { - UPLOAD_SCOPE.try_with(Clone::clone).ok() - } - - pub(crate) async fn bind(self, future: F) -> F::Output { - UPLOAD_SCOPE.scope(self, future).await - } - - pub(crate) fn read_guard(&self) -> ReadGuard { - self.reads.active.fetch_add(1, Ordering::AcqRel); - ReadGuard(self.reads.clone()) - } - - async fn cancelled(&self) { - let mut cancelled = self.cancelled.clone(); - while !*cancelled.borrow_and_update() { - if cancelled.changed().await.is_err() { - break; - } - } - } - - fn check(&self) -> Result<(), AuthedApiError> { - if *self.cancelled.borrow() { - Err("Recording upload cancelled; local files retained".into()) - } else { - Ok(()) - } - } -} - -pub(crate) async fn cancellable( - future: F, -) -> Result { - if let Some(scope) = UploadScope::current() { - scope.check()?; - let result = tokio::select! { - result = future => result, - _ = scope.cancelled() => return Err("Recording upload cancelled; local files retained".into()), - }; - scope.check()?; - Ok(result) - } else { - Ok(future.await) - } -} - -pub(crate) async fn file_io( - operation: impl FnOnce() -> io::Result + Send + 'static, -) -> io::Result { - let scope = UploadScope::current(); - if let Some(scope) = &scope { - scope.check().map_err(io::Error::other)?; - } - let guard = scope.map(|scope| scope.read_guard()); - let task = tokio::task::spawn_blocking(move || { - let _guard = guard; - operation() - }); - cancellable(task) - .await - .map_err(io::Error::other)? - .map_err(io::Error::other)? -} - -pub(crate) struct Session { - pub(crate) directory: PathBuf, - video_id: String, - ownership: Mutex>, - request_context: Mutex>, - cancelled: tokio::sync::watch::Sender, - ready: tokio::sync::Notify, - terminal: tokio::sync::watch::Sender>>, - verified: Mutex>, - ledger: Mutex<()>, - reads: Arc, - #[cfg(target_os = "linux")] - strict_control: Mutex>, -} - -impl Session { - pub(crate) fn new(directory: PathBuf, video_id: String) -> Arc { - Arc::new(Self { - directory, - video_id, - ownership: Mutex::new(None), - request_context: Mutex::new(None), - cancelled: tokio::sync::watch::channel(false).0, - ready: tokio::sync::Notify::new(), - terminal: tokio::sync::watch::channel(None).0, - verified: Mutex::new(None), - ledger: Mutex::new(()), - reads: Arc::new(ReadTracker::default()), - #[cfg(target_os = "linux")] - strict_control: Mutex::new(None), - }) - } - - pub(crate) async fn run(&self, future: F) -> F::Output { - let scope = UploadScope { - cancelled: self.cancelled.subscribe(), - reads: self.reads.clone(), - }; - let result = std::panic::AssertUnwindSafe(scope.bind(future)) - .catch_unwind() - .await; - self.wait_for_reads().await; - match result { - Ok(result) => result, - Err(panic) => std::panic::resume_unwind(panic), - } - } - - pub(crate) async fn wait_for_reads(&self) { - while self.reads.active.load(Ordering::Acquire) != 0 { - tokio::select! { _ = self.reads.changed.notified() => {}, _ = tokio::time::sleep(Duration::from_millis(10)) => {} } - } - } - - pub(crate) fn adopt(&self, lock: UploadLock) -> Result<(), AuthedApiError> { - if self - .directory - .canonicalize() - .map_err(|error| error.to_string())? - != lock.project_path() - { - return Err("Upload ownership path changed".into()); - } - *self - .ownership - .lock() - .unwrap_or_else(PoisonError::into_inner) = Some(lock); - self.load_context()?; - self.check() - } - - fn load_context(&self) -> Result<(), AuthedApiError> { - if let Some(intent) = read(&self.directory)? { - if intent.video_id != self.video_id || intent.cancelled { - return Err("Recording upload identity changed or was cancelled".into()); - } - let context = UploadRequestContext::new( - intent.server_url, - intent - .owner_id - .ok_or(AuthedApiError::InvalidAuthentication)?, - )?; - *self - .request_context - .lock() - .unwrap_or_else(PoisonError::into_inner) = Some(context); - } - Ok(()) - } - - pub(crate) fn cached_verification(&self) -> Result, AuthedApiError> { - let verification = read(&self.directory)?.and_then(|intent| intent.verification); - if verification - .as_ref() - .is_some_and(UploadVerification::requires_reupload) - { - self.clear_verification()?; - return Ok(None); - } - if let Some(verification) = &verification - && let Err(error) = verify_local_artifact(&self.directory, verification) - { - self.clear_verification()?; - return Err(error); - } - Ok(verification) - } - - pub(crate) fn acquire(&self) -> Result<(), AuthedApiError> { - let mut ownership = self - .ownership - .lock() - .unwrap_or_else(PoisonError::into_inner); - if ownership.is_some() { - return self.check(); - } - let lock = acquire_upload_lock(&self.directory)?; - let meta = - RecordingMeta::load_for_project(&self.directory).map_err(|error| error.to_string())?; - if let Some(id) = upload_video_id(&meta.upload) - && id != self.video_id - { - return Err("Recording upload identity changed".into()); - } - self.load_context()?; - *ownership = Some(lock); - self.check() - } - - pub(crate) fn context(&self) -> Option { - self.request_context - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone() - } - - pub(crate) fn check(&self) -> Result<(), AuthedApiError> { - if *self.cancelled.borrow() { - Err("Recording upload cancelled; local files retained".into()) - } else { - Ok(()) - } - } - - #[cfg(target_os = "linux")] - pub(crate) fn bind_control(&self, control: super::strict_instant::Control) { - *self - .strict_control - .lock() - .unwrap_or_else(PoisonError::into_inner) = Some(control); - } - - pub(crate) fn cancel(&self) { - #[cfg(target_os = "linux")] - if let Some(control) = self - .strict_control - .lock() - .unwrap_or_else(PoisonError::into_inner) - .as_ref() - { - control.deny(); - } - self.cancelled.send_replace(true); - self.ready.notify_one(); - } - - pub(crate) async fn cancelled(&self) { - let mut cancelled = self.cancelled.subscribe(); - while !*cancelled.borrow_and_update() { - if cancelled.changed().await.is_err() { - break; - } - } - } - - pub(crate) fn persist_upload(&self, upload: UploadMeta) -> Result<(), AuthedApiError> { - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - self.check()?; - let mut meta = - RecordingMeta::load_for_project(&self.directory).map_err(|error| error.to_string())?; - meta.upload = Some(upload); - meta.save_for_project().map_err(|error| error.to_string())?; - Ok(()) - } - - pub(crate) fn persist_local_complete( - &self, - recording: cap_project::InstantRecordingMeta, - sharing: cap_project::SharingMeta, - ) -> Result<(), AuthedApiError> { - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - self.check()?; - let mut meta = - RecordingMeta::load_for_project(&self.directory).map_err(|error| error.to_string())?; - meta.inner = cap_project::RecordingMetaInner::Instant(recording); - meta.sharing = Some(sharing); - meta.save_for_project().map_err(|error| error.to_string())?; - Ok(()) - } - - pub(crate) fn mark_ready(&self, preserve_local: bool) -> Result<(), AuthedApiError> { - self.acquire()?; - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(mut intent) = read(&self.directory)? { - intent.local_ready = true; - intent.preserve_local |= preserve_local; - intent.retry_at = None; - write(&self.directory, &intent)?; - } - self.ready.notify_one(); - Ok(()) - } - - pub(crate) async fn wait_ready(&self) -> Result<(), AuthedApiError> { - loop { - self.check()?; - match read(&self.directory)? { - Some(intent) if intent.cancelled => { - return Err("Recording upload was cancelled".into()); - } - Some(intent) if !intent.local_ready => {} - _ => return Ok(()), - } - tokio::select! { _ = self.ready.notified() => {}, _ = self.cancelled() => {}, _ = tokio::time::sleep(Duration::from_millis(100)) => {} } - } - } - - pub(crate) fn record_failure(&self, error: &AuthedApiError) { - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - let result = (|| { - let Some(mut intent) = read(&self.directory)? else { - return Ok::<_, AuthedApiError>(()); - }; - intent.needs_authentication = matches!(error, AuthedApiError::InvalidAuthentication); - if !matches!( - error, - AuthedApiError::InvalidAuthentication | AuthedApiError::VerificationPending - ) { - intent.failures = intent.failures.saturating_add(1); - } - intent.retry_at = (intent.failures < 5).then(|| { - chrono::Utc::now() - .timestamp() - .saturating_add(30 * (1_i64 << intent.failures.min(5))) - }); - intent.error = Some(error.to_string()); - write(&self.directory, &intent) - })(); - if let Err(error) = result { - warn!(%error, "Could not save upload retry state"); - } - } - - pub(crate) fn mark_cancelled(&self) -> Result<(), AuthedApiError> { - self.cancel(); - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(mut intent) = read(&self.directory)? { - intent.cancelled = true; - intent.retry_at = None; - write(&self.directory, &intent)?; - } - Ok(()) - } - - pub(crate) fn set_verification( - &self, - verification: &UploadVerification, - ) -> Result<(), AuthedApiError> { - self.check()?; - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(mut intent) = read(&self.directory)? { - if intent.required_audio != Some(verification.required_audio) { - return Err("Upload audio intent changed".into()); - } - intent.verification = Some(verification.clone()); - intent.receipt = None; - write(&self.directory, &intent)?; - } - Ok(()) - } - - pub(crate) fn clear_verification(&self) -> Result<(), AuthedApiError> { - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - *self.verified.lock().unwrap_or_else(PoisonError::into_inner) = None; - if let Some(mut intent) = read(&self.directory)? { - intent.verification = None; - intent.receipt = None; - write(&self.directory, &intent)?; - } - Ok(()) - } - - pub(crate) fn record_receipt( - &self, - verification: &UploadVerification, - receipt: &VerifiedUploadReceipt, - ) -> Result<(), AuthedApiError> { - self.check()?; - verification - .verified_receipt( - &self.video_id, - &serde_json::json!({"success":true,"status":"verified","verification":receipt}), - )? - .ok_or("Recording verification is pending")?; - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - *self.verified.lock().unwrap_or_else(PoisonError::into_inner) = - Some((verification.clone(), receipt.clone())); - if let Some(mut intent) = read(&self.directory)? { - intent.verification = Some(verification.clone()); - intent.receipt = Some(receipt.clone()); - intent.failures = 0; - intent.needs_authentication = false; - intent.retry_at = None; - intent.error = None; - write(&self.directory, &intent)?; - } - Ok(()) - } - - pub(crate) async fn complete_locally(&self, app: &AppHandle) -> Result<(), AuthedApiError> { - self.check()?; - if let Some(context) = self.context() { - context.check(app).await?; - } - let _ledger = self.ledger.lock().unwrap_or_else(PoisonError::into_inner); - let intent = read(&self.directory)?; - let evidence = intent - .as_ref() - .and_then(|intent| intent.verification.clone().zip(intent.receipt.clone())) - .or_else(|| { - self.verified - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone() - }) - .ok_or("Recording verification evidence is missing")?; - let (verification, receipt) = (&evidence.0, &evidence.1); - verification - .verified_receipt( - &self.video_id, - &serde_json::json!({"success":true,"status":"verified","verification":receipt}), - )? - .ok_or("Recording verification is pending")?; - verify_local_artifact(&self.directory, verification)?; - let mut meta = - RecordingMeta::load_for_project(&self.directory).map_err(|error| error.to_string())?; - if !matches!( - meta.inner, - cap_project::RecordingMetaInner::Instant( - cap_project::InstantRecordingMeta::Complete { .. } - ) - ) || upload_video_id(&meta.upload).is_some_and(|id| id != self.video_id) - || meta - .sharing - .as_ref() - .is_some_and(|sharing| sharing.id != self.video_id) - || intent.as_ref().is_some_and(|intent| intent.cancelled) - { - return Err("Recording is not locally ready for upload completion".into()); - } - self.check()?; - meta.upload = Some(UploadMeta::Complete); - meta.save_for_project().map_err(|error| error.to_string())?; - self.check()?; - if intent.as_ref().is_some_and(|intent| !intent.preserve_local) - && crate::general_settings::GeneralSettingsStore::get(app) - .ok() - .flatten() - .unwrap_or_default() - .delete_instant_recordings_after_upload - { - verify_local_artifact(&self.directory, verification)?; - self.check()?; - if let Err(error) = std::fs::remove_dir_all(&self.directory) { - warn!(%error, "Verified recording retained because cleanup failed"); - } - } - Ok(()) - } -} - -fn upload_video_id(upload: &Option) -> Option<&str> { - match upload { - Some( - UploadMeta::SegmentUpload { video_id, .. } - | UploadMeta::MultipartUpload { video_id, .. } - | UploadMeta::SinglePartUpload { video_id, .. }, - ) => Some(video_id), - _ => None, - } -} - -fn verify_local_artifact( - directory: &Path, - verification: &UploadVerification, -) -> Result<(), AuthedApiError> { - use cap_recording::upload_verification::UploadArtifact; - match &verification.artifact { - UploadArtifact::Segments { manifest_sha256 } => { - let events = resume::collect_segment_events(directory, verification.required_audio)?; - let manifest = manifest_from_events(&events); - let compact = UploadVerification::segments( - &serde_json::to_vec(&manifest)?, - verification.required_audio, - ); - let pretty = UploadVerification::segments( - &serde_json::to_vec_pretty(&manifest)?, - verification.required_audio, - ); - if ![compact, pretty].iter().any(|request| matches!(&request.artifact, UploadArtifact::Segments { manifest_sha256: current } if current == manifest_sha256)) { - return Err("Local recording manifest changed after upload".into()); - } - } - UploadArtifact::Mp4 { - file_size, - duration, - .. - } => { - let path = directory.join("content/output.mp4"); - verify_file_path( - &path, - &directory - .canonicalize() - .map_err(|error| error.to_string())? - .join("content/output.mp4"), - )?; - let info = std::fs::symlink_metadata(&path).map_err(|error| error.to_string())?; - if info.file_type().is_symlink() - || !info.is_file() - || info.len() != *file_size - || build_video_meta(&path)?.duration_in_secs != *duration - { - return Err("Local recording changed after upload".into()); - } - } - } - Ok(()) -} - -fn manifest_from_events( - events: &[cap_enc_ffmpeg::segmented_stream::SegmentCompletedEvent], -) -> SegmentUploadManifest { - use cap_enc_ffmpeg::segmented_stream::SegmentMediaType; - let mut state = SegmentUploadState::new(); - for event in events { - match (event.is_init, event.media_type) { - (true, SegmentMediaType::Video) => state.video_init_uploaded = true, - (true, SegmentMediaType::Audio) => state.audio_init_uploaded = true, - (false, SegmentMediaType::Video) => { - state - .uploaded_video_segments - .insert(event.index, event.duration); - } - (false, SegmentMediaType::Audio) => { - state - .uploaded_audio_segments - .insert(event.index, event.duration); - } - } - } - state.to_complete_manifest() -} - -pub(crate) async fn resume_audio( - app: &AppHandle, - directory: &Path, - fallback: bool, -) -> Result, AuthedApiError> { - Ok(match read(directory)? { - Some(intent) - if intent.cancelled - || intent.failures >= 5 - || intent - .retry_at - .is_some_and(|at| at > chrono::Utc::now().timestamp()) => - { - None - } - Some(intent) => { - if intent.needs_authentication { - let context = UploadRequestContext::new( - intent.server_url, - intent - .owner_id - .ok_or(AuthedApiError::InvalidAuthentication)?, - )?; - if context.check(app).await.is_err() { - return Ok(None); - } - } - intent.required_audio - } - None => Some(fallback), - }) -} - -pub(crate) async fn resume_existing( - app: AppHandle, - meta: RecordingMeta, - lock: UploadLock, - required_audio: bool, -) -> Result, AuthedApiError> { - let directory = meta.project_path.clone(); - let video_id = upload_video_id(&meta.upload) - .map(str::to_string) - .ok_or("No resumable upload identity")?; - let session = Session::new(lock.project_path().to_path_buf(), video_id.clone()); - session.adopt(lock)?; - session.mark_ready(false)?; - let worker = session.clone(); - supervise(app.clone(), session.clone(), async move { - match meta.upload.clone() { - Some(UploadMeta::SegmentUpload { - video_id, - pre_created_video, - recording_dir, - }) => { - verify_bundle_path(&directory, &recording_dir)?; - if video_id != pre_created_video.id || video_id != pre_created_video.config.id { - return Err("Upload identity changed".into()); - } - let events = resume::collect_segment_events(&directory, required_audio)?; - let (sender, receiver) = std::sync::mpsc::channel(); - for event in events { - sender.send(event).map_err(|error| error.to_string())?; - } - drop(sender); - SegmentUploader::run( - app.clone(), - receiver, - None, - directory.clone(), - pre_created_video, - required_audio, - worker.clone(), - ) - .await?; - } - Some(UploadMeta::MultipartUpload { - video_id, - file_path, - pre_created_video, - recording_dir, - }) => { - verify_bundle_path(&directory, &recording_dir)?; - if video_id != pre_created_video.id || video_id != pre_created_video.config.id { - return Err("Upload identity changed".into()); - } - verify_file_path(&file_path, &directory.join("content/output.mp4"))?; - InstantMultipartUpload::run( - app.clone(), - file_path, - pre_created_video, - directory.clone(), - None, - required_audio, - worker.clone(), - ) - .await?; - } - Some(UploadMeta::SinglePartUpload { - video_id, - file_path, - screenshot_path, - recording_dir, - }) => { - verify_bundle_path(&directory, &recording_dir)?; - verify_file_path(&file_path, &meta.output_path())?; - verify_file_path(&screenshot_path, &directory.join("screenshots/display.jpg"))?; - let metadata = build_video_meta(&file_path)?; - let file_size = std::fs::metadata(&file_path).map_err(|error| error.to_string())?.len(); - let duration = metadata.duration_in_secs; - let uploaded = upload_video( - &app, - video_id.clone(), - file_path, - screenshot_path, - meta.sharing.is_some() - && matches!(meta.inner, cap_project::RecordingMetaInner::Studio(_)), - None, - ) - .await?; - if uploaded.id != video_id { - return Err("Server upload identity changed".into()); - } - if matches!(meta.inner, cap_project::RecordingMetaInner::Instant(_)) { - let verification = UploadVerification::mp4(file_size, duration, required_audio, uploaded.object_identity.ok_or("Server did not return the uploaded object identity; local recording retained")?)?; - await_upload_verification(&app, &video_id, &verification, &worker).await?; - } else { - let mut current = RecordingMeta::load_for_project(&directory) - .map_err(|error| error.to_string())?; - if upload_video_id(¤t.upload) != Some(video_id.as_str()) { - return Err("Upload identity changed during transfer".into()); - } - worker.check()?; - current.upload = Some(UploadMeta::Complete); - current.sharing = Some(cap_project::SharingMeta { - id: uploaded.id, - link: uploaded.link, - content_hash: None, - }); - current - .save_for_project() - .map_err(|error| error.to_string())?; - return Ok(()); - } - } - _ => return Err("No resumable upload remains".into()), - } - let screenshot = directory.join("screenshots/display.jpg"); - if screenshot.is_file() { - let bytes = compress_image(screenshot).await?; - worker.check()?; - singlepart_uploader( - app.clone(), - PresignedS3PutRequest { - video_id, - subpath: "screenshot/screen-capture.jpg".into(), - method: PresignedS3PutRequestMethod::Put, - meta: None, - }, - bytes.len() as u64, - futures::stream::once(async move { Ok::<_, io::Error>(Bytes::from(bytes)) }), - ) - .await?; - } - worker.complete_locally(&app).await - }) - .await?; - Ok(session) -} - -fn verify_bundle_path(expected: &Path, stored: &Path) -> Result<(), AuthedApiError> { - if expected.canonicalize().map_err(|error| error.to_string())? - != stored.canonicalize().map_err(|error| error.to_string())? - { - return Err("Stored recording path changed".into()); - } - Ok(()) -} - -fn verify_file_path(stored: &Path, expected: &Path) -> Result<(), AuthedApiError> { - let parent = expected - .parent() - .ok_or("Upload input has no local parent")?; - let parent_info = parent - .symlink_metadata() - .map_err(|error| error.to_string())?; - if parent_info.file_type().is_symlink() || !parent_info.is_dir() { - return Err("Upload input directory is not a local directory".into()); - } - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - if parent_info.file_attributes() & 0x400 != 0 { - return Err("Upload input directory is a reparse point".into()); - } - } - let metadata = stored - .symlink_metadata() - .map_err(|error| error.to_string())?; - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - if metadata.file_attributes() & 0x400 != 0 { - return Err("Upload input is a reparse point".into()); - } - } - if !metadata.is_file() || metadata.file_type().is_symlink() || metadata.len() == 0 { - return Err("Upload input is not a valid local file".into()); - } - verify_bundle_path(expected, stored) -} - -fn pending_reupload(meta: &RecordingMeta, intent: &Intent) -> Result { - use cap_recording::upload_verification::UploadArtifact; - let sharing = meta - .sharing - .as_ref() - .ok_or("Recording share identity is missing")?; - if sharing.id != intent.video_id || intent.cancelled || !intent.local_ready { - return Err("Recording retry identity changed or was cancelled".into()); - } - let video = VideoUploadInfo { - id: intent.video_id.clone(), - link: sharing.link.clone(), - config: S3UploadMeta { - id: intent.video_id.clone(), - }, - }; - let segmented = match intent - .verification - .as_ref() - .map(|request| &request.artifact) - { - Some(UploadArtifact::Segments { .. }) => true, - Some(UploadArtifact::Mp4 { .. }) => false, - None => match meta.project_path.join("content/display").symlink_metadata() { - Ok(_) => true, - Err(error) if error.kind() == io::ErrorKind::NotFound => false, - Err(error) => return Err(error.to_string().into()), - }, - }; - Ok(if segmented { - UploadMeta::SegmentUpload { - video_id: intent.video_id.clone(), - pre_created_video: video, - recording_dir: meta.project_path.clone(), - } - } else { - UploadMeta::MultipartUpload { - video_id: intent.video_id.clone(), - pre_created_video: video, - recording_dir: meta.project_path.clone(), - file_path: meta.project_path.join("content/output.mp4"), - } - }) -} - -fn prepare_retry_state( - directory: &Path, - meta: &mut RecordingMeta, - intent: &mut Intent, - save_meta: impl FnOnce(&RecordingMeta) -> Result<(), String>, -) -> Result<(), AuthedApiError> { - let reupload = matches!(meta.upload, Some(UploadMeta::Complete)); - if reupload { - meta.upload = Some(pending_reupload(meta, intent)?); - intent.verification = None; - intent.receipt = None; - } - if upload_video_id(&meta.upload) != Some(intent.video_id.as_str()) { - return Err("Recording retry identity changed".into()); - } - intent.failures = 0; - intent.needs_authentication = false; - intent.retry_at = None; - intent.error = None; - write(directory, intent)?; - if reupload { - save_meta(meta)?; - } - Ok(()) -} - -pub(crate) fn reconcile_reupload(meta: &mut RecordingMeta) -> Result<(), AuthedApiError> { - if !matches!(meta.upload, Some(UploadMeta::Complete)) - || !matches!( - meta.inner, - cap_project::RecordingMetaInner::Instant( - cap_project::InstantRecordingMeta::Complete { .. } - ) - ) - { - return Ok(()); - } - let Some(intent) = read(&meta.project_path)? else { - return Ok(()); - }; - if intent.cancelled - || !intent.local_ready - || intent.verification.is_some() - || intent.receipt.is_some() - { - return Ok(()); - } - meta.upload = Some(pending_reupload(meta, &intent)?); - meta.save_for_project().map_err(|error| error.to_string())?; - Ok(()) -} - -pub(crate) async fn retry_existing( - app: AppHandle, - directory: &Path, -) -> Result, AuthedApiError> { - if read(directory)?.is_none() { - return Ok(None); - } - let lock = acquire_upload_lock(directory)?; - let mut intent = read(directory)?.ok_or("Upload intent disappeared")?; - if intent.cancelled || !intent.local_ready { - return Err("This recording is not available for upload retry".into()); - } - let context = UploadRequestContext::new( - intent.server_url.clone(), - intent - .owner_id - .clone() - .ok_or(AuthedApiError::InvalidAuthentication)?, - )?; - context.check(&app).await?; - let required_audio = intent - .required_audio - .ok_or("Recording audio intent is unknown")?; - let mut meta = RecordingMeta::load_for_project(directory).map_err(|error| error.to_string())?; - if !matches!( - meta.inner, - cap_project::RecordingMetaInner::Instant( - cap_project::InstantRecordingMeta::Complete { .. } - ) - ) { - return Err("Recording must finish locally before retry".into()); - } - let link = meta - .sharing - .as_ref() - .map(|sharing| sharing.link.clone()) - .ok_or("Recording share identity is missing")?; - prepare_retry_state(directory, &mut meta, &mut intent, |meta| { - meta.save_for_project().map_err(|error| error.to_string()) - })?; - let session = resume_existing(app, meta, lock, required_audio).await?; - let mut terminal = session.terminal.subscribe(); - timeout(Duration::from_secs(30 * 60), async { - loop { - if let Some(result) = terminal.borrow_and_update().clone() { - result.map_err(AuthedApiError::from)?; - return Ok(Some(link)); - } - terminal - .changed() - .await - .map_err(|_| "Upload completion channel closed")?; - } - }) - .await - .map_err(|_| AuthedApiError::VerificationPending)? -} - -struct Job { - session: Arc, - handle: JoinHandle<()>, -} - -static JOBS: std::sync::OnceLock>> = std::sync::OnceLock::new(); -static PUMP: std::sync::OnceLock>>> = std::sync::OnceLock::new(); -static WAKE: tokio::sync::Notify = tokio::sync::Notify::const_new(); -static STOPPING: AtomicBool = AtomicBool::new(false); - -fn jobs() -> &'static Mutex> { - JOBS.get_or_init(Mutex::default) -} - -pub(crate) fn has_capacity() -> bool { - !STOPPING.load(Ordering::Acquire) - && jobs().lock().unwrap_or_else(PoisonError::into_inner).len() < 2 -} - -pub(crate) async fn supervise( - app: AppHandle, - session: Arc, - work: F, -) -> Result<(), AuthedApiError> -where - F: std::future::Future> + Send + 'static, -{ - let mut work = Some(work); - let registered = { - let mut jobs = jobs().lock().unwrap_or_else(PoisonError::into_inner); - if STOPPING.load(Ordering::Acquire) || jobs.contains_key(&session.directory) { - false - } else { - let directory = session.directory.clone(); - let worker = session.clone(); - let context = session.context(); - let work = work.take().unwrap(); - let handle = spawn_actor(async move { - let result = std::panic::AssertUnwindSafe( - worker.run(crate::web_api::inherit_upload_context(context, work)), - ) - .catch_unwind() - .await - .unwrap_or_else(|_| Err("Upload worker failed; local recording retained".into())); - worker - .terminal - .send_replace(Some(result.as_ref().copied().map_err(ToString::to_string))); - if let Err(error) = result { - if !matches!(error, AuthedApiError::VerificationPending) { - crate::notifications::NotificationType::UploadFailed.send(&app); - } - worker.record_failure(&error); - warn!(%error, "Recording upload retained for retry"); - } - emit_upload_complete(&app, &worker.video_id); - }); - jobs.insert( - directory, - Job { - session: session.clone(), - handle, - }, - ); - true - } - }; - if !registered { - session.cancel(); - let result = session - .run(crate::web_api::inherit_upload_context( - session.context(), - work.unwrap(), - )) - .await; - session - .terminal - .send_replace(Some(result.as_ref().copied().map_err(ToString::to_string))); - if let Err(error) = result { - session.record_failure(&error); - } - } - Ok(()) -} - -async fn join_job(job: Job) { - if let Err(error) = job.handle.await { - job.session.record_failure(&AuthedApiError::Other(format!( - "Upload supervisor failed: {error}" - ))); - warn!(%error, "Upload supervisor failed"); - } -} - -pub(crate) async fn reap() { - let completed = { - let mut jobs = jobs().lock().unwrap_or_else(PoisonError::into_inner); - let paths: Vec<_> = jobs - .iter() - .filter(|(_, job)| job.handle.is_finished()) - .map(|(path, _)| path.clone()) - .collect(); - paths - .into_iter() - .filter_map(|path| jobs.remove(&path)) - .collect::>() - }; - for job in completed { - join_job(job).await; - } -} - -pub(crate) async fn cancel(directory: &Path) { - let job = jobs() - .lock() - .unwrap_or_else(PoisonError::into_inner) - .remove(directory); - if let Some(job) = job { - job.session.mark_cancelled().ok(); - join_job(job).await; - } -} - -pub(crate) fn mark_cancelled(directory: &Path) -> Result<(), AuthedApiError> { - if let Some(mut intent) = read(directory)? { - intent.cancelled = true; - intent.retry_at = None; - write(directory, &intent)?; - } - Ok(()) -} - -pub(crate) fn init(app: AppHandle) { - let mut pump = PUMP - .get_or_init(Mutex::default) - .lock() - .unwrap_or_else(PoisonError::into_inner); - if pump.is_some() { - return; - } - *pump = Some(spawn_actor(async move { - loop { - if STOPPING.load(Ordering::Acquire) { - return; - } - tokio::select! { - ready = crate::startup::wait_for_window(&app) => { - if !ready { - return; - } - break; - } - _ = WAKE.notified() => {}, - } - } - let mut first = true; - while !STOPPING.load(Ordering::Acquire) { - if let Err(error) = crate::resume_uploads(app.clone(), first).await { - warn!(%error, "Upload reconciliation failed; recordings retained"); - } - first = false; - tokio::select! { - _ = WAKE.notified() => {}, - _ = tokio::time::sleep(Duration::from_secs(30)) => {}, - } - } - })); -} - -pub(crate) async fn shutdown() { - STOPPING.store(true, Ordering::Release); - WAKE.notify_one(); - let pump = PUMP - .get_or_init(Mutex::default) - .lock() - .unwrap_or_else(PoisonError::into_inner) - .take(); - if let Some(pump) = pump { - let _ = pump.await; - } - let owned = std::mem::take(&mut *jobs().lock().unwrap_or_else(PoisonError::into_inner)); - for job in owned.values() { - job.session.cancel(); - } - for (_, job) in owned { - join_job(job).await; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn project() -> (tempfile::TempDir, Arc) { - let directory = tempfile::tempdir().unwrap(); - std::fs::write( - directory.path().join("retained-media"), - b"original recording bytes", - ) - .unwrap(); - write( - directory.path(), - &Intent { - version: 1, - video_id: "owned-video".into(), - server_url: "https://example.invalid".into(), - owner_id: Some("owner".into()), - required_audio: Some(true), - local_ready: false, - cancelled: false, - preserve_local: false, - failures: 0, - needs_authentication: false, - retry_at: None, - error: None, - verification: None, - receipt: None, - }, - ) - .unwrap(); - RecordingMeta { - platform: None, - project_path: directory.path().to_path_buf(), - pretty_name: "retained".into(), - sharing: None, - inner: cap_project::RecordingMetaInner::Instant( - cap_project::InstantRecordingMeta::InProgress { recording: true }, - ), - upload: None, - } - .save_for_project() - .unwrap(); - let lock = UploadLock::acquire(directory.path()).unwrap(); - let session = Session::new(lock.project_path().to_path_buf(), "owned-video".into()); - session.adopt(lock).unwrap(); - (directory, session) - } - - fn receipt(request: &UploadVerification) -> VerifiedUploadReceipt { - VerifiedUploadReceipt { - version: 1, - video_id: "owned-video".into(), - artifact: request.artifact.clone(), - file_size: 4096, - duration: 10.0, - has_audio: true, - full_decode: true, - required_audio_verified: request.required_audio, - } - } - - #[tokio::test] - async fn local_ready_releases_completion_gate_without_waiting_for_network() { - let (directory, session) = project(); - let mut gate = Box::pin(session.wait_ready()); - assert!(futures::poll!(gate.as_mut()).is_pending()); - session.mark_ready(false).unwrap(); - gate.await.unwrap(); - let intent = read(directory.path()).unwrap().unwrap(); - assert!(intent.local_ready); - assert!(intent.verification.is_none()); - assert!(intent.receipt.is_none()); - assert!(directory.path().join("retained-media").exists()); - } - - #[tokio::test] - async fn cancellation_is_persisted_and_never_opens_completion_gate() { - let (directory, session) = project(); - let mut gate = Box::pin(session.wait_ready()); - assert!(futures::poll!(gate.as_mut()).is_pending()); - session.mark_cancelled().unwrap(); - assert!(gate.await.is_err()); - assert!(read(directory.path()).unwrap().unwrap().cancelled); - assert_eq!( - std::fs::read(directory.path().join("retained-media")).unwrap(), - b"original recording bytes" - ); - } - - #[test] - fn restart_preserves_identity_and_actual_audio_intent_with_exclusive_ownership() { - let (directory, session) = project(); - session.mark_ready(false).unwrap(); - session.record_failure(&AuthedApiError::Timeout); - assert!(matches!( - UploadLock::acquire(directory.path()), - Err(cap_recording::upload_resume::UploadLockError::Busy) - )); - drop(session); - let lock = UploadLock::acquire(directory.path()).unwrap(); - let resumed = Session::new(lock.project_path().to_path_buf(), "owned-video".into()); - resumed.adopt(lock).unwrap(); - let intent = read(directory.path()).unwrap().unwrap(); - assert_eq!(intent.video_id, "owned-video"); - assert_eq!(intent.owner_id.as_deref(), Some("owner")); - assert_eq!(intent.required_audio, Some(true)); - assert!(intent.local_ready); - assert_eq!(intent.failures, 1); - assert!(directory.path().join("retained-media").exists()); - } - - #[test] - fn processing_and_authentication_do_not_exhaust_failed_transfer_budget() { - let (directory, session) = project(); - for _ in 0..8 { - session.record_failure(&AuthedApiError::VerificationPending); - } - assert_eq!(read(directory.path()).unwrap().unwrap().failures, 0); - session.record_failure(&AuthedApiError::InvalidAuthentication); - let intent = read(directory.path()).unwrap().unwrap(); - assert!(intent.needs_authentication); - assert_eq!(intent.failures, 0); - for _ in 0..5 { - session.record_failure(&AuthedApiError::Timeout); - } - let intent = read(directory.path()).unwrap().unwrap(); - assert_eq!(intent.failures, 5); - assert!(intent.retry_at.is_none()); - assert_eq!(intent.video_id, "owned-video"); - assert!(directory.path().join("retained-media").exists()); - } - - #[test] - fn server_retransfer_clears_only_cached_evidence() { - let (directory, session) = project(); - let request = UploadVerification::segments(b"manifest", true); - session.set_verification(&request).unwrap(); - session - .record_receipt(&request, &receipt(&request)) - .unwrap(); - session.clear_verification().unwrap(); - session.record_failure(&AuthedApiError::ReuploadRequired); - let intent = read(directory.path()).unwrap().unwrap(); - assert!(intent.verification.is_none()); - assert!(intent.receipt.is_none()); - assert_eq!(intent.video_id, "owned-video"); - assert!(directory.path().join("retained-media").exists()); - } - - #[test] - fn wrong_generation_or_probe_only_receipt_never_persists_completion() { - let (directory, session) = project(); - let request = UploadVerification::segments(b"manifest", true); - session.set_verification(&request).unwrap(); - for change in 0..5 { - let mut invalid = receipt(&request); - match change { - 0 => invalid.full_decode = false, - 1 => invalid.has_audio = false, - 2 => invalid.video_id = "other-video".into(), - 3 => invalid.required_audio_verified = false, - _ => { - invalid.artifact = - UploadVerification::segments(b"other-manifest", true).artifact - } - } - assert!(session.record_receipt(&request, &invalid).is_err()); - assert!(read(directory.path()).unwrap().unwrap().receipt.is_none()); - } - assert!(directory.path().join("retained-media").exists()); - } - - #[test] - fn unknown_legacy_upload_does_not_acquire_an_invented_sidecar() { - let directory = tempfile::tempdir().unwrap(); - let lock = UploadLock::acquire(directory.path()).unwrap(); - let session = Session::new(lock.project_path().to_path_buf(), "legacy-video".into()); - session.adopt(lock).unwrap(); - session.record_failure(&AuthedApiError::Timeout); - session.mark_ready(false).unwrap(); - assert!(read(directory.path()).unwrap().is_none()); - } - - #[tokio::test] - async fn cancelled_job_retains_lock_until_its_worker_is_joined() { - let (directory, session) = project(); - let (release, released) = tokio::sync::oneshot::channel(); - let worker = session.clone(); - let handle = tokio::spawn(async move { - worker.cancelled().await; - released.await.unwrap(); - assert!(worker.directory.join("retained-media").exists()); - }); - let job = Job { - session: session.clone(), - handle, - }; - session.cancel(); - drop(session); - let mut joined = Box::pin(join_job(job)); - assert!(futures::poll!(joined.as_mut()).is_pending()); - assert!(matches!( - UploadLock::acquire(directory.path()), - Err(cap_recording::upload_resume::UploadLockError::Busy) - )); - release.send(()).unwrap(); - joined.await; - assert!(UploadLock::acquire(directory.path()).is_ok()); - assert!(directory.path().join("retained-media").exists()); - } - - #[cfg(unix)] - #[test] - fn upload_input_cannot_escape_through_a_parent_symlink() { - let directory = tempfile::tempdir().unwrap(); - let outside = tempfile::tempdir().unwrap(); - std::fs::write(outside.path().join("output.mp4"), b"private bytes").unwrap(); - std::os::unix::fs::symlink(outside.path(), directory.path().join("content")).unwrap(); - let path = directory.path().join("content/output.mp4"); - assert!(verify_file_path(&path, &path).is_err()); - assert_eq!( - std::fs::read(outside.path().join("output.mp4")).unwrap(), - b"private bytes" - ); - } - #[test] - fn legacy_mp4_proof_without_object_generation_retransfers_without_changing_identity() { - let (directory, session) = project(); - let mut intent = read(directory.path()).unwrap().unwrap(); - intent.verification = Some(serde_json::from_value(serde_json::json!({"version":1,"artifact":{"kind":"mp4","fileSize":4096,"duration":10.0},"requiredAudio":true})).unwrap()); - write(directory.path(), &intent).unwrap(); - assert!(session.cached_verification().unwrap().is_none()); - let retained = read(directory.path()).unwrap().unwrap(); - assert!(retained.verification.is_none()); - assert!(retained.receipt.is_none()); - assert_eq!(retained.video_id, "owned-video"); - assert!(directory.path().join("retained-media").exists()); - } - #[test] - fn concurrent_local_save_and_upload_start_preserve_both_updates() { - let (directory, session) = project(); - let barrier = Arc::new(std::sync::Barrier::new(2)); - let worker = session.clone(); - let upload_barrier = barrier.clone(); - let uploader = std::thread::spawn(move || { - upload_barrier.wait(); - worker - .persist_upload(UploadMeta::SinglePartUpload { - video_id: "owned-video".into(), - file_path: worker.directory.join("content/output.mp4"), - screenshot_path: worker.directory.join("screenshots/display.jpg"), - recording_dir: worker.directory.clone(), - }) - .unwrap(); - }); - barrier.wait(); - session - .persist_local_complete( - cap_project::InstantRecordingMeta::Complete { - fps: 30, - sample_rate: Some(48000), - }, - cap_project::SharingMeta { - id: "owned-video".into(), - link: "https://example.invalid/s/owned-video".into(), - content_hash: None, - }, - ) - .unwrap(); - uploader.join().unwrap(); - let meta = RecordingMeta::load_for_project(directory.path()).unwrap(); - assert!(matches!( - meta.inner, - cap_project::RecordingMetaInner::Instant( - cap_project::InstantRecordingMeta::Complete { .. } - ) - )); - assert_eq!(upload_video_id(&meta.upload), Some("owned-video")); - assert_eq!(meta.sharing.unwrap().id, "owned-video"); - assert!(directory.path().join("retained-media").exists()); - } - #[tokio::test] - async fn cancellation_drops_inherited_request_future_without_authorizing_completion() { - struct PendingRequest(Arc); - impl Drop for PendingRequest { - fn drop(&mut self) { - self.0.store(true, Ordering::Release); - } - } - let (_directory, session) = project(); - let dropped = Arc::new(AtomicBool::new(false)); - let completed = Arc::new(AtomicBool::new(false)); - let (entered, started) = tokio::sync::oneshot::channel(); - let worker = session.clone(); - let request_dropped = dropped.clone(); - let completion = completed.clone(); - let task = tokio::spawn(async move { - worker - .run(async move { - let task = - tokio::spawn(crate::web_api::inherit_upload_context(None, async move { - let _request = PendingRequest(request_dropped); - entered.send(()).unwrap(); - cancellable(std::future::pending::<()>()).await?; - completion.store(true, Ordering::Release); - Ok::<_, AuthedApiError>(()) - })); - task.await.unwrap() - }) - .await - }); - started.await.unwrap(); - session.cancel(); - assert!( - timeout(Duration::from_secs(2), task) - .await - .unwrap() - .unwrap() - .is_err() - ); - assert!(dropped.load(Ordering::Acquire)); - assert!(!completed.load(Ordering::Acquire)); - } - - #[tokio::test] - async fn cancelled_filesystem_future_keeps_ownership_until_real_read_finishes() { - let (directory, session) = project(); - let (entered, started) = tokio::sync::oneshot::channel(); - let (release, released) = std::sync::mpsc::channel(); - let closed = Arc::new(AtomicBool::new(false)); - let finished = closed.clone(); - let path = directory.path().join("retained-media"); - let worker = session.clone(); - let mut task = tokio::spawn(async move { - worker - .run(file_io(move || { - use std::io::Read; - let mut file = std::fs::File::open(path)?; - entered.send(()).unwrap(); - released.recv_timeout(Duration::from_secs(2)).unwrap(); - let mut bytes = Vec::new(); - file.read_to_end(&mut bytes)?; - drop(file); - finished.store(true, Ordering::Release); - Ok(bytes) - })) - .await - }); - started.await.unwrap(); - session.cancel(); - assert!(futures::poll!(&mut task).is_pending()); - assert!(!closed.load(Ordering::Acquire)); - assert!(matches!( - UploadLock::acquire(directory.path()), - Err(cap_recording::upload_resume::UploadLockError::Busy) - )); - release.send(()).unwrap(); - assert!( - timeout(Duration::from_secs(2), task) - .await - .unwrap() - .unwrap() - .is_err() - ); - assert!(closed.load(Ordering::Acquire)); - assert_eq!(session.reads.active.load(Ordering::Acquire), 0); - drop(session); - assert!(UploadLock::acquire(directory.path()).is_ok()); - } - - #[tokio::test] - async fn cancellation_closes_progressive_reader_without_waiting_for_recording_done() { - let (directory, session) = project(); - let path = directory.path().join("retained-media"); - let (done, receiver) = flume::bounded(1); - let worker = session.clone(); - let task = tokio::spawn(async move { - worker - .run( - super::super::from_pending_file_to_chunks(path, Some(receiver)) - .try_collect::>(), - ) - .await - }); - timeout(Duration::from_secs(2), async { - while session.reads.active.load(Ordering::Acquire) == 0 { - tokio::task::yield_now().await; - } - }) - .await - .unwrap(); - session.cancel(); - assert!( - timeout(Duration::from_secs(2), task) - .await - .unwrap() - .unwrap() - .is_err() - ); - assert_eq!(session.reads.active.load(Ordering::Acquire), 0); - assert!(done.is_disconnected()); - assert_eq!( - std::fs::read(directory.path().join("retained-media")).unwrap(), - b"original recording bytes" - ); - } - fn completed_retry_fixture( - directory: &Path, - session: &Session, - segmented: bool, - ) -> (RecordingMeta, Intent) { - session - .persist_local_complete( - cap_project::InstantRecordingMeta::Complete { - fps: 30, - sample_rate: Some(48000), - }, - cap_project::SharingMeta { - id: "owned-video".into(), - link: "https://example.invalid/s/owned-video".into(), - content_hash: None, - }, - ) - .unwrap(); - session.mark_ready(false).unwrap(); - let request = if segmented { - std::fs::create_dir_all(directory.join("content/display")).unwrap(); - UploadVerification::segments(b"manifest", true) - } else { - UploadVerification::mp4(4096, 10.0, true, "\"old-object\"".into()).unwrap() - }; - session.set_verification(&request).unwrap(); - session - .record_receipt(&request, &receipt(&request)) - .unwrap(); - let mut meta = RecordingMeta::load_for_project(directory).unwrap(); - meta.upload = Some(UploadMeta::Complete); - meta.save_for_project().unwrap(); - (meta, read(directory).unwrap().unwrap()) - } - - #[test] - fn explicit_reupload_reuses_original_identity_and_clears_completed_proof() { - for segmented in [false, true] { - let (directory, session) = project(); - let (mut meta, mut intent) = - completed_retry_fixture(directory.path(), &session, segmented); - prepare_retry_state(directory.path(), &mut meta, &mut intent, |meta| { - meta.save_for_project().map_err(|error| error.to_string()) - }) - .unwrap(); - let pending = RecordingMeta::load_for_project(directory.path()).unwrap(); - assert_eq!(upload_video_id(&pending.upload), Some("owned-video")); - assert_eq!( - matches!(pending.upload, Some(UploadMeta::SegmentUpload { .. })), - segmented - ); - let retained = read(directory.path()).unwrap().unwrap(); - assert!(retained.verification.is_none()); - assert!(retained.receipt.is_none()); - assert_eq!(retained.owner_id.as_deref(), Some("owner")); - assert!(directory.path().join("retained-media").exists()); - } - } - - #[test] - fn interrupted_reupload_metadata_write_remains_recoverable_without_stale_proof() { - for segmented in [false, true] { - let (directory, session) = project(); - let (mut meta, mut intent) = - completed_retry_fixture(directory.path(), &session, segmented); - assert!( - prepare_retry_state(directory.path(), &mut meta, &mut intent, |_| Err( - "injected metadata replacement failure".into() - )) - .is_err() - ); - let mut original = RecordingMeta::load_for_project(directory.path()).unwrap(); - assert!(matches!(original.upload, Some(UploadMeta::Complete))); - assert!( - read(directory.path()) - .unwrap() - .unwrap() - .verification - .is_none() - ); - reconcile_reupload(&mut original).unwrap(); - assert_eq!(upload_video_id(&original.upload), Some("owned-video")); - assert_eq!( - matches!(original.upload, Some(UploadMeta::SegmentUpload { .. })), - segmented - ); - assert!(directory.path().join("retained-media").exists()); - } - } -} diff --git a/apps/desktop/src-tauri/src/upload/preparation.rs b/apps/desktop/src-tauri/src/upload/preparation.rs deleted file mode 100644 index f8a5025a28f..00000000000 --- a/apps/desktop/src-tauri/src/upload/preparation.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::{SegmentUploadState, lifecycle}; -use crate::{api, web_api::inherit_upload_context}; -use cap_recording::upload_preparation::Preparation; -pub(crate) use cap_recording::upload_preparation::Segment; -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; -use tauri::AppHandle; - -pub(super) struct Task(tokio::task::JoinHandle<()>); - -impl Task { - pub(super) async fn stop(mut self) { - self.0.abort(); - if let Err(error) = (&mut self.0).await - && !error.is_cancelled() - { - tracing::warn!(%error, "Optional recording preparation stopped"); - } - } -} - -impl Drop for Task { - fn drop(&mut self) { - self.0.abort(); - } -} - -pub(super) fn start( - app: AppHandle, - video_id: String, - state: Arc>, - session: Arc, -) -> Task { - let context = session.context(); - Task(tokio::spawn(inherit_upload_context(context, async move { - let mut preparation = Preparation::default(); - let mut next_request = tokio::time::Instant::now(); - let mut interval = tokio::time::interval(Duration::from_secs(30)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = session.cancelled() => return, - _ = interval.tick() => {} - } - if tokio::time::Instant::now() < next_request { - continue; - } - let batch = { - let state = state.lock().unwrap_or_else(|error| error.into_inner()); - preparation.next_batch( - state.uploaded_video_segments.keys().copied(), - state.uploaded_audio_segments.keys().copied(), - ) - }; - if batch.is_empty() { - continue; - } - let result = tokio::select! { - _ = session.cancelled() => return, - result = api::prepare_recording_segments(&app, &video_id, &batch) => result, - }; - match result { - Ok(Some(prepared)) => { - preparation.acknowledge(&batch, &prepared); - } - Ok(None) => return, - Err(error) => { - preparation.request_failed(); - tracing::debug!(%error, "Optional recording preparation unavailable"); - } - } - next_request = tokio::time::Instant::now() + preparation.retry_delay(); - } - }))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn stopping_preparation_cancels_an_in_flight_request() { - let (started, ready) = tokio::sync::oneshot::channel(); - let handle = tokio::spawn(async move { - started.send(()).unwrap(); - std::future::pending::<()>().await; - }); - let abort = handle.abort_handle(); - let task = Task(handle); - ready.await.unwrap(); - task.stop().await; - assert!(abort.is_finished()); - } -} diff --git a/apps/desktop/src-tauri/src/upload/recovery_age.rs b/apps/desktop/src-tauri/src/upload/recovery_age.rs deleted file mode 100644 index 2bc2e4ae3a8..00000000000 --- a/apps/desktop/src-tauri/src/upload/recovery_age.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::{path::Path, time::SystemTime}; - -const MAX_AUTOMATIC_RECOVERY_AGE: std::time::Duration = - std::time::Duration::from_secs(24 * 60 * 60); - -pub(crate) fn eligible(project: &Path, now: SystemTime) -> bool { - project - .metadata() - .and_then(|metadata| metadata.created()) - .ok() - .and_then(|created| now.duration_since(created).ok()) - .is_some_and(|age| age <= MAX_AUTOMATIC_RECOVERY_AGE) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{fs, time::Duration}; - - #[test] - fn recovery_expires_even_when_old_recording_metadata_changes() { - let project = std::env::temp_dir().join(format!("cap-recovery-age-{}", std::process::id())); - fs::create_dir(&project).unwrap(); - let created = project.metadata().unwrap().created().unwrap(); - assert!(eligible(&project, created)); - assert!(eligible(&project, created + MAX_AUTOMATIC_RECOVERY_AGE)); - fs::write(project.join("recording-meta.json"), b"{}").unwrap(); - assert!(!eligible( - &project, - created + MAX_AUTOMATIC_RECOVERY_AGE + Duration::from_secs(1) - )); - assert!(!eligible(&project, SystemTime::UNIX_EPOCH)); - fs::remove_dir_all(&project).unwrap(); - assert!(!eligible(&project, created)); - } -} diff --git a/apps/desktop/src-tauri/src/upload/resume.rs b/apps/desktop/src-tauri/src/upload/resume.rs deleted file mode 100644 index 30dd92cfca9..00000000000 --- a/apps/desktop/src-tauri/src/upload/resume.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) use cap_recording::upload_resume::collect_segment_events; diff --git a/apps/desktop/src-tauri/src/web_api.rs b/apps/desktop/src-tauri/src/web_api.rs deleted file mode 100644 index b15d334648d..00000000000 --- a/apps/desktop/src-tauri/src/web_api.rs +++ /dev/null @@ -1,325 +0,0 @@ -use reqwest::StatusCode; -use tauri::{Emitter, Manager, Runtime}; -use thiserror::Error; -use tracing::{debug, error, warn}; - -use crate::{ - ArcLock, - auth::{AuthSecret, AuthStore}, - http_client, -}; - -#[derive(Error, Debug)] -pub enum AuthedApiError { - #[error("User is not authenticated or credentials have expired!")] - InvalidAuthentication, - #[error("User needs to upgrade their account to use this feature!")] - UpgradeRequired, - #[error("App state is still initializing")] - AppStateUnavailable, - #[error("AuthedApiError/AuthStore: {0}")] - AuthStore(String), - #[error("AuthedApiError/Request: {0}")] - Request(reqwest::Error), - #[error("AuthedApiError/Deserialization: {0}")] - Deserialization(#[from] serde_json::Error), - #[error("The request has timed out")] - Timeout, - #[error( - "Cloud verification rejected the uploaded recording; local data must be uploaded again" - )] - ReuploadRequired, - #[error("Recording is awaiting cloud verification; local files are retained")] - VerificationPending, - #[error("AuthedApiError/Other: {0}")] - Other(String), -} - -impl From for AuthedApiError { - fn from(err: reqwest::Error) -> Self { - match err { - err if err.is_timeout() => AuthedApiError::Timeout, - err => AuthedApiError::Request(err.without_url()), - } - } -} - -impl From<&'static str> for AuthedApiError { - fn from(value: &'static str) -> Self { - AuthedApiError::Other(value.into()) - } -} - -impl From for AuthedApiError { - fn from(value: String) -> Self { - AuthedApiError::Other(value) - } -} - -fn apply_env_headers(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - let mut req = req - .header("X-Cap-Desktop-Version", env!("CARGO_PKG_VERSION")) - .header("X-Cap-Desktop-Features", "googleDriveUpload"); - - if let Ok(s) = std::env::var("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") { - req = req.header("x-vercel-protection-bypass", s); - } - - req -} - -fn default_server_url() -> String { - option_env!("VITE_SERVER_URL") - .unwrap_or("https://cap.so") - .to_string() -} - -async fn current_server_url(manager: &T) -> Result -where - T: Manager + ?Sized, - R: Runtime, -{ - let Some(app_state) = manager.try_state::>() else { - return Err(AuthedApiError::AppStateUnavailable); - }; - - Ok(app_state.read().await.server_url.clone()) -} - -#[derive(Clone)] -pub(crate) struct UploadRequestContext { - server_url: String, - owner_id: String, -} - -tokio::task_local! { - static UPLOAD_REQUEST: UploadRequestContext; -} - -impl UploadRequestContext { - pub(crate) fn new(server_url: String, owner_id: String) -> Result { - if server_url.is_empty() || owner_id.is_empty() { - return Err(AuthedApiError::InvalidAuthentication); - } - Ok(Self { - server_url, - owner_id, - }) - } - - fn check_identity( - &self, - server_url: &str, - owner_id: Option<&str>, - ) -> Result<(), AuthedApiError> { - if server_url != self.server_url || owner_id != Some(self.owner_id.as_str()) { - return Err(AuthedApiError::InvalidAuthentication); - } - Ok(()) - } - - pub(crate) async fn check(&self, app: &tauri::AppHandle) -> Result<(), AuthedApiError> { - let auth = AuthStore::get(app) - .map_err(AuthedApiError::AuthStore)? - .ok_or(AuthedApiError::InvalidAuthentication)?; - self.check_identity(¤t_server_url(app).await?, auth.user_id.as_deref()) - } - - pub(crate) async fn run(self, future: F) -> F::Output { - UPLOAD_REQUEST.scope(self, future).await - } - - pub(crate) fn current() -> Option { - UPLOAD_REQUEST.try_with(Clone::clone).ok() - } -} - -pub(crate) fn inherit_upload_context( - context: Option, - future: F, -) -> impl std::future::Future { - let scope = crate::upload::lifecycle::UploadScope::current(); - async move { - let request = async move { - match context { - Some(context) => context.run(future).await, - None => future.await, - } - }; - match scope { - Some(scope) => scope.bind(request).await, - None => request.await, - } - } -} - -async fn do_authed_request( - client: &reqwest::Client, - auth: &AuthStore, - build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, - url: String, -) -> Result { - let req = build(client, url).header( - "Authorization", - format!( - "Bearer {}", - match &auth.secret { - AuthSecret::ApiKey { api_key } => api_key, - AuthSecret::Session { token, .. } => token, - } - ), - ); - - apply_env_headers(req).send().await -} - -#[allow(async_fn_in_trait)] -pub trait ManagerExt: Manager { - async fn authed_api_request( - &self, - path: impl Into, - build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, - ) -> Result; - - async fn api_request( - &self, - path: impl Into, - build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, - ) -> Result; - - async fn make_app_url(&self, pathname: impl AsRef) -> String; - - async fn is_server_url_custom(&self) -> bool; -} - -impl + Emitter, R: Runtime> ManagerExt for T { - async fn authed_api_request( - &self, - path: impl Into, - build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, - ) -> Result { - let Some(auth) = AuthStore::get(self.app_handle()).map_err(AuthedApiError::AuthStore)? - else { - debug!("Skipping authenticated API request because user is not logged in"); - return Err(AuthedApiError::InvalidAuthentication); - }; - - let path = path.into(); - let server_url = crate::upload::lifecycle::cancellable(current_server_url(self)).await??; - let server_url = if let Some(context) = UploadRequestContext::current() { - context.check_identity(&server_url, auth.user_id.as_deref())?; - context.server_url - } else { - server_url - }; - let url = format!("{server_url}{path}"); - let response = crate::upload::lifecycle::cancellable(do_authed_request( - &self.state::(), - &auth, - build, - url, - )) - .await??; - - if response.status() == StatusCode::UNAUTHORIZED { - error!("Authentication expired. Please log in again."); - return Err(AuthedApiError::InvalidAuthentication); - } - - Ok(response) - } - - async fn api_request( - &self, - path: impl Into, - build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, - ) -> Result { - let url = self.make_app_url(path.into()).await; - - apply_env_headers(build(&self.state::(), url)) - .send() - .await - } - - async fn make_app_url(&self, pathname: impl AsRef) -> String { - let pathname = pathname.as_ref(); - match current_server_url(self).await { - Ok(server_url) => format!("{server_url}{pathname}"), - Err(err) => { - warn!("App state unavailable while building app URL: {err}"); - format!("{}{}", default_server_url(), pathname) - } - } - } - - async fn is_server_url_custom(&self) -> bool { - match current_server_url(self).await { - Ok(server_url) => { - if let Some(env_url) = std::option_env!("VITE_SERVER_URL") { - return server_url != env_url; - } - - false - } - Err(err) => { - warn!("App state unavailable while reading server URL settings: {err}"); - false - } - } - } -} - -#[cfg(test)] -mod upload_context_tests { - use super::*; - - #[test] - fn upload_request_errors_do_not_expose_signed_urls() { - let url = reqwest::Url::parse( - "https://storage.invalid/object?X-Amz-Signature=private-signature&upload_id=private-session", - ) - .unwrap(); - let error = reqwest::Client::new() - .put(url.clone()) - .header("invalid\nheader", "value") - .build() - .unwrap_err() - .with_url(url); - assert!(error.url().is_some()); - let error = AuthedApiError::from(error); - assert!( - matches!(&error, AuthedApiError::Request(inner) if inner.is_builder() && inner.url().is_none()) - ); - for diagnostic in [error.to_string(), format!("{error:?}")] { - assert!(!diagnostic.contains("private-signature")); - assert!(!diagnostic.contains("private-session")); - assert!(!diagnostic.contains("storage.invalid")); - } - } - - #[test] - fn upload_request_identity_cannot_follow_a_changed_account_or_server() { - let context = - UploadRequestContext::new("https://original.invalid".into(), "owner".into()).unwrap(); - assert!( - context - .check_identity("https://original.invalid", Some("owner")) - .is_ok() - ); - assert!( - context - .check_identity("https://changed.invalid", Some("owner")) - .is_err() - ); - assert!( - context - .check_identity("https://original.invalid", Some("another-owner")) - .is_err() - ); - assert!( - context - .check_identity("https://original.invalid", None) - .is_err() - ); - } -} diff --git a/apps/desktop/src-tauri/src/window_exclusion.rs b/apps/desktop/src-tauri/src/window_exclusion.rs deleted file mode 100644 index 8bdf228afc6..00000000000 --- a/apps/desktop/src-tauri/src/window_exclusion.rs +++ /dev/null @@ -1,407 +0,0 @@ -#[cfg(target_os = "macos")] -use scap_targets::{Window, WindowId}; -use serde::{Deserialize, Serialize}; -use specta::Type; -#[cfg(target_os = "macos")] -use tracing::{debug, info, warn}; - -#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct WindowExclusion { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bundle_identifier: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub owner_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub window_title: Option, -} - -impl WindowExclusion { - pub fn matches( - &self, - bundle_identifier: Option<&str>, - owner_name: Option<&str>, - window_title: Option<&str>, - ) -> bool { - if let Some(identifier) = self.bundle_identifier.as_deref() - && bundle_identifier - .map(|candidate| candidate == identifier) - .unwrap_or(false) - { - return true; - } - - if let Some(expected_owner) = self.owner_name.as_deref() { - let owner_matches = owner_name - .map(|candidate| candidate == expected_owner) - .unwrap_or(false); - - if self.window_title.is_some() { - return owner_matches - && self - .window_title - .as_deref() - .map(|expected_title| { - window_title - .map(|candidate| candidate == expected_title) - .unwrap_or(false) - }) - .unwrap_or(false); - } - - if owner_matches { - return true; - } - } - - if let Some(expected_title) = self.window_title.as_deref() { - return window_title - .map(|candidate| candidate == expected_title) - .unwrap_or(false); - } - - false - } -} - -#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))] -pub fn filter_for_instant_mode( - mut exclusions: Vec, - camera_title: &str, -) -> Vec { - exclusions.retain(|e| e.window_title.as_deref() != Some(camera_title)); - exclusions -} - -#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))] -fn matches_window_title(exclusions: &[WindowExclusion], title: &str) -> bool { - exclusions - .iter() - .any(|entry| entry.matches(None, None, Some(title))) -} - -#[cfg(any(target_os = "macos", test))] -fn excludes_own_window( - exclusions: &[WindowExclusion], - window: &crate::windows::CapWindowId, -) -> bool { - matches!(window, crate::windows::CapWindowId::RecordingControls) - || matches_window_title(exclusions, &window.title()) -} - -#[cfg(target_os = "macos")] -fn append_native_window_id(ids: &mut Vec, native_id: &WindowId) -> bool { - if ids.contains(native_id) { - return false; - } - ids.push(native_id.clone()); - true -} - -#[cfg(target_os = "macos")] -pub fn resolve_window_ids(exclusions: &[WindowExclusion]) -> Vec { - if exclusions.is_empty() { - return Vec::new(); - } - - Window::list() - .into_iter() - .filter_map(|window| { - let owner_name = window.owner_name(); - let window_title = window.name(); - let bundle_identifier = window.raw_handle().bundle_identifier(); - let matches = exclusions.iter().any(|entry| { - entry.matches( - bundle_identifier.as_deref(), - owner_name.as_deref(), - window_title.as_deref(), - ) - }); - - if !matches { - return None; - } - - let window_id = window.id(); - info!( - %window_id, - owner_name = ?owner_name, - window_title = ?window_title, - bundle_identifier = ?bundle_identifier, - "Resolved excluded macOS window" - ); - Some(window_id) - }) - .collect() -} - -#[cfg(target_os = "macos")] -pub fn append_matching_webview_window_ids( - ids: &mut Vec, - app: &tauri::AppHandle, - exclusions: &[WindowExclusion], -) { - use crate::windows::CapWindowId; - use std::str::FromStr; - use tauri::Manager; - - for (label, window) in app.webview_windows() { - let Ok(window_id) = CapWindowId::from_str(&label) else { - continue; - }; - let title = window_id.title(); - if !excludes_own_window(exclusions, &window_id) { - continue; - } - let Some(native_id) = webview_window_id(&window) else { - warn!( - label = %label, - title = %title, - "Excluded Tauri webview window has no native window id" - ); - continue; - }; - if !append_native_window_id(ids, &native_id) { - debug!( - window_id = %native_id, - label = %label, - title = %title, - "Excluded Tauri webview window id already resolved" - ); - continue; - } - info!( - window_id = %native_id, - label = %label, - title = %title, - "Resolved excluded Tauri webview window" - ); - } -} - -#[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 ns_window = unsafe { ns_window.as_ref() }?; - let number = unsafe { ns_window.windowNumber() }; - - if number <= 0 { - return None; - } - - number.to_string().parse().ok() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn title_exclusion(title: &str) -> WindowExclusion { - WindowExclusion { - bundle_identifier: None, - owner_name: None, - window_title: Some(title.to_string()), - } - } - - fn bundle_exclusion(bundle_id: &str) -> WindowExclusion { - WindowExclusion { - bundle_identifier: Some(bundle_id.to_string()), - owner_name: None, - window_title: None, - } - } - - fn owner_exclusion(owner: &str) -> WindowExclusion { - WindowExclusion { - bundle_identifier: None, - owner_name: Some(owner.to_string()), - window_title: None, - } - } - - #[test] - fn matches_by_window_title() { - let exclusion = title_exclusion("Cap Camera"); - assert!(exclusion.matches(None, None, Some("Cap Camera"))); - assert!(!exclusion.matches(None, None, Some("Other Window"))); - assert!(!exclusion.matches(None, None, None)); - } - - #[test] - fn matches_by_bundle_identifier() { - let exclusion = bundle_exclusion("com.cap.desktop"); - assert!(exclusion.matches(Some("com.cap.desktop"), None, None)); - assert!(!exclusion.matches(Some("com.other.app"), None, None)); - assert!(!exclusion.matches(None, None, None)); - } - - #[test] - fn matches_by_owner_name() { - let exclusion = owner_exclusion("Cap"); - assert!(exclusion.matches(None, Some("Cap"), None)); - assert!(!exclusion.matches(None, Some("Other"), None)); - assert!(!exclusion.matches(None, None, None)); - } - - #[test] - fn matches_owner_and_title_requires_both() { - let exclusion = WindowExclusion { - bundle_identifier: None, - owner_name: Some("Cap".to_string()), - window_title: Some("Cap Camera".to_string()), - }; - assert!(exclusion.matches(None, Some("Cap"), Some("Cap Camera"))); - assert!(!exclusion.matches(None, Some("Cap"), Some("Wrong Title"))); - assert!(!exclusion.matches(None, Some("Wrong Owner"), Some("Cap Camera"))); - assert!(!exclusion.matches(None, None, Some("Cap Camera"))); - } - - #[test] - fn empty_exclusion_matches_nothing() { - let exclusion = WindowExclusion { - bundle_identifier: None, - owner_name: None, - window_title: None, - }; - assert!(!exclusion.matches(None, None, None)); - assert!(!exclusion.matches(Some("any"), Some("any"), Some("any"))); - } - - #[test] - fn bundle_identifier_takes_priority() { - let exclusion = WindowExclusion { - bundle_identifier: Some("com.cap.desktop".to_string()), - owner_name: None, - window_title: Some("Cap Camera".to_string()), - }; - assert!(exclusion.matches(Some("com.cap.desktop"), None, None)); - assert!(exclusion.matches(Some("com.cap.desktop"), None, Some("Wrong"))); - } - - #[test] - fn instant_mode_removes_camera_exclusion() { - let exclusions = vec![ - title_exclusion("Cap"), - title_exclusion("Cap Camera"), - title_exclusion("Cap Settings"), - title_exclusion("Cap Recording Controls"), - ]; - - let filtered = filter_for_instant_mode(exclusions, "Cap Camera"); - - assert_eq!(filtered.len(), 3); - assert!( - filtered - .iter() - .all(|e| e.window_title.as_deref() != Some("Cap Camera")) - ); - assert!( - filtered - .iter() - .any(|e| e.window_title.as_deref() == Some("Cap")) - ); - assert!( - filtered - .iter() - .any(|e| e.window_title.as_deref() == Some("Cap Settings")) - ); - assert!( - filtered - .iter() - .any(|e| e.window_title.as_deref() == Some("Cap Recording Controls")) - ); - } - - #[test] - fn instant_mode_noop_when_camera_absent() { - let exclusions = vec![title_exclusion("Cap"), title_exclusion("Cap Settings")]; - - let filtered = filter_for_instant_mode(exclusions, "Cap Camera"); - assert_eq!(filtered.len(), 2); - } - - #[test] - fn instant_mode_handles_empty_list() { - let filtered = filter_for_instant_mode(vec![], "Cap Camera"); - assert!(filtered.is_empty()); - } - - #[test] - fn matches_webview_title_using_exclusion_rules() { - let exclusions = vec![title_exclusion("Cap Camera")]; - - assert!(matches_window_title(&exclusions, "Cap Camera")); - assert!(!matches_window_title(&exclusions, "Cap Recording Controls")); - } - - #[test] - fn own_controls_are_excluded_with_empty_or_custom_rules() { - use crate::windows::CapWindowId; - - assert!(excludes_own_window(&[], &CapWindowId::RecordingControls)); - assert!(excludes_own_window( - &[title_exclusion("Unrelated window")], - &CapWindowId::RecordingControls, - )); - assert!(!excludes_own_window(&[], &CapWindowId::Camera)); - } - - #[test] - fn own_window_exclusions_preserve_default_and_instant_camera_rules() { - use crate::windows::CapWindowId; - - let defaults = crate::general_settings::default_excluded_windows(); - assert!(excludes_own_window( - &defaults, - &CapWindowId::RecordingControls - )); - assert!(excludes_own_window(&defaults, &CapWindowId::Camera)); - let instant = filter_for_instant_mode(defaults, &CapWindowId::Camera.title()); - assert!(!excludes_own_window(&instant, &CapWindowId::Camera)); - assert!(excludes_own_window( - &instant, - &CapWindowId::RecordingControls - )); - } - - #[cfg(target_os = "macos")] - #[test] - fn native_exclusion_ids_do_not_require_a_visible_cg_window() { - let native_id: WindowId = "4294967294".parse().unwrap(); - let mut ids = Vec::new(); - assert!(append_native_window_id(&mut ids, &native_id)); - assert_eq!(ids, vec![native_id.clone()]); - assert!(!append_native_window_id(&mut ids, &native_id)); - assert_eq!(ids.len(), 1); - } - - #[test] - fn default_exclusions_contain_camera() { - let defaults = crate::general_settings::default_excluded_windows(); - assert!( - defaults - .iter() - .any(|e| e.window_title.as_deref() == Some("Cap Camera")), - "Default exclusions must include 'Cap Camera' — instant mode filtering depends on this" - ); - } - - #[test] - fn default_exclusions_only_match_camera_by_title() { - let defaults = crate::general_settings::default_excluded_windows(); - let camera = defaults - .iter() - .find(|e| e.window_title.as_deref() == Some("Cap Camera")) - .expect("Cap Camera must be in default exclusions"); - - assert!( - camera.bundle_identifier.is_none(), - "Default camera exclusion should match by title only" - ); - assert!( - camera.owner_name.is_none(), - "Default camera exclusion should match by title only" - ); - } -} diff --git a/apps/desktop/src-tauri/src/window_position_persistence.rs b/apps/desktop/src-tauri/src/window_position_persistence.rs deleted file mode 100644 index 2831a1bb5af..00000000000 --- a/apps/desktop/src-tauri/src/window_position_persistence.rs +++ /dev/null @@ -1,126 +0,0 @@ -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}; - -#[derive(Default)] -struct PendingState { - main: Option, - camera_position: Option<(f64, f64, Option)>, -} - -pub struct WindowPositionPersistence { - pending: Mutex, - notify: Arc, -} - -impl WindowPositionPersistence { - fn new() -> Arc { - Arc::new(Self { - pending: Mutex::new(PendingState::default()), - notify: Arc::new(Notify::new()), - }) - } - - pub fn queue_main(&self, position: WindowPosition) { - { - let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner()); - guard.main = Some(position); - } - self.notify.notify_one(); - } - - pub fn queue_camera(&self, x: f64, y: f64, display_id: Option) { - { - let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner()); - guard.camera_position = Some((x, y, display_id)); - } - self.notify.notify_one(); - } - - fn take_pending(&self) -> PendingState { - let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner()); - std::mem::take(&mut *guard) - } -} - -pub fn install(app: &AppHandle) { - let persistence = WindowPositionPersistence::new(); - app.manage(persistence.clone()); - - let app_handle = app.clone(); - let notify = persistence.notify.clone(); - tokio::spawn(async move { - const DEBOUNCE: Duration = Duration::from_millis(350); - const MIN_INTERVAL: Duration = Duration::from_millis(150); - - let mut last_flush = std::time::Instant::now() - .checked_sub(MIN_INTERVAL) - .unwrap_or_else(std::time::Instant::now); - - loop { - notify.notified().await; - - if crate::app_is_exiting(&app_handle) { - break; - } - - tokio::time::sleep(DEBOUNCE).await; - - if crate::app_is_exiting(&app_handle) { - break; - } - - let elapsed = last_flush.elapsed(); - let remaining = MIN_INTERVAL.saturating_sub(elapsed); - if !remaining.is_zero() { - tokio::time::sleep(remaining).await; - } - - let Some(persistence) = app_handle.try_state::>() else { - break; - }; - let pending = persistence.take_pending(); - - if pending.main.is_none() && pending.camera_position.is_none() { - continue; - } - - let write_app = app_handle.clone(); - let write_result = tokio::task::spawn_blocking(move || { - GeneralSettingsStore::update(&write_app, |settings| { - if let Some(main) = pending.main { - settings.main_window_position = Some(main); - } - if let Some((x, y, display_id)) = pending.camera_position { - crate::update_camera_window_position_settings(settings, x, y, display_id); - } - }) - }) - .await; - - match write_result { - Ok(Ok(())) => {} - Ok(Err(err)) => tracing::warn!("Failed to persist window position: {err}"), - Err(err) => tracing::warn!("Window position persistence task panicked: {err}"), - } - - last_flush = std::time::Instant::now(); - } - }); -} - -pub fn queue_main_position(app: &AppHandle, position: WindowPosition) { - if let Some(persistence) = app.try_state::>() { - persistence.queue_main(position); - } -} - -pub fn queue_camera_position(app: &AppHandle, x: f64, y: f64, display_id: Option) { - if let Some(persistence) = app.try_state::>() { - persistence.queue_camera(x, y, display_id); - } -} diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs deleted file mode 100644 index 8e25dc422a0..00000000000 --- a/apps/desktop/src-tauri/src/windows.rs +++ /dev/null @@ -1,4483 +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::{ - collections::HashMap, - ops::Deref, - path::{Path, PathBuf}, - str::FromStr, - sync::{ - Arc, Mutex, Weak, - 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::{EditorInstances, 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, ScreenshotEditorInstances}, - 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 TELEPROMPTER_PANEL_LEVEL: objc2_app_kit::NSWindowLevel = 101; - -const DEFAULT_FALLBACK_DISPLAY_WIDTH: f64 = 1920.0; -const DEFAULT_FALLBACK_DISPLAY_HEIGHT: f64 = 1080.0; - -#[tauri::command] -#[specta::specta] -pub(crate) async fn restore_main_window_geometry(window: WebviewWindow) -> Result { - if window.label() != "main" { - return Err("Only the main window can restore its geometry".into()); - } - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = window.app_handle().clone(); - handle - .run_on_main_thread(move || { - let result = restore_main_window_bounds(&window).map(|()| false); - let _ = tx.send(result.map_err(|error| error.to_string())); - }) - .map_err(|error| error.to_string())?; - rx.await.map_err(|error| error.to_string())? -} - -fn restore_main_window_bounds(window: &WebviewWindow) -> tauri::Result<()> { - let inner = window.inner_size()?; - let outer = window.outer_size()?; - let scale = window.scale_factor()?; - let before = window.outer_position().ok(); - let monitor = window.current_monitor().ok().flatten(); - let frame = ( - (f64::from(outer.width) - f64::from(inner.width)).max(0.0) / scale, - (f64::from(outer.height) - f64::from(inner.height)).max(0.0) / scale, - ); - let (width, height) = crate::main_window_geometry::SIZE; - if (width - f64::from(inner.width) / scale).abs() > 0.5 - || (height - f64::from(inner.height) / scale).abs() > 0.5 - { - window.set_size(LogicalSize::new(width, height))?; - } - if let Some((before, monitor)) = before.zip(monitor) - && let Ok(after) = window.outer_position() - { - let area = monitor.work_area(); - let (x, y) = crate::main_window_geometry::restored_position( - (f64::from(before.x), f64::from(before.y)), - ((width + frame.0) * scale, (height + frame.1) * scale), - (f64::from(area.position.x), f64::from(area.position.y)), - (f64::from(area.size.width), f64::from(area.size.height)), - scale, - ); - if x != f64::from(after.x) || y != f64::from(after.y) { - let _ = window.set_position(PhysicalPosition::new( - (x + 0.5).floor() as i32, - (y + 0.5).floor() as i32, - )); - } - } - Ok(()) -} - -#[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) { - if let Some(state) = window.app_handle().try_state::() { - state.suspend_overlay(window.label()); - } - let _ = window.set_ignore_cursor_events(true); - if let Err(error) = window.hide() { - warn!(%error, label = window.label(), "Failed to hide overlay; destroying it"); - let _ = window.destroy(); - } -} - -pub fn show_overlay(window: &WebviewWindow) { - if let Some(session) = window - .app_handle() - .state::() - .picker_session() - { - crate::target_select_overlay::restore_overlay_reveal(window, session); - } -} - -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::(); - if let Some(focus_manager) = focus_manager.as_ref() { - focus_manager.suspend_all_overlays(); - } - - 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 if matches!(id, CapWindowId::Main) { - crate::hide_main_window(app); - } 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) { - let app_handle = app_state.handle.clone(); - let requested = app_handle.state::(); - let snapshot = requested.snapshot(); - if snapshot.camera.pending - || snapshot.camera.error.is_some() - || snapshot.camera.value != app_state.selected_camera_id - || app_state.is_recording_active_or_pending() - { - return; - } - if let Some(id) = app_state.selected_camera_id.clone() - && !app_state.camera_in_use - { - if let Err(error) = crate::permissions::check_camera_access() { - warn!(%error, "Camera preview requires permission before restoring input"); - return; - } - 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; - } - } - - requested.publish_camera_if_current(snapshot.camera.revision, || { - app_state.camera_in_use = true; - app_state.camera_cleanup_done = false; - }); - } -} - -pub(crate) async fn restore_main_window_inputs(app: &AppHandle) { - crate::restore_requested_inputs(app).await; -} - -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)); -} - -#[derive(Clone, Copy, Debug, PartialEq)] -struct ContentWindowRect { - x: f64, - y: f64, - width: f64, - height: f64, -} - -#[derive(Debug, PartialEq)] -struct ContentWindowFit { - frame: ContentWindowRect, - inner: (f64, f64), - minimum: (f64, f64), -} - -fn fit_content_window( - work_area: ContentWindowRect, - frame: ContentWindowRect, - inner: (f64, f64), - minimum: (f64, f64), - preferred: Option<(f64, f64)>, -) -> Option { - let requested = preferred.unwrap_or(inner); - if ![work_area.x, work_area.y, frame.x, frame.y] - .into_iter() - .all(f64::is_finite) - || ![ - work_area.width, - work_area.height, - frame.width, - frame.height, - inner.0, - inner.1, - minimum.0, - minimum.1, - requested.0, - requested.1, - ] - .into_iter() - .all(|value| value.is_finite() && value > 0.0) - { - return None; - } - - let decoration = ( - (frame.width - inner.0).max(0.0), - (frame.height - inner.1).max(0.0), - ); - let available = ( - work_area.width - 32.0 - decoration.0, - work_area.height - 32.0 - decoration.1, - ); - if available.0 <= 0.0 || available.1 <= 0.0 { - return None; - } - let minimum = (minimum.0.min(available.0), minimum.1.min(available.1)); - let inner = ( - requested.0.clamp(minimum.0, available.0), - requested.1.clamp(minimum.1, available.1), - ); - let width = inner.0 + decoration.0; - let height = inner.1 + decoration.1; - let left = work_area.x + 16.0; - let bottom = work_area.y + 16.0; - let right = (work_area.x + work_area.width - width - 16.0).max(left); - let top = (work_area.y + work_area.height - height - 16.0).max(bottom); - let (x, y) = if preferred.is_some() { - ( - work_area.x + (work_area.width - width) / 2.0, - work_area.y + (work_area.height - height) / 2.0, - ) - } else { - (frame.x.clamp(left, right), frame.y.clamp(bottom, top)) - }; - Some(ContentWindowFit { - frame: ContentWindowRect { - x, - y, - width, - height, - }, - inner, - minimum, - }) -} - -fn monitor_work_area(monitor: &Monitor) -> Option<(ContentWindowRect, f64)> { - let scale = monitor.scale_factor(); - let area = monitor.work_area(); - Some((logical_work_area(area.position, area.size, scale)?, scale)) -} - -fn logical_work_area( - position: PhysicalPosition, - size: PhysicalSize, - scale: f64, -) -> Option { - if !scale.is_finite() || scale <= 0.0 { - return None; - } - Some(ContentWindowRect { - x: position.x as f64 / scale, - y: position.y as f64 / scale, - width: size.width as f64 / scale, - height: size.height as f64 / scale, - }) -} - -fn initial_content_window_fit( - app: &AppHandle, - id: &CapWindowId, -) -> Option<(ContentWindowFit, f64)> { - let preferred = id.preferred_content_size()?; - let monitor = app - .cursor_position() - .ok() - .and_then(|position| { - app.monitor_from_point(position.x, position.y) - .ok() - .flatten() - }) - .or_else(|| app.primary_monitor().ok().flatten())?; - let (area, scale) = monitor_work_area(&monitor)?; - let frame = ContentWindowRect { - width: preferred.0, - height: preferred.1, - ..area - }; - Some(( - fit_content_window(area, frame, preferred, id.min_size()?, Some(preferred))?, - scale, - )) -} - -async fn fit_content_window_bounds(window: &WebviewWindow, id: &CapWindowId, initial: bool) { - let Some(preferred) = id.preferred_content_size() else { - return; - }; - let Some(minimum) = id.min_size() else { - return; - }; - if window.is_maximized().unwrap_or(false) || window.is_fullscreen().unwrap_or(false) { - return; - } - #[cfg(target_os = "macos")] - { - let (tx, rx) = tokio::sync::oneshot::channel(); - let result = window.run_on_main_thread({ - let window = window.clone(); - move || { - if tx.is_closed() { - return; - } - let result = - fit_macos_content_window(&window, minimum, initial.then_some(preferred)); - let _ = tx.send(result); - } - }); - if let Err(error) = result { - warn!(%error, "Failed to schedule content window bounds update"); - } else if let Err(error) = - await_window_operation(rx, "Content window bounds update", Duration::from_secs(5)).await - { - warn!(%error, "Failed to fit content window bounds"); - } - } - #[cfg(not(target_os = "macos"))] - { - let initial_monitor = initial - .then(|| { - window - .app_handle() - .cursor_position() - .ok() - .and_then(|position| { - window - .monitor_from_point(position.x, position.y) - .ok() - .flatten() - }) - }) - .flatten(); - let Some((area, scale)) = initial_monitor - .or_else(|| window.current_monitor().ok().flatten()) - .or_else(|| window.primary_monitor().ok().flatten()) - .as_ref() - .and_then(monitor_work_area) - else { - return; - }; - let (Ok(position), Ok(outer), Ok(inner), Ok(current_scale)) = ( - window.outer_position(), - window.outer_size(), - window.inner_size(), - window.scale_factor(), - ) else { - return; - }; - if !current_scale.is_finite() || current_scale <= 0.0 { - return; - } - let frame = ContentWindowRect { - x: position.x as f64 / current_scale, - y: position.y as f64 / current_scale, - width: outer.width as f64 / current_scale, - height: outer.height as f64 / current_scale, - }; - let Some(fit) = fit_content_window( - area, - frame, - ( - inner.width as f64 / current_scale, - inner.height as f64 / current_scale, - ), - minimum, - initial.then_some(preferred), - ) else { - return; - }; - let _ = window.set_min_size(Some(LogicalSize::new(fit.minimum.0, fit.minimum.1))); - #[cfg(windows)] - { - let _ = window.set_position(PhysicalPosition::new( - (fit.frame.x * scale).round() as i32, - (fit.frame.y * scale).round() as i32, - )); - let _ = window.set_size(PhysicalSize::new( - (fit.inner.0 * scale).round() as u32, - (fit.inner.1 * scale).round() as u32, - )); - } - #[cfg(target_os = "linux")] - { - let _ = scale; - let _ = window.set_size(LogicalSize::new(fit.inner.0, fit.inner.1)); - let _ = window.set_position(LogicalPosition::new(fit.frame.x, fit.frame.y)); - } - } -} - -#[cfg(target_os = "macos")] -fn fit_macos_content_window( - window: &WebviewWindow, - minimum: (f64, f64), - preferred: Option<(f64, f64)>, -) -> Result<(), String> { - use objc2::{MainThreadMarker, runtime::NSObjectProtocol, sel}; - use objc2_app_kit::{NSEvent, NSScreen, NSWindow}; - use objc2_foundation::{NSPoint, NSRect, NSSize}; - - let main_thread = MainThreadMarker::new().ok_or("Window bounds require the main thread")?; - let native = window.ns_window().map_err(|error| error.to_string())? as *const NSWindow; - let native = unsafe { native.as_ref() }.ok_or("Content window is unavailable")?; - let cursor_screen = preferred.and_then(|_| { - let cursor = unsafe { NSEvent::mouseLocation() }; - NSScreen::screens(main_thread).iter().find(|screen| { - let frame = screen.frame(); - cursor.x >= frame.origin.x - && cursor.x < frame.origin.x + frame.size.width - && cursor.y >= frame.origin.y - && cursor.y < frame.origin.y + frame.size.height - }) - }); - let screen = cursor_screen - .or_else(|| native.screen()) - .or_else(|| NSScreen::mainScreen(main_thread)) - .ok_or("Content window screen is unavailable")?; - let visible = screen.visibleFrame(); - let screen_frame = screen.frame(); - let safe_top = if screen.respondsToSelector(sel!(safeAreaInsets)) { - unsafe { screen.safeAreaInsets().top } - } else { - 0.0 - }; - let top = (visible.origin.y + visible.size.height) - .min(screen_frame.origin.y + screen_frame.size.height - safe_top); - let area = ContentWindowRect { - x: visible.origin.x, - y: visible.origin.y, - width: visible.size.width, - height: top - visible.origin.y, - }; - let frame = native.frame(); - let content = native - .contentView() - .ok_or("Content view is unavailable")? - .frame(); - let Some(fit) = fit_content_window( - area, - ContentWindowRect { - x: frame.origin.x, - y: frame.origin.y, - width: frame.size.width, - height: frame.size.height, - }, - (content.size.width, content.size.height), - minimum, - preferred, - ) else { - return Ok(()); - }; - native.setMinSize(NSSize::new( - fit.minimum.0 + fit.frame.width - fit.inner.0, - fit.minimum.1 + fit.frame.height - fit.inner.1, - )); - native.setFrame_display( - NSRect::new( - NSPoint::new(fit.frame.x, fit.frame.y), - NSSize::new(fit.frame.width, fit.frame.height), - ), - true, - ); - Ok(()) -} - -#[cfg(any(target_os = "macos", test))] -async fn await_window_operation( - receiver: tokio::sync::oneshot::Receiver>, - operation: &str, - timeout: Duration, -) -> Result<(), String> { - tokio::time::timeout(timeout, receiver) - .await - .map_err(|_| format!("{operation} timed out"))? - .map_err(|_| format!("{operation} was cancelled"))? -} - -#[cfg(target_os = "macos")] -struct PendingControlsWindow(Option); - -#[cfg(target_os = "macos")] -impl Drop for PendingControlsWindow { - fn drop(&mut self) { - if let Some(window) = self.0.take() { - let _ = window.destroy(); - } - } -} - -#[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 { - fn preferred_content_size(&self) -> Option<(f64, f64)> { - match self { - Self::Settings => Some((782.0, 775.0)), - Self::Editor { .. } => Some((1275.0, 800.0)), - Self::ScreenshotEditor { .. } => Some((1240.0, 800.0)), - _ => None, - } - } - 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 => crate::main_window_geometry::SIZE, - 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 fn show<'a>( - &'a self, - app: &'a AppHandle, - ) -> futures::future::BoxFuture<'a, tauri::Result> { - let picker_session = matches!(self, Self::TargetSelectOverlay { .. }) - .then(|| app.state::().picker_session()) - .flatten(); - Box::pin(self.show_inner(app, picker_session)) - } - - pub(crate) fn show_for_picker<'a>( - &'a self, - app: &'a AppHandle, - session: u32, - ) -> futures::future::BoxFuture<'a, tauri::Result> { - Box::pin(self.show_inner(app, Some(session))) - } - - async fn show_inner( - &self, - app: &AppHandle, - picker_session: Option, - ) -> tauri::Result { - let picker = app.state::(); - let _picker_creation = if matches!(self, Self::TargetSelectOverlay { .. }) { - let guard = picker.creation.lock().await; - if picker_session.is_none_or(|session| !picker.picker_is_current(session)) { - return Err(tauri::Error::WindowNotFound); - } - Some(guard) - } else { - None - }; - let reveal_generation = crate::clean_capture::generation(app); - if matches!(self, Self::Main { .. }) && crate::clean_capture::phase(app).is_some() { - crate::clean_capture::show_main_controls(app) - .await - .map_err(|error| tauri::Error::Io(std::io::Error::other(error)))?; - } - #[cfg(target_os = "linux")] - crate::clean_capture::admit_wayland_window_creation(app) - .map_err(|error| tauri::Error::Io(std::io::Error::other(error)))?; - let mut project_opening = match self { - Self::Editor { project_path } => { - let state = app.state::(); - Some( - ProjectWindowOpening::acquire( - app, - project_path, - state.ids.clone(), - &state.counter, - &state.open_gates, - |id| CapWindowId::Editor { id }, - ) - .await?, - ) - } - Self::ScreenshotEditor { path } => { - let state = app.state::(); - Some( - ProjectWindowOpening::acquire( - app, - path, - state.ids.clone(), - &state.counter, - &state.open_gates, - |id| CapWindowId::ScreenshotEditor { id }, - ) - .await?, - ) - } - _ => None, - }; - let window_id = project_opening - .as_ref() - .map(|opening| opening.id.clone()) - .unwrap_or_else(|| self.id(app)); - - 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) { - if crate::clean_capture::phase(app) - .is_some_and(|phase| phase != crate::clean_capture::Phase::Restoring) - { - return Ok(window); - } - #[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); - } - crate::clean_capture::guarded_show( - window.clone(), - reveal_generation, - true, - false, - ) - .await?; - 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(); - let (show_tx, show_rx) = tokio::sync::oneshot::channel(); - app.run_on_main_thread({ - let app = app.clone(); - move || { - if show_tx.is_closed() { - return; - } - use tauri_nspanel::ManagerExt; - let result = app - .get_webview_panel(&label) - .map(|panel| { - panel.order_front_regardless(); - panel.show(); - }) - .map_err(|error| { - format!("Recording controls panel is unavailable: {error:?}") - }); - let _ = show_tx.send(result); - } - })?; - await_window_operation( - show_rx, - "Showing recording controls", - Duration::from_secs(5), - ) - .await - .map_err(|error| tauri::Error::Anyhow(anyhow!(error)))?; - 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)); - crate::clean_capture::guarded_show(window.clone(), reveal_generation, true, false) - .await?; - fake_window::spawn_fake_window_listener(app.clone(), window.clone()); - return Ok(window); - } - - let existing_window = match project_opening.as_ref() { - Some(opening) => opening.existing_window.clone(), - None if !matches!(self, Self::Camera { .. } | Self::InProgressRecording { .. }) => { - window_id.get(app) - } - None => None, - }; - if !matches!(self, Self::Camera { .. } | Self::InProgressRecording { .. }) - && let Some(window) = existing_window - { - if let Some(session) = picker_session { - crate::target_select_overlay::request_overlay_reveal(&window, session, false); - return Ok(window); - } - if matches!(self, Self::WindowCaptureOccluder { .. }) - && let Err(error) = window.set_ignore_cursor_events(true) - { - hide_overlay(&window); - let _ = window.destroy(); - return Err(error); - } - 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 - { - crate::hide_main_window(app); - 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 { .. }) { - recenter_window_if_offscreen(&window); - } - fit_content_window_bounds(&window, &window_id, false).await; - - crate::clean_capture::guarded_show(window.clone(), reveal_generation, true, true) - .await?; - - 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 window_id.activates_dock() { - crate::permissions::sync_macos_dock_visibility(app); - } - - return Ok(window); - } - - let _id = window_id; - 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 crate::panel_manager::try_to_panel; - - // Tao's delegate carries the native events from queued AppKit resizes. - 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, - ); - - let resized_window = window.clone(); - window.on_window_event(move |event| { - if matches!(event, tauri::WindowEvent::Resized(_) | tauri::WindowEvent::ScaleFactorChanged { .. }) - && let Err(error) = crate::platform::constrain_main_window_to_visible_frame( - &resized_window.as_ref().window(), - ) - { - warn!(%error, "Failed to fit resized Main window to its visible screen area"); - } - }); - - 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); - } - } - - crate::clean_capture::guarded_show( - window.clone(), - reveal_generation, - false, - false, - ) - .await?; - } - - window - } - Self::TargetSelectOverlay { - display_id, - target_mode, - } => { - let picker_session = picker_session.ok_or(tauri::Error::WindowNotFound)?; - let overlay_instance = picker - .register_overlay(&self.id(app).label(), picker_session) - .ok_or(tauri::Error::WindowNotFound)?; - 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}&overlayInstance={overlay_instance}{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(all(target_os = "macos", debug_assertions))] - { - window_builder = window_builder.background_throttling( - tauri::utils::config::BackgroundThrottlingPolicy::Disabled, - ); - } - - #[cfg(debug_assertions)] - if crate::picker_benchmark::enabled() { - window_builder = - window_builder.initialization_script(crate::picker_benchmark::SCRIPT); - } - - #[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")] - if cap_recording::screenshot::uses_wayland_portal() { - let Some(bounds) = display.raw_handle().logical_bounds() else { - return Err(tauri::Error::WindowNotFound); - }; - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } else { - 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()); - } - - if !picker.picker_is_current(picker_session) { - return Err(tauri::Error::WindowNotFound); - } - let window = window_builder.build()?; - lock_window_text_scale(&window); - if !picker.picker_is_current(picker_session) { - let _ = window.destroy(); - return Err(tauri::Error::WindowNotFound); - } - - if let Err(error) = window.set_ignore_cursor_events(true) { - hide_overlay(&window); - let _ = window.destroy(); - return Err(error); - } - - #[cfg(target_os = "linux")] - if cap_recording::screenshot::uses_wayland_portal() { - use tauri::{LogicalPosition, LogicalSize}; - let Some(bounds) = display.raw_handle().logical_bounds() else { - return Err(tauri::Error::WindowNotFound); - }; - let _ = window.set_position(LogicalPosition::new( - bounds.position().x(), - bounds.position().y(), - )); - let _ = window.set_size(LogicalSize::new( - bounds.size().width(), - bounds.size().height(), - )); - } else { - 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(), picker_session); - - #[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::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 panel = match window.to_panel() { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert target select overlay to panel: {:?}", e); - hide_overlay(&window); - let _ = window.destroy(); - crate::permissions::sync_macos_dock_visibility(&app); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - #[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); - - crate::target_select_overlay::mark_overlay_native_ready(&window, overlay_instance); - - crate::permissions::schedule_macos_dock_visibility_sync(&app); - } - }) - .ok(); - } - - #[cfg(not(target_os = "macos"))] - { - crate::target_select_overlay::mark_overlay_native_ready( - &window, - overlay_instance, - ); - } - - window - } - Self::Settings { page } => { - let mut builder = self - .window_builder( - app, - format!("/settings/{}", page.clone().unwrap_or_default()), - ) - .resizable(true) - .maximized(false) - .focused(true); - - #[cfg(target_os = "macos")] - { - builder = builder.transparent(true); - } - - let window = builder.build()?; - lock_window_text_scale(&window); - - fit_content_window_bounds(&window, &_id, true).await; - - window - } - Self::Editor { project_path } => { - let open_started = std::time::Instant::now(); - hide_recording_windows(app, false); - release_camera_preview_if_idle(app); - - PendingEditorInstances::start_prewarm(app, _id.label(), project_path.clone()).await; - - let builder = self - .window_builder_with_id(app, "/editor", &_id, _id.label()) - .maximizable(true) - .focused(true); - #[cfg(debug_assertions)] - let builder = if crate::stop_editor_benchmark::enabled() { - builder.initialization_script(crate::stop_editor_benchmark::SCRIPT) - } else { - builder - }; - #[cfg(debug_assertions)] - let builder = if crate::stop_editor_benchmark::dom_capture_requested() { - builder.initialization_script(crate::stop_editor_benchmark::DOM_SCRIPT) - } else { - builder - }; - let window = builder.build()?; - if let Some(opening) = project_opening.as_mut() { - opening.own_window(&window); - } - lock_window_text_scale(&window); - - fit_content_window_bounds(&window, &_id, true).await; - - // 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); - - PendingScreenshotEditorInstances::start_prewarm(app, _id.label(), path.clone()) - .await; - - let window = self - .window_builder_with_id(app, "/screenshot-editor", &_id, _id.label()) - .maximizable(true) - .focused(true) - .build()?; - if let Some(opening) = project_opening.as_mut() { - opening.own_window(&window); - } - lock_window_text_scale(&window); - - fit_content_window_bounds(&window, &_id, true).await; - - window.show().ok(); - window.set_focus().ok(); - - window - } - Self::Upgrade => { - crate::hide_main_window(app); - - 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 => { - crate::hide_main_window(app); - - 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 => { - crate::hide_main_window(app); - - 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 = {}; - window.__CAP__.cleanCaptureGeneration = {}; - ", - state.camera_ws_port, - centered, - enable_native_camera_preview, - reveal_generation - )) - .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 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 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, - ); - - 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"))] - { - crate::clean_capture::guarded_show( - window.clone(), - reveal_generation, - false, - false, - ) - .await?; - } - - 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")] - if cap_recording::screenshot::uses_wayland_portal() { - let Some(bounds) = display.raw_handle().logical_bounds() else { - return Err(tauri::Error::WindowNotFound); - }; - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } else { - 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")] - if cap_recording::screenshot::uses_wayland_portal() { - use tauri::{LogicalPosition, LogicalSize}; - let Some(bounds) = display.raw_handle().logical_bounds() else { - return Err(tauri::Error::WindowNotFound); - }; - let _ = window.set_position(LogicalPosition::new( - bounds.position().x(), - bounds.position().y(), - )); - let _ = window.set_size(LogicalSize::new( - bounds.size().width(), - bounds.size().height(), - )); - } else { - 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"); - hide_overlay(&window); - let _ = window.destroy(); - return Err(err); - } - - #[cfg(target_os = "macos")] - { - crate::platform::set_window_level(window.as_ref().window(), 900); - } - - crate::clean_capture::guarded_show(window.clone(), reveal_generation, false, false) - .await?; - - 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 cap_recording::screenshot::uses_wayland_portal() { - let Some(bounds) = display.raw_handle().logical_bounds() else { - return Err(tauri::Error::WindowNotFound); - }; - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } else 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 cap_recording::screenshot::uses_wayland_portal() { - use tauri::{LogicalPosition, LogicalSize}; - let Some(bounds) = display.raw_handle().logical_bounds() else { - return Err(tauri::Error::WindowNotFound); - }; - let _ = window.set_position(LogicalPosition::new( - bounds.position().x(), - bounds.position().y(), - )); - let _ = window.set_size(LogicalSize::new( - bounds.size().width(), - bounds.size().height(), - )); - } else 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")] - { - let mut pending_window = PendingControlsWindow(Some(window.clone())); - let (show_tx, show_rx) = tokio::sync::oneshot::channel(); - let scheduled = 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; - if show_tx.is_closed() { - crate::permissions::sync_macos_dock_visibility(&app); - return; - } - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - 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 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); - let _ = show_tx.send(Err(format!("Failed to prepare recording controls: {e:?}"))); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - 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); - let _ = show_tx.send(Ok(())); - } - }); - let shown = match scheduled { - Ok(()) => await_window_operation( - show_rx, - "Showing recording controls", - Duration::from_secs(5), - ) - .await - .map_err(|error| tauri::Error::Anyhow(anyhow!(error))), - Err(error) => Err(error), - }; - shown?; - pending_window.0 = None; - - 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); - } - - if let Some(opening) = project_opening.as_mut() { - opening.commit(); - } - 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_id(app, url, &id, 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); - - self.window_builder_with_id(app, url, &id, label) - } - - fn window_builder_with_id<'a>( - &'a self, - app: &'a AppHandle, - url: impl Into, - id: &CapWindowId, - label: impl Into, - ) -> WebviewWindowBuilder<'a, Wry, AppHandle> { - 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)); - - #[cfg(all(target_os = "macos", debug_assertions))] - if matches!(id, CapWindowId::Main) { - // Main stays hidden until its frontend mounts; WebKit must finish loading Vite first. - builder = builder - .background_throttling(tauri::utils::config::BackgroundThrottlingPolicy::Disabled); - } - - 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() { - let preferred = id.preferred_content_size().unwrap_or(min); - let (inner, minimum) = initial_content_window_fit(app, id) - .map(|(fit, _)| (fit.inner, fit.minimum)) - .unwrap_or((preferred, min)); - builder = builder - .inner_size(inner.0, inner.1) - .min_inner_size(minimum.0, minimum.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 id = project_window_id_for_key(&state.ids, &project_window_key(project_path)) - .expect("editor window is not reserved"); - 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 id = project_window_id_for_key(&state.ids, &project_window_key(path)) - .expect("screenshot editor window is not reserved"); - 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 known capture-based remote displays (Shadow and RDP), 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() - && native_content_protection_allowed(window_title) - && window_capture_excluded(app, window_title) -} - -fn native_content_protection_allowed(window_title: &str) -> bool { - !cfg!(target_os = "macos") || window_title != CapWindowId::RecordingControls.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 - && native_content_protection_allowed(&title) - && 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); - } - } -} - -fn project_window_key(path: &Path) -> PathBuf { - std::fs::canonicalize(path) - .or_else(|_| std::path::absolute(path)) - .unwrap_or_else(|_| path.to_path_buf()) -} - -fn project_window_id_for_key(ids: &Mutex>, key: &Path) -> Option { - let entries = ids.lock().unwrap().clone(); - entries - .into_iter() - .find_map(|(path, id)| (project_window_key(&path) == key).then_some(id)) -} - -fn next_project_window_id(counter: &AtomicU32) -> tauri::Result { - counter - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |value| { - value.checked_add(1) - }) - .map_err(|_| tauri::Error::Io(std::io::Error::other("project window IDs exhausted"))) -} - -#[derive(Default, Clone)] -struct ProjectWindowOpenGates { - paths: Arc>, -} - -type ProjectWindowGateMap = HashMap>>; - -impl ProjectWindowOpenGates { - fn for_path(&self, key: PathBuf) -> Arc> { - let mut paths = self.paths.lock().unwrap(); - paths.retain(|_, gate| gate.strong_count() > 0); - if let Some(gate) = paths.get(&key).and_then(Weak::upgrade) { - return gate; - } - let gate = Arc::new(tokio::sync::Mutex::new(())); - paths.insert(key, Arc::downgrade(&gate)); - gate - } -} - -struct ProjectWindowOpening { - app: AppHandle, - ids: Arc>>, - id: CapWindowId, - existing_window: Option, - owned_window: Option, - committed: bool, - _gate: tokio::sync::OwnedMutexGuard<()>, -} - -impl ProjectWindowOpening { - async fn acquire( - app: &AppHandle, - path: &Path, - ids: Arc>>, - counter: &AtomicU32, - gates: &ProjectWindowOpenGates, - make_id: fn(u32) -> CapWindowId, - ) -> tauri::Result { - let key = project_window_key(path); - let gate = gates.for_path(key.clone()).lock_owned().await; - let previous_id = project_window_id_for_key(&ids, &key); - if let Some(previous_id) = previous_id { - let id = make_id(previous_id); - if let Some(window) = id.get(app) { - return Ok(Self { - app: app.clone(), - ids, - id, - existing_window: Some(window), - owned_window: None, - committed: true, - _gate: gate, - }); - } - } - - let numeric_id = next_project_window_id(counter)?; - let id = make_id(numeric_id); - { - let mut entries = ids.lock().unwrap(); - entries.retain(|(_, id)| Some(*id) != previous_id); - entries.push((path.to_path_buf(), numeric_id)); - } - Ok(Self { - app: app.clone(), - ids, - id, - existing_window: None, - owned_window: None, - committed: false, - _gate: gate, - }) - } - - fn own_window(&mut self, window: &WebviewWindow) { - self.owned_window = Some(window.clone()); - } - - fn commit(&mut self) { - self.committed = true; - } -} - -impl Drop for ProjectWindowOpening { - fn drop(&mut self) { - if self.committed { - return; - } - let numeric_id = match self.id { - CapWindowId::Editor { id } | CapWindowId::ScreenshotEditor { id } => id, - _ => return, - }; - self.ids - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .retain(|(_, id)| *id != numeric_id); - if let Some(window) = self.owned_window.take() { - if let Err(error) = window.destroy() { - warn!(label = %self.id.label(), %error, "Failed to destroy unfinished project window"); - } - let native_window = window.as_ref().window().clone(); - match self.id { - CapWindowId::Editor { .. } => { - tauri::async_runtime::spawn(EditorInstances::remove(native_window)); - } - CapWindowId::ScreenshotEditor { .. } => { - tauri::async_runtime::spawn(ScreenshotEditorInstances::remove(native_window)); - } - _ => {} - } - } - let app = self.app.clone(); - let id = self.id.clone(); - tauri::async_runtime::spawn(async move { - let label = id.label(); - match id { - CapWindowId::Editor { .. } => { - PendingEditorInstances::get(&app) - .cancel_prewarm(&label) - .await; - } - CapWindowId::ScreenshotEditor { .. } => { - PendingScreenshotEditorInstances::get(&app) - .cancel_prewarm(&label) - .await; - } - _ => {} - } - }); - } -} - -#[derive(Default, Clone)] -pub struct EditorWindowIds { - pub ids: Arc>>, - pub counter: Arc, - open_gates: ProjectWindowOpenGates, -} - -impl EditorWindowIds { - pub fn get(app: &AppHandle) -> Self { - app.state::().deref().clone() - } -} - -#[derive(Default, Clone)] -pub struct ScreenshotEditorWindowIds { - pub ids: Arc>>, - pub counter: Arc, - open_gates: ProjectWindowOpenGates, -} - -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 = project_window_id_for_key(&ids.ids, &project_window_key(path))?; - CapWindowId::Editor { id }.get(app) -} - -#[cfg(test)] -mod content_window_tests { - use super::*; - - fn rect(x: f64, y: f64, width: f64, height: f64) -> ContentWindowRect { - ContentWindowRect { - x, - y, - width, - height, - } - } - - fn assert_inside(frame: ContentWindowRect, area: ContentWindowRect) { - assert!(frame.x >= area.x + 16.0); - assert!(frame.y >= area.y + 16.0); - assert!(frame.x + frame.width <= area.x + area.width - 16.0); - assert!(frame.y + frame.height <= area.y + area.height - 16.0); - } - - #[test] - fn small_display_fits_all_content_windows_and_lowers_oversized_minimums() { - let area = rect(0.0, 25.0, 1024.0, 703.0); - for id in [ - CapWindowId::Settings, - CapWindowId::Editor { id: 0 }, - CapWindowId::ScreenshotEditor { id: 0 }, - ] { - let preferred = id.preferred_content_size().unwrap(); - let fit = fit_content_window( - area, - rect(0.0, 0.0, preferred.0, preferred.1), - preferred, - id.min_size().unwrap(), - Some(preferred), - ) - .unwrap(); - assert_inside(fit.frame, area); - assert!(fit.minimum.0 <= fit.inner.0); - assert!(fit.minimum.1 <= fit.inner.1); - } - } - - #[test] - fn roomy_display_keeps_preferred_size_and_minimum() { - let area = rect(0.0, 24.0, 2560.0, 1376.0); - let preferred = (1275.0, 800.0); - let fit = fit_content_window( - area, - rect(0.0, 0.0, 1275.0, 800.0), - preferred, - preferred, - Some(preferred), - ) - .unwrap(); - assert_eq!(fit.inner, preferred); - assert_eq!(fit.minimum, preferred); - assert_inside(fit.frame, area); - } - - #[test] - fn decorated_window_fits_outer_frame_on_negative_origin_display() { - let area = rect(-1536.0, 24.0, 1536.0, 800.0); - let fit = fit_content_window( - area, - rect(0.0, 0.0, 1291.0, 839.0), - (1275.0, 800.0), - (1275.0, 800.0), - Some((1275.0, 800.0)), - ) - .unwrap(); - assert_eq!(fit.inner, (1275.0, 729.0)); - assert_eq!(fit.minimum, (1275.0, 729.0)); - assert_inside(fit.frame, area); - } - - #[test] - fn work_area_uses_the_target_monitors_scale() { - for scale in [1.0, 1.25, 2.0] { - let position = PhysicalPosition::new(-1920, 30); - let size = PhysicalSize::new(1920, 1000); - let area = logical_work_area(position, size, scale).unwrap(); - let fit = fit_content_window( - area, - rect(area.x, area.y, 1275.0, 800.0), - (1275.0, 800.0), - (1275.0, 800.0), - Some((1275.0, 800.0)), - ) - .unwrap(); - assert_inside(fit.frame, area); - assert!(fit.frame.x * scale >= position.x as f64); - assert!( - (fit.frame.x + fit.frame.width) * scale <= (position.x + size.width as i32) as f64 - ); - } - assert!( - logical_work_area( - PhysicalPosition::new(0, 0), - PhysicalSize::new(1024, 768), - 0.0 - ) - .is_none() - ); - } - - #[test] - fn reopening_shrinks_and_recovers_an_oversized_offscreen_window() { - let area = rect(1920.0, 0.0, 1024.0, 728.0); - let fit = fit_content_window( - area, - rect(-2000.0, 3000.0, 1800.0, 1200.0), - (1800.0, 1200.0), - (1275.0, 800.0), - None, - ) - .unwrap(); - assert_eq!(fit.inner, (992.0, 696.0)); - assert_inside(fit.frame, area); - } - - #[test] - fn reopening_preserves_a_valid_user_size_and_position() { - let area = rect(0.0, 24.0, 1920.0, 1016.0); - let frame = rect(43.0, 71.0, 900.0, 650.0); - let fit = fit_content_window(area, frame, (900.0, 650.0), (780.0, 560.0), None).unwrap(); - assert_eq!(fit.frame, frame); - } - - #[test] - fn invalid_or_unusable_geometry_is_ignored() { - for area in [ - rect(0.0, 0.0, 0.0, 768.0), - rect(0.0, 0.0, 20.0, 20.0), - rect(f64::NAN, 0.0, 1024.0, 768.0), - ] { - assert!( - fit_content_window( - area, - rect(0.0, 0.0, 1275.0, 800.0), - (1275.0, 800.0), - (1275.0, 800.0), - None - ) - .is_none() - ); - } - } - - #[test] - fn only_macos_controls_bypass_native_content_protection() { - assert_eq!( - native_content_protection_allowed(&CapWindowId::RecordingControls.title()), - !cfg!(target_os = "macos") - ); - assert!(native_content_protection_allowed( - &CapWindowId::Camera.title() - )); - assert!(native_content_protection_allowed( - &CapWindowId::Settings.title() - )); - assert!(native_content_protection_allowed( - &CapWindowId::Teleprompter.title() - )); - } - - #[tokio::test] - async fn panel_show_waits_for_acknowledgment_and_propagates_failure() { - let (tx, rx) = tokio::sync::oneshot::channel(); - let wait = await_window_operation(rx, "Test panel", Duration::from_secs(1)); - tokio::pin!(wait); - assert!(futures::poll!(&mut wait).is_pending()); - tx.send(Err("Native panel conversion failed".to_string())) - .unwrap(); - assert_eq!( - wait.await, - Err("Native panel conversion failed".to_string()) - ); - } - - #[tokio::test] - async fn timed_out_panel_show_cancels_queued_native_work() { - let (tx, rx) = tokio::sync::oneshot::channel(); - let result = await_window_operation(rx, "Test panel", Duration::ZERO).await; - assert_eq!(result, Err("Test panel timed out".to_string())); - assert!(tx.is_closed()); - } - - #[tokio::test] - async fn panel_show_success_and_cancel_are_distinct() { - let (tx, rx) = tokio::sync::oneshot::channel(); - tx.send(Ok(())).unwrap(); - assert!( - await_window_operation(rx, "Test panel", Duration::from_secs(1)) - .await - .is_ok() - ); - let (tx, rx) = tokio::sync::oneshot::channel(); - drop(tx); - assert_eq!( - await_window_operation(rx, "Test panel", Duration::from_secs(1)).await, - Err("Test panel was cancelled".to_string()) - ); - } -} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json deleted file mode 100644 index f7dcad0c39a..00000000000 --- a/apps/desktop/src-tauri/tauri.conf.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "Cap - Development", - "identifier": "so.cap.desktop.dev", - "mainBinaryName": "Cap - Development", - "build": { - "beforeDevCommand": "bun run localdev", - "devUrl": "http://localhost:3002", - "beforeBuildCommand": "bun run 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": { - "minimumSystemVersion": "12.3", - "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", - "pulseaudio-utils", - "libpipewire-0.3-0", - "libasound2t64 | libasound2", - "libxkbcommon0", - "libxkbcommon-x11-0", - "libssl3t64 | libssl3" - ], - "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/apps/desktop/src-tauri/tests/crash_sentinel.rs b/apps/desktop/src-tauri/tests/crash_sentinel.rs deleted file mode 100644 index 2db8770e5bb..00000000000 --- a/apps/desktop/src-tauri/tests/crash_sentinel.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[path = "../src/crash_sentinel.rs"] -pub mod crash_sentinel; diff --git a/apps/desktop/src-tauri/tests/exit_shutdown.rs b/apps/desktop/src-tauri/tests/exit_shutdown.rs deleted file mode 100644 index 6561e8442cb..00000000000 --- a/apps/desktop/src-tauri/tests/exit_shutdown.rs +++ /dev/null @@ -1,527 +0,0 @@ -#[path = "../src/exit_shutdown.rs"] -mod exit_shutdown; - -use exit_shutdown::{ - AppExitAction, ExitBlocked, ExitRequestDecision, UpdateInstallState, abort_join_handles, - app_exit_action, collect_device_inventory, handle_exit_requested, prepare_then_begin_exit, - read_target_under_cursor, recording_start_allowed, run_while_active, with_idle_recording_state, -}; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; - -struct DropFlag(Arc); - -impl Drop for DropFlag { - fn drop(&mut self) { - self.0.store(true, Ordering::Release); - } -} - -#[test] -fn exit_refusals_do_not_start_shutdown_or_disable_active_watchers() { - for reason in [ - ExitBlocked::StateUnavailable, - ExitBlocked::RecordingActive, - ExitBlocked::FinalizationActive, - ExitBlocked::ExportActive, - ExitBlocked::UploadActive, - ExitBlocked::UpdateInstalling, - ] { - let state = tokio::sync::RwLock::new(()); - let exiting = AtomicBool::new(false); - let result = with_idle_recording_state( - &state, - |_| Err(reason), - || { - exiting.store(true, Ordering::Release); - true - }, - ); - assert_eq!(result, Err(reason)); - assert!(!exiting.load(Ordering::Acquire)); - assert_eq!( - run_while_active(|| exiting.load(Ordering::Acquire), || 1), - Some(1) - ); - assert!(!reason.message().is_empty()); - } - assert!(recording_start_allowed(true, false).is_err()); - assert!(!ExitBlocked::AlreadyExiting.message().is_empty()); -} - -#[test] -fn failed_handoff_preparation_leaves_recording_available_and_success_commits_before_unlock() { - for succeeds in [false, true] { - let state = tokio::sync::RwLock::new(()); - let exiting = AtomicBool::new(false); - let prepared = AtomicBool::new(false); - let result = with_idle_recording_state( - &state, - |_| Ok(()), - || { - prepare_then_begin_exit( - || { - assert!(state.try_write().is_err()); - assert!(!exiting.load(Ordering::Acquire)); - prepared.store(true, Ordering::Release); - if succeeds { - Ok(()) - } else { - Err("Child launch failed".into()) - } - }, - || { - assert!(state.try_write().is_err()); - assert!(prepared.load(Ordering::Acquire)); - !exiting.swap(true, Ordering::AcqRel) - }, - ) - }, - ) - .unwrap(); - assert_eq!(result.is_ok(), succeeds); - assert_eq!(exiting.load(Ordering::Acquire), succeeds); - assert!(state.try_write().is_ok()); - assert_eq!( - recording_start_allowed(exiting.load(Ordering::Acquire), false).is_ok(), - !succeeds - ); - } -} - -#[test] -fn recording_and_finalization_refusals_never_dispatch_an_installer() { - for reason in [ - ExitBlocked::RecordingActive, - ExitBlocked::FinalizationActive, - ] { - let state = tokio::sync::RwLock::new(()); - let install = UpdateInstallState::default(); - let dispatched = AtomicBool::new(false); - let result = with_idle_recording_state( - &state, - |_| Err(reason), - || { - let _admission = install.begin().unwrap(); - dispatched.store(true, Ordering::Release); - }, - ); - assert_eq!(result, Err(reason)); - assert!(!dispatched.load(Ordering::Acquire)); - assert!(!install.blocks_recording()); - } -} - -#[test] -fn installer_failure_releases_only_its_start_block_without_beginning_shutdown() { - let state = tokio::sync::RwLock::new(false); - let install = UpdateInstallState::default(); - let exiting = AtomicBool::new(false); - let run_install = || -> Result<(), &'static str> { - let _admission = with_idle_recording_state( - &state, - |_| Ok(()), - || { - assert!(state.try_write().is_err()); - install.begin().unwrap() - }, - ) - .unwrap(); - assert!(install.is_installing()); - assert!(install.begin().is_none()); - let mut recording = state.try_write().unwrap(); - let admitted = - recording_start_allowed(exiting.load(Ordering::Acquire), install.blocks_recording()); - if admitted.is_ok() { - *recording = true; - } - assert!(admitted.is_err()); - assert!(!*recording); - Err("Installer failed before dispatch") - }; - assert!(run_install().is_err()); - assert!(!install.blocks_recording()); - assert!(!exiting.load(Ordering::Acquire)); - assert!(recording_start_allowed(false, install.blocks_recording()).is_ok()); - assert!(run_install().is_err()); - assert!(!install.blocks_recording()); -} - -#[test] -fn successful_installer_keeps_start_block_only_when_it_may_exit_later() { - for can_exit_later in [false, true] { - let install = UpdateInstallState::default(); - let admission = install.begin().unwrap(); - assert!(install.is_installing()); - admission.complete(can_exit_later); - assert!(!install.is_installing()); - assert_eq!(install.blocks_recording(), can_exit_later); - assert_eq!( - recording_start_allowed(false, install.blocks_recording()).is_err(), - can_exit_later - ); - assert_eq!(install.begin().is_none(), can_exit_later); - } -} - -#[test] -fn quit_during_stop_does_not_wait_for_or_interrupt_the_recording_writer() { - let state = tokio::sync::RwLock::new(true); - let mut stopping = state.try_write().unwrap(); - let result = with_idle_recording_state( - &state, - |_| panic!("Busy recording state must not be inspected"), - || -> bool { panic!("Shutdown must not begin while Stop holds the state") }, - ); - assert_eq!(result, Err(ExitBlocked::StateUnavailable)); - *stopping = false; - drop(stopping); - let exiting = AtomicBool::new(false); - assert_eq!( - with_idle_recording_state( - &state, - |recording| { - if *recording { - Err(ExitBlocked::RecordingActive) - } else { - Ok(()) - } - }, - || !exiting.swap(true, Ordering::AcqRel), - ), - Ok(true) - ); -} - -#[test] -fn recording_start_and_exit_admission_cannot_both_succeed() { - let state = Arc::new(tokio::sync::RwLock::new(false)); - let exiting = Arc::new(AtomicBool::new(false)); - let (entered_tx, entered_rx) = std::sync::mpsc::sync_channel(1); - let (release_tx, release_rx) = std::sync::mpsc::sync_channel(1); - let exit = { - let state = state.clone(); - let exiting = exiting.clone(); - std::thread::spawn(move || { - with_idle_recording_state( - &state, - |recording| { - if *recording { - Err(ExitBlocked::RecordingActive) - } else { - Ok(()) - } - }, - || { - entered_tx.send(()).unwrap(); - release_rx - .recv_timeout(std::time::Duration::from_secs(2)) - .unwrap(); - !exiting.swap(true, Ordering::AcqRel) - }, - ) - }) - }; - entered_rx - .recv_timeout(std::time::Duration::from_secs(2)) - .unwrap(); - let start_was_locked = state.try_write().is_err(); - release_tx.send(()).unwrap(); - assert_eq!(exit.join().unwrap(), Ok(true)); - assert!(start_was_locked); - let mut starting = state.try_write().unwrap(); - let start = recording_start_allowed(exiting.load(Ordering::Acquire), false); - if start.is_ok() { - *starting = true; - } - assert!(start.is_err()); - assert!(!*starting); - drop(starting); - - let started_first = tokio::sync::RwLock::new(false); - let mut starting = started_first.try_write().unwrap(); - recording_start_allowed(false, false).unwrap(); - *starting = true; - drop(starting); - assert_eq!( - with_idle_recording_state( - &started_first, - |recording| { - if *recording { - Err(ExitBlocked::RecordingActive) - } else { - Ok(()) - } - }, - || -> bool { panic!("An admitted recording must prevent shutdown") }, - ), - Err(ExitBlocked::RecordingActive) - ); -} - -#[test] -fn run_while_active_skips_operation_once_exit_begins() { - let called = Arc::new(AtomicBool::new(false)); - let flag = called.clone(); - - let result = run_while_active( - || true, - move || { - flag.store(true, Ordering::Release); - 1usize - }, - ); - - assert_eq!(result, None); - assert!(!called.load(Ordering::Acquire)); -} - -#[test] -fn collect_device_inventory_skips_hardware_queries_once_exit_begins() { - let cameras_called = Arc::new(AtomicBool::new(false)); - let microphones_called = Arc::new(AtomicBool::new(false)); - let camera_flag = cameras_called.clone(); - let microphone_flag = microphones_called.clone(); - - let result = collect_device_inventory( - || true, - true, - true, - move || { - camera_flag.store(true, Ordering::Release); - vec![1usize] - }, - move || { - microphone_flag.store(true, Ordering::Release); - vec![2usize] - }, - ); - - assert_eq!(result, None); - assert!(!cameras_called.load(Ordering::Acquire)); - assert!(!microphones_called.load(Ordering::Acquire)); -} - -#[test] -fn collect_device_inventory_respects_permissions_before_probings() { - let cameras_called = Arc::new(AtomicBool::new(false)); - let microphones_called = Arc::new(AtomicBool::new(false)); - let camera_flag = cameras_called.clone(); - let microphone_flag = microphones_called.clone(); - - let result = collect_device_inventory( - || false, - false, - true, - move || { - camera_flag.store(true, Ordering::Release); - vec![1usize] - }, - move || { - microphone_flag.store(true, Ordering::Release); - vec![2usize] - }, - ); - - assert_eq!(result, Some((Vec::::new(), vec![2usize]))); - assert!(!cameras_called.load(Ordering::Acquire)); - assert!(microphones_called.load(Ordering::Acquire)); -} - -#[test] -fn collect_device_inventory_stops_before_second_probe_when_exit_begins_midway() { - let exiting = Arc::new(AtomicBool::new(false)); - let cameras_called = Arc::new(AtomicBool::new(false)); - let microphones_called = Arc::new(AtomicBool::new(false)); - let exit_flag = exiting.clone(); - let camera_flag = cameras_called.clone(); - let microphone_flag = microphones_called.clone(); - - let result = collect_device_inventory( - move || exit_flag.load(Ordering::Acquire), - true, - true, - move || { - camera_flag.store(true, Ordering::Release); - exiting.store(true, Ordering::Release); - vec![1usize] - }, - move || { - microphone_flag.store(true, Ordering::Release); - vec![2usize] - }, - ); - - assert_eq!(result, None); - assert!(cameras_called.load(Ordering::Acquire)); - assert!(!microphones_called.load(Ordering::Acquire)); -} - -#[test] -fn read_target_under_cursor_skips_queries_once_exit_begins() { - let display_called = Arc::new(AtomicBool::new(false)); - let window_called = Arc::new(AtomicBool::new(false)); - let display_flag = display_called.clone(); - let window_flag = window_called.clone(); - - let result = read_target_under_cursor( - || true, - move || { - display_flag.store(true, Ordering::Release); - Some(1usize) - }, - move || { - window_flag.store(true, Ordering::Release); - Some(2usize) - }, - ); - - assert_eq!(result, None); - assert!(!display_called.load(Ordering::Acquire)); - assert!(!window_called.load(Ordering::Acquire)); -} - -#[test] -fn read_target_under_cursor_stops_before_window_query_when_exit_begins_midway() { - let exiting = Arc::new(AtomicBool::new(false)); - let display_called = Arc::new(AtomicBool::new(false)); - let window_called = Arc::new(AtomicBool::new(false)); - let exit_flag = exiting.clone(); - let display_flag = display_called.clone(); - let window_flag = window_called.clone(); - - let result = read_target_under_cursor( - move || exit_flag.load(Ordering::Acquire), - move || { - display_flag.store(true, Ordering::Release); - exiting.store(true, Ordering::Release); - Some(1usize) - }, - move || { - window_flag.store(true, Ordering::Release); - Some(2usize) - }, - ); - - assert_eq!(result, None); - assert!(display_called.load(Ordering::Acquire)); - assert!(!window_called.load(Ordering::Acquire)); -} - -#[tokio::test] -async fn abort_all_tasks_cancels_background_tracking() { - let task_flag = Arc::new(AtomicBool::new(false)); - let tasks_flag = Arc::new(AtomicBool::new(false)); - - let task = { - let task_flag = task_flag.clone(); - tokio::spawn(async move { - let _flag = DropFlag(task_flag); - std::future::pending::<()>().await; - }) - }; - - let tasks = vec![{ - let tasks_flag = tasks_flag.clone(); - tokio::spawn(async move { - let _flag = DropFlag(tasks_flag); - std::future::pending::<()>().await; - }) - }]; - - tokio::task::yield_now().await; - abort_join_handles(tasks, Some(task)); - tokio::task::yield_now().await; - - assert!(task_flag.load(Ordering::Acquire)); - assert!(tasks_flag.load(Ordering::Acquire)); -} - -#[test] -fn app_exit_action_matches_current_platform() { - #[cfg(target_os = "macos")] - assert_eq!(app_exit_action(7), AppExitAction::Process(7)); - - #[cfg(not(target_os = "macos"))] - assert_eq!(app_exit_action(7), AppExitAction::Runtime(7)); -} - -#[test] -fn exit_requested_prevents_user_exit_when_already_exiting() { - let prevented = Arc::new(AtomicBool::new(false)); - let prevented_flag = prevented.clone(); - - let decision = handle_exit_requested(true, false, false, false, move || { - prevented_flag.store(true, Ordering::Release); - }); - - assert_eq!(decision, ExitRequestDecision::AlreadyExiting); - assert!(prevented.load(Ordering::Acquire)); -} - -#[test] -fn exit_requested_allows_runtime_exit_when_already_exiting() { - let prevented = Arc::new(AtomicBool::new(false)); - let prevented_flag = prevented.clone(); - - let decision = handle_exit_requested(true, false, true, false, move || { - prevented_flag.store(true, Ordering::Release); - }); - - assert_eq!(decision, ExitRequestDecision::AllowRuntimeExit); - assert!(!prevented.load(Ordering::Acquire)); -} - -#[test] -fn exit_requested_allows_runtime_exit_when_export_cancel_is_draining() { - let prevented = Arc::new(AtomicBool::new(false)); - let prevented_flag = prevented.clone(); - - let decision = handle_exit_requested(true, true, true, false, move || { - prevented_flag.store(true, Ordering::Release); - }); - - assert_eq!(decision, ExitRequestDecision::AllowRuntimeExit); - assert!(!prevented.load(Ordering::Acquire)); -} - -#[test] -fn exit_requested_prevents_runtime_exit_during_export() { - let prevented = Arc::new(AtomicBool::new(false)); - let prevented_flag = prevented.clone(); - - let decision = handle_exit_requested(false, true, true, false, move || { - prevented_flag.store(true, Ordering::Release); - }); - - assert_eq!(decision, ExitRequestDecision::ExportActive); - assert!(prevented.load(Ordering::Acquire)); -} - -#[test] -fn exit_requested_allows_runtime_restart_without_starting_cleanup() { - let prevented = Arc::new(AtomicBool::new(false)); - let prevented_flag = prevented.clone(); - - let decision = handle_exit_requested(false, false, true, true, move || { - prevented_flag.store(true, Ordering::Release); - }); - - assert_eq!(decision, ExitRequestDecision::AllowRuntimeRestart); - assert!(!prevented.load(Ordering::Acquire)); -} - -#[test] -fn exit_requested_allows_unpreventable_runtime_restart_during_export() { - let prevented = Arc::new(AtomicBool::new(false)); - let prevented_flag = prevented.clone(); - - let decision = handle_exit_requested(false, true, true, true, move || { - prevented_flag.store(true, Ordering::Release); - }); - - assert_eq!(decision, ExitRequestDecision::AllowRuntimeRestart); - assert!(!prevented.load(Ordering::Acquire)); -} diff --git a/apps/desktop/src-tauri/tests/export_estimate_request.rs b/apps/desktop/src-tauri/tests/export_estimate_request.rs deleted file mode 100644 index 9516cd6bd1b..00000000000 --- a/apps/desktop/src-tauri/tests/export_estimate_request.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[path = "../src/export/estimate_request.rs"] -mod estimate_request; diff --git a/apps/desktop/src-tauri/tests/web_api_startup.rs b/apps/desktop/src-tauri/tests/web_api_startup.rs deleted file mode 100644 index c9cf9f24809..00000000000 --- a/apps/desktop/src-tauri/tests/web_api_startup.rs +++ /dev/null @@ -1,43 +0,0 @@ -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 tokio::sync::RwLock; - -#[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 raw_state_access = catch_unwind(AssertUnwindSafe(|| { - let _ = handle.state::>>(); - })); - - assert!(raw_state_access.is_err()); - - let url = futures::executor::block_on(handle.make_app_url("/api/upload/multipart/initiate")); - - assert_eq!( - url, - format!( - "{}{}", - option_env!("VITE_SERVER_URL").unwrap_or("https://cap.so"), - "/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 is_custom = futures::executor::block_on(handle.is_server_url_custom()); - - assert!(!is_custom); - - std::mem::forget(app); -} diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx deleted file mode 100644 index 64f8ed73a7f..00000000000 --- a/apps/desktop/src/app.tsx +++ /dev/null @@ -1,408 +0,0 @@ -import { Route, Router, useCurrentMatches } from "@solidjs/router"; -import { - focusManager, - QueryClient, - QueryClientProvider, -} from "@tanstack/solid-query"; -import { - getCurrentWebviewWindow, - type WebviewWindow, -} from "@tauri-apps/api/webviewWindow"; -import { message } from "@tauri-apps/plugin-dialog"; -import { - createEffect, - createSignal, - lazy, - onCleanup, - onMount, - Suspense, -} from "solid-js"; -import { Toaster } from "solid-toast"; - -import "@cap/ui-solid/main.css"; -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 NewMainPage = lazy(() => import("./routes/(window-chrome)/new-main")); -const SettingsGeneralPage = lazy( - () => import("./routes/(window-chrome)/settings/general"), -); -const SettingsQualityPage = lazy( - () => import("./routes/(window-chrome)/settings/quality"), -); -const SettingsRecordingsPage = lazy( - () => import("./routes/(window-chrome)/settings/recordings"), -); -const SettingsTranscriptionPage = lazy( - () => import("./routes/(window-chrome)/settings/transcription"), -); -const SettingsScreenshotsPage = lazy( - () => import("./routes/(window-chrome)/settings/screenshots"), -); -const SettingsAutomationsPage = lazy( - () => import("./routes/(window-chrome)/settings/automations"), -); -const SettingsHotkeysPage = lazy( - () => import("./routes/(window-chrome)/settings/hotkeys"), -); -const SettingsCliPage = lazy( - () => import("./routes/(window-chrome)/settings/cli"), -); -const SettingsChangelogPage = lazy( - () => import("./routes/(window-chrome)/settings/changelog"), -); -const SettingsFeedbackPage = lazy( - () => import("./routes/(window-chrome)/settings/feedback"), -); -const SettingsExperimentalPage = lazy( - () => import("./routes/(window-chrome)/settings/experimental"), -); -const SettingsLicensePage = lazy( - () => import("./routes/(window-chrome)/settings/license"), -); -const SettingsIntegrationsPage = lazy( - () => import("./routes/(window-chrome)/settings/integrations"), -); -const SettingsS3ConfigPage = lazy( - () => import("./routes/(window-chrome)/settings/integrations/s3-config"), -); -const SettingsGoogleDriveConfigPage = lazy( - () => - import( - "./routes/(window-chrome)/settings/integrations/google-drive-config" - ), -); -const OnboardingPage = lazy( - () => import("./routes/(window-chrome)/onboarding"), -); -const UpgradePage = lazy(() => import("./routes/(window-chrome)/upgrade")); -const UpdatePage = lazy(() => import("./routes/(window-chrome)/update")); -const CameraPage = lazy(() => import("./routes/camera")); -const CaptureAreaPage = lazy(() => import("./routes/capture-area")); -const DebugPage = lazy(() => import("./routes/debug")); -const EditorPage = lazy(() => import("./routes/editor")); -const InProgressRecordingPage = lazy( - () => import("./routes/in-progress-recording"), -); -const ModeSelectPage = lazy(() => import("./routes/mode-select")); -const NotificationsPage = lazy(() => import("./routes/notifications")); -const RecordingsOverlayPage = lazy(() => import("./routes/recordings-overlay")); -const ScreenshotEditorPage = lazy(() => import("./routes/screenshot-editor")); -const TargetSelectOverlayPage = lazy( - () => import("./routes/target-select-overlay"), -); -const WindowCaptureOccluderPage = lazy( - () => import("./routes/window-capture-occluder"), -); -const TeleprompterPage = lazy(() => import("./routes/teleprompter")); - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - mutations: { - onError: (e) => { - message(`Error\n${e}`); - }, - }, - }, -}); - -export default function App() { - return ( - - - - - - ); -} - -function Inner() { - const currentWindow = getCurrentWebviewWindow(); - createThemeListener(currentWindow); - createHiddenWindowQueryPause(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); - }); - prewarmFontCaches(); - }); - - return ( - <> - - - { - const matches = useCurrentMatches(); - - onMount(() => { - let autoShow = true; - for (const match of matches()) { - if (match.route.info?.AUTO_SHOW_WINDOW === false) - autoShow = false; - } - - if ( - location.pathname === "/" || - location.pathname === "/camera" || - location.pathname === "/target-select-overlay" || - location.pathname === "/window-capture-occluder" - ) { - return; - } - - if (autoShow) { - void currentWindow.show(); - void currentWindow.setFocus(); - } else { - // The route reveals itself after its first themed paint to - // avoid a load flash. Safety net so the window is never left - // hidden if that self-reveal never runs (e.g. chunk load error). - setTimeout(() => void currentWindow.show(), 2000); - } - }); - - return {props.children}; - }} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -// 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 -// font caches instead. -function prewarmFontCaches() { - const warm = () => { - try { - const canvas = document.createElement("canvas"); - canvas.width = 32; - canvas.height = 32; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - ctx.font = "16px 'Geist Sans'"; - ctx.fillText("Ag", 0, 24); - ctx.font = "16px system-ui"; - ctx.fillText("😀", 0, 24); - } catch {} - }; - - if ("requestIdleCallback" in window) requestIdleCallback(warm); - else setTimeout(warm, 250); -} - -function createHiddenWindowQueryPause(currentWindow: WebviewWindow) { - if (currentWindow.label !== "main") return; - - let focusGeneration = 0; - - const unlisteners = [ - currentWindow.listen("main-window-hidden", () => { - focusManager.setFocused(false); - }), - currentWindow.onFocusChanged((event) => { - focusGeneration += 1; - if (event.payload) { - focusManager.setFocused(undefined); - return; - } - - const generation = focusGeneration; - void currentWindow.isVisible().then((visible) => { - if (visible || generation !== focusGeneration) return; - focusManager.setFocused(false); - }); - }), - ]; - - onCleanup(() => { - for (const unlisten of unlisteners) void unlisten.then((fn) => fn()); - }); -} - -function createThemeListener(currentWindow: WebviewWindow) { - const [appTheme, setAppTheme] = createSignal(); - let disposed = false; - let stopSettingsListening: (() => void) | undefined; - let stopThemeListening: (() => void) | undefined; - - createEffect(() => { - update(appTheme()); - }); - - onMount(() => { - void generalSettingsStore - .get() - .then((settings) => { - if (!disposed) setAppTheme(settings?.theme ?? null); - }) - .catch((error) => - console.error("Failed to load general settings:", error), - ); - - void generalSettingsStore - .listen((settings) => { - setAppTheme(settings?.theme ?? null); - }) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - stopSettingsListening = unlisten; - }) - .catch((error) => - console.error("Failed to listen to general settings:", error), - ); - - void currentWindow - .onThemeChanged(() => update(appTheme())) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - stopThemeListening = unlisten; - }) - .catch((error) => - console.error("Failed to listen to window theme changes:", error), - ); - }); - - onCleanup(() => { - disposed = true; - stopSettingsListening?.(); - stopThemeListening?.(); - }); - - function update(appTheme: AppTheme | null | undefined) { - if (location.pathname === "/camera") return; - - if (appTheme === undefined || appTheme === null) return; - - const isDark = - appTheme === "dark" || - (appTheme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - - try { - if (appTheme === "system") { - localStorage.removeItem("cap-theme"); - } else { - localStorage.setItem("cap-theme", appTheme); - } - } catch {} - - commands.setTheme(appTheme).then(() => { - document.documentElement.classList.toggle("dark", isDark); - }); - } -} diff --git a/apps/desktop/src/assets/illustrations/cloud-1.png b/apps/desktop/src/assets/illustrations/cloud-1.png deleted file mode 100644 index 6a8b14ff6b2c56131ba1d1435ed1da20abd54181..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467102 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 z 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 z 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

F+S&`vwYDqNcxGtKj8 z^&$eO0Av&9j0nYSY&9+SfC;1KEveWeCewtK>gGbNO$9>mBQOhO=^i{bfEoFeF-Pvz ze&a&F0O*oyZhF&MkIrHQ&Kiq0Md2pUpjXjDy z50(?#rGN^Ocb`X1mSgqH{!CT_@_pbNHqH@PzbG3oXL7O)Z2srHW*V7w9WZzXWKq5j z^>KK^hb-8+%0O}xk{nxHiRd6G0Qt~qeh1~-Cd$`9STZiCVC+i8Zmw+`b3NFj_Vu^F zga0cEQ*YB6i6|fO+@!fWu7Sj#?Wt*TEG7V0&x!S&$+pVz!q_@cXg>m(;l?`uO?*hU z$nn5UE%QA%cUD|{4!*qaxJNBwkV*Kk2J`@V{wf1Z7LkEdrrGMa0G)OX+ z-hg>gX0wV}QE{QO-kX#B4)AoA&sk}c1<=_nPgq305`e-4wZsYGl?)&&q>~9xEY*@j~`rjnz}n1C73YbPvyD&&Sui>jdpY&fX{z; zV4Io%QMvhHds&?>4o8RpUEEP}>$3+~g_9s;MKXDA{00iiL?hS0$&2`Um?9hyEq?)n zfFOVfVqounC`&2V7~ZC}?P)sDW*g_^p4?QNYcvMz zGp6C@`kZaHv^>3C??e!aJf5!I=gzfdL%PIX^$5Rta`WP4&P{(0US=lLGUiq>!R{#ia_DN%!NZ+`iFUYm_9+@Y&ZxfJ#Zf?7A7K|#0s z;o-3hC@DayI_m7K&a&bL5dK=;>Yn-N`adl|1MJ$4kNzLPD=ElaAkY@>mJMR}t2Joa zk9~~ohJCx!ksGjmQ4Ln#;9q2eBaW{Tq)*Aa)ht(hpKDUPnI31-?0``+JzqKSBj-%; zHiGYC*nwFdrI!b0!TcNj{EU1wNl3w;To~n`4v&3fN0TPG88|n~;X+->zxYdF-*v4^ z7XPjA=cvPPLtQ8zCs;N;Y*(Kfq3-~E z6Na3cOFWEuC4MIla{|vX+l9mqqT)Kfh4n^LTfWvrzNRTPM5bq*g~GOc#l3A0+hi=Q z3;{4Kv;|?Ixy1nki8+UJ-r=*toVK>(0HK?dq)?fW%Ap%3z*(Qu6^Y!mER|4@mMU?% zZ(^6Za2z+Vo}fZFelEU~&)9fJ6O9DQXeNv-c*wSFj9tdI=*r%K=`z%z&R4zuPGcXT zZq!$;%(1@Kq3O%|X3l8>&=DNjRF>`Yg;>d{^YH%-Mi|DNlfHlOod$$6?@ad&kf*L` z907ZhX>R`Sb@l*z{^P>sQ3OA4P`la1+Gz`oQJ~JB3o!Xq*9#bGKu+uU9SN^%lN)#l zZQp}+@udwemp=GY!vez9*9JVjtJnc6Y8w%dIXf*CX#>RIHI}P8$L1o`HfE;fL29gQ zB29yqHc6S+Md6zWtC^?8`Y=;ZJT|JWXmS(~2?}xn1P$OjTN!=s&iw26$Hx%BkOwhi z*6i7W4qW9AiY*0vchLr?setDO@?z1C1+pyaq~BgT&};yRnF^By9{a%(2ZyS$Eyp7; zHItf^*=C|3(OgD4k<TGqDtj-bzA6um@_Sg2v9|c>}}J=12cmrz~HGu zXV#%G*=q1dO`a+4`;}x2&CF^%colj@B(a)MIDKQv`Dl8n-5ByYM4(oNydaGlu zTMuf=Gtv~C2wx3AT>=VOnX@n6sVE`9rzkn-+H#t&Ov;LAN{(d}FpF1Wnpw=)zKQup z^X*3<=hoY~FoJDg%QnPvAj5REF=??)5Ddii4JHQPGo)-=z&cKtV8Yl!o_w7Eb7iLR zy$syPyRV`Iv=g(p;>P0M>Y>WCSs_9m!~|N)FY+yBCKD61CcB_wJC%pUMCr2qA(V*$ zCgHvH{{iqkm^^(%uam;7(x0V zC&g>!=VLP;_fo&_!MGtq;1Kf=2Q~aVu!DG*roK63G`rPCJ2$73M4ln27B&sv=K>qi z`Z!Dj(){TJ8*ywdFxkS%9kdfRuEEZ3O$=pnu-eLaP%(e{*ddPZwrsLb(f&uD=ca^< zp<>@usN)w1^{!xgRX%#ZsOxnPe2~#>g;Vw=ct3NS;iO|*lno=(b#oOKd{eq`Enup6 z$Q;!3J{QIX^{@j6-xy;92h=$*HJK+_0Gp8LFfBkYdk6J!&Xf{7ux+?dX36#CAeTGf zN7OoQk-}*AB6VxwCD0FJ+|q3I@bL>QswyKDp2H{XH% ze=#4&@2BZEnznMSW9fS*1t#KnST*}?eTT6>W@Wlh9QW(P>zR2J7s&fOl{@^$aWt>^ zT<7H`=OQ2@17mVQvioInjbNW6a*)`|(EkSv4`nU)=M_BZoY+r~fJ+d&2}v3FH%Zybx?DAtuL32?8^ zi1Jni4g;JxSIIjqpPO@XFE8wQxmH>UXw3Bj!3hyei}@v4?}-_9aegm~L*@Y8znhj~ z2s}|aQGA}vWc(D6XNh@nhHZ@2z(+AH+;C1l>HEr+QIu-^9F49Rqq+q@}7W}D<20$G>oyOStRPjS&8oe|Y&moiNMI6O9 z0H-3ZiDO$#%<-mXAgbo2XgHspW(lC@FuUpjJ7Ad&cEs%_=-lzUnPK1pjkL{xRypXu z2fmp`Z+nlSK!OBeeOzd#nwn2|77GIf7Fm~U)3=H=&2|95xx$Cm4aXNVb3s5Apq8?S zg#IrkLI7sPCfOWx2{d>GF!JC)v}-TRJEHgby?I)pL+Wk*W(N~=^g`iZ-v*Eij_NhbNz?8;;HRma|$H_o_5}zP8FZB(juW8(b;S|Qukg}Rw zfAKo!9*KvvEfwUMKp(N4!?S@FyS!7uYX_0sk$n#C__aXeI$+dVa7LY&hE$+G@ed-; z=o}M(&@@@=>|OVZe~SM7+yQoU*LFP7d*_A>gd!AN>aD4Ht0x z{?8d??(>Mj?{yYC|NHurW#c9dWzBtLCVrS_7yK*O=k4+UyJC>(45bxnp)cG&QZs{G zBS?RDC5ejWV^P?17vxLc2?jCNZ6XBTU^l5!sr1j&SpWP*T`Xa#``A%Yz;QE4`33-m zd(VBq-?Fm@KpUNlC< zFN1A7y)AH4ihN9|3$1IpH#QY$Y%$rqC1zo!0i3!yb%FCC_TDdczp3 z*SuXB8-yEl0SgWMrMB@BfwlTKL)7tx^@oVM^Rd9zivko~OdHwR2>F;4u)x*FN$`$s zKuyZd4cPd?k_t?}g}{?5RQNb<%)WrNts47G&zU(6=7=y>{D9%zC_fPz3c%!`Xy8B{ z%t&pk%f?E8&~+b7A<0_JfLAhWxGoh_MF*cZW~#K;)$ccOK>UxO-lXgmWbt-mOk+_b zk#UA~=nO*pCi6$9&dA+&&yqgBaBOU`g>}p&@Z&R#u|?3p1f=+`Ys<5&?UjYKWjtb8 zoW^WLI+J-}dgeQ<=K*o#tW#(Hvo87?33D*V$ctF8v9@>6FIcLWC;gMYuQmnmsG}=Q z`UXIVwhi1<>EWF33nu(L5A{)15waznn;=uX(EOz_38nqn-ek)X3YJ{9jpr?LJbwdI(tHRln)_rMdWioXAkVKlbfGJNlUx0MEP&^m z#+Z5Kz=TfBbu>4kdXs{v;iX*YW%l| zX96vH+vr;0pzcmxrvWgQ27_s5xDdqO5p2v8N7yIxB-c(6!r%I2&HMr^m=9WJ3lgM_ z-2fJg1`ZZW6>TBZ(&Q#_0JIr!9Dbv@MVN5AH6ZvIEFKqvaWm{-&cvd%f&n}JK3Q-F z=2x=5r&|9Af#6vk9Lu6LF%_~ijbjhyXCQgjY+{QIZw-OZofT*dr)F+;))xHU@LA@4 z@tkPRs_@f=yFBNtDhF^}qgq(aOmDSFM$AlE0hZrDkR|OO%o}KS@*M$-cnW|eLSX^6 zTYcMx&wy=il(j7CK02coEzxOp!1>Iy+D%v~tWEZb^Cyk7&R!dcm{$VeCuAD1zu_|GVLQ;^D@^)%c+UHUEo1n~}E8?fxcHb{mZ z$AvQijO<%EZVD?#+NHBn+)ty=WyrUJ2&`jyFW3YI1o--09F7mpg)-k9IKP-*DryC6 z9w}$!8}(*>M`(&uz8Pr*fGhSn))$TCPZmaCI&Vqa?e%a6NHB%Ty10(&kpg^Sq(X@g z{g}_XLfK?m*1xD!Qt^=aF7~6D_%t&gDlUVeYXi1f2lG(Q+J8j>5{gpFCR^gS?P4Y{ zu@0Run}s#2DuOI3Qwc*1N<0eO5nLTIUQtPy*Vw)eG39<3>lzBEySG)uqx~OJR;DmE zDbpax)AD|=xS`ZebIzN9l9-mA!um8Ny~XjP$%yZiS=aAfKq0bzxQ2df`C^+oz=%pr zYLADK_ygqmwT4SWL`Mlb-#0<%#{y2?K|l(W)-|a(i8l7c=jyw>zPN^&EE>gJn+xPxoWb|l zhL2LmamvF4*+(Y3^%no9+XW4w5z@-F`otjQ-|$&5`4h|^V;I5b1$8> zBmMZ9=KHu1HVu7#JI4YM9G+ObsNjto!`#%D*N<(EBbJMU$`#y?N+6_{0234eun{p6 zE#$!TIAIT=c06VEy*C=+@#D(QWu-df6ZA~M9wccxt5?PBR0OPXs-M@viT10=!}DbTc1{^V8=N2eHi5m1*SjTF|ali(mTg4*FhXf{owEdrZ)1 zZ6P&p8u?vgdHD0w7(^)`Ep5>1+gU~)CZ2Oe2&=6M(dsl;61HU^eBk6#3ksDvSYX3!1+gg7f?By8#c z7;e{{-C&Ak+I|V_(7AjuAh=&)-=LopeizGuG^30+Xj3I40Wc~+_vp_7{8@aj1I(_(SZRrT6TnLIIVC{N zNmdx-gd+gOOu6Jitj2OCH(xh7&0$#Vm`$_{-miUv5npk zT7U^a za7xBN`t~7yh(;EBaE)vHS4Y?^GcF#$);PH6>wbAKDL%K~*9L^151GL)|nsZGNINPRfB+fx?Arkod zP+_AvmQ)S7AfV63X4DuQU~w}sB3wjzuKk9*tCn^G!Uck+Z5vK8nHieq^mZmrY?nA3 z7U6qft3MF*@Vl)I29C(B9HyLC=k@aact+YD#!WtJ0&Ht9Y@-zPI+m@OGw zBA-SmTV+(SeTaXd%yh4_OqMEwaPQ;ay3LFffem4Sse&!rytO$gUmsU|9lHgbQ6@3- z$8sQn+{{zE@vYzrr@vD_vVLb?9tG16lHU-r&`SuZje1v#Gt_@HGP?wJV6%x>zQ z04&VOq2D1dwPpm91nn4pFac88xFc&mb1AT84ZCc96DHQ?c+d8SXYMpziStxkKKgse zdW5Buqcf=BSBIwAo@D+rHr4Em8Uszh_*-gDyY3_F%f~5a^L4o|KxXslM>*3%UG0jh zVEB*?K0uydTi}3oD^q8$Z6Nrj;%gI(9_Vmn);!Zpk~k2fu;{IKYT6@~$r5%g>l~jU zi-X`#rAc52;1bg1M4D^D`{vm%Zy}%F4t+lZ)6=&W1v4NWx+FN}L8x_E|!G`1x?V;4a$r3XK8+AR!WMXIV^y6D& z{Z+jhoxQHPCj3SE#g$r4U^|M5H+-)#@R%p|r(B@pYzPUaX(n2E!Xn8W@9X<>>h#Ef zBKL#A>-HW?!+%{HHV1s15hkWKB9P3llcMj!N;#gQxfz&2a-pTvqkt6du$0G%4+(KF z3ZO57CU`h0TRFndbAT5Au z=KsvWBd!r7its%KOou3IHDR%?mH7M_^xYvqF@k`B3IJ5_d1R-)?L^6Bl2yfVmg$Nq zGci#PzjgpUlJS`B(inNdlnz+^!rF2rf+gur2#}Y|kqh^*TElO4c&kyul z15FM*KZ3c`l^fjO>!jIm*^dslSio_@*&O(k?H^I6Xb(7QvXw+9d70rEV;&l1q_ZvRgpUIS1GyX*WjzzzyNDNH38#KC}Qbw$@; z#4+uFE+W;Pxg(C?x#6Fer{_zV54=8=f2aB#pqQQyHctx;+@SET;uK$9pb^8KYNaij(xi@0 zu&=JIZh5}@)*1#@lgZQWyUO@CjWH)BAwCbkTeuxGZEt_)%G${nK zJW0K&fHW|#I_$o{zlAb&InIdyPTud@&w*(WfGmA1^EsUxL>vpq1K|k9i`-cT&m??c$1(=BfLhh%_L14yU zoK}FxKyCx|d_K+1$0Y&>0bhiv;(~vcCC};J$J5zb7apkjJ)(Ka+pq&=&gz+F@=yQ) zH4h1;B$?0ZJ`o5qU$&MESX!~Ag>0VjwpP*V;)HVL5)~C5D|Chp+qN)|P64i-Y~D{8!aWAQs;rJq`r0sAlK3^cS47#! z*e75HuZz+XeIv0v4);&vAh_(4pUA}Lm;(9PpowwZXKjAd>I?gsw?Q}i2Ge0%WTg%{F1q)32tj+%fmeYW=;=u9m0rEV;&&A-Gf(*YDi|1oAS#_<*&HSkIFSyPD zk^|*i>(Gq>Py`~IGeGdFmz-0uKSE+ATx_Fz6tliAO?&_4BzGx=acv3K)$mGhg{P#%zbX;L7odixgOl~n`l`pYYqatkPYf{U@|^g+u=N= z&+$7D1ROsCK{WimK+9SMVJvG0B?MT~JzTB=WS!UFke0=I(UM< zWa+MS9z=a|?xj_ejbj~8geBHwS-7TtuY3(_;|I&n+yD?15p#F4mt#M6#v$v9eO}t? z;uwzhby9{<|G%{NQm%Nmxat1|j4HHKFhX>$@_Ix5jp4G%qG$c7IcEgOQ--m2q+B+Ek$Apg03m)OXh4OBY0xN6rcPoh<$0$85& z&0?^y@cVsX;u?@{{_aGikbNVH@JE0X(Fpa;i*Mw_q>0ef#WaU3y!+y%+R={<gW+kwwES*c|7qs1)Rp@=_q% z(oTH92>79ZlFv8J`eYqC>&EYK;KTuvK#0K&8^@y^v_BELt7(!Y<)i?W`2`BwCC_`T zAX?(6YZ2%Z2yh@|3R?i*%(2eFRiwF?XPD?*=0hN4%{g<>g-vzdO>(iH%Ldql>q|{q zAn;pJ-VCgdX+Sjcr@oymZ~0j2?V5F#uqlIgD6=&&BNu=?TAqB5DrEhHkST;X0F$yu z#fd*c!a#N*H|tC8(R|=Pz?p13HS^TSNAkWYrh6BFAGJ&nu%hss*1>cj+sj12CEOpa z0a=edT*w}vDQ4@N0qCNA*~SD2=gkM>76p_k^Plw>js=Tml>!g|0FB8L{1`HnW&3h| zgD}}!oyk}TWd;KSz$QUC;vUw2&XzkNW10a=*4DCQ6u)($wzLCNUU_rVe(duOx=>(F z0lu-0F?Ipy!T8gHu<}kYmQ*1JW8_tsMhDnE3%-5cI-^IdAM9Wro{^P^eC_@ue#Z{> z{a%(LH`$`xuqZp>SWX5_>$exovv*ipy0>S%=00_j`dWxw)*41Et5g_(tUpA#K zw42iyU_Cs1@H{}CpED47e1Qgi5K1Tt!9xRhzWW;gT=AQVA3=d5W9Dd@@>fjsEdbDI z14jwsf2iLd>;Lx^KlctOgV+WI@u{0~3P2t%*l6C(wY~EV+eSLN5jA0_067|4S>GuT z5U^*fon8v15#n-j>M!Fms91-6NX_!)u0TW-8np%ELU}SSoI=`o52WuF_?`Dqge?xD39$2!1DlkewF~> z;#~XMRK%y%IrGNMu|MS=Q4`1V-3gG!rg9fz#`UEuLi^mq3t3`qkKNg_#L;e2@ zHV=g<8=dt+!4Ld$)OEKfEfbd6)*XRncHk)51sJA*+~SEcBD-gST6uDy6X(aNskTj2 z+!-HkyWBRwIOl=|7i`=c!1LBXpU>@_EIKQ~25AEw*KZW$xu7p$W2qZg+QGfFbNxPQ zvfO&Vu4@OFIWNtm@^ECO4@DYG!7EbBFl>h&Usk zGmLZ?90zAGx(274aFjFxrcT8`vBCOwVD~=;LjoW)Wd^ZsCXNxL?~9T^2IhGi7XP70 zPxf~LR6{WMB=^ahxc-Z;3}ZF+e2W>ZLjZkUzwkQeK-{Zq6U#TsK|w#cU)E*CwpY-> zfky9>ZdR0<;1qt-&ke56fN@FSg7w9{#*Dynu9Xo`&lGG9VeBklQ)C9nGkY5l#ohjVS*LJ7JupLywMt`W)Op$4 zU)V;Bk;Szg+a&U>U^y*=V5}NU2Vm0RJpjrI^lONZSx+#9=KM*va9W|Hkg#Xo(G|NJ%nL@8WFn zOZR~!<^s$I(a|ElDcf+W4-WrE_hIClLQY0+Fjw~lY_Yd??cUp`2xQ+H5PARYGI`wb zyg91oLWbtd#Kggk^;+j&3grG!|6c)C-&(rG;(=4~ia0(*b90J61AA^83x)lzMHTuc zenjNFvL=G`6Gp^Nd=TKgYMO~g)mkk47H zZ{IBQ9N#aRiUx4*P@uXJfY7wlh2aT6!P!?#TLf#WT^$Grk%s$(GUXYh>zdkb)@K^{ zXTT!IJl{;Y(`(6}nq$N_B0cjB=B9&~9_wF)NDdOeQLw@GgtSFV9fBeq*IWymc{DK2 zV#2_2?Zfc;v>UfzN5awbgOfSrYVG!G*H?HPv7c^;+% zxoqfDHs_z~n9 zJ-mNszUwGxc6KelMa;$e_pokUsq~_G@o{ZC%aHfEvRSZ~P$8jDAi z3XnHpppY5jd$(<4i;VfkS|BKLN*fvm5Qr}uRl1P%+C&~?45{-lt!GNXs2e-JVvMu) zu+=oP`-5HrG<7!21pvz8GqNtl8qnoUvqLi81J)j8fzk6VG2!#tLTBDiVDk z$(+2*AC;J0Ux;Uu&3%UK?TIWO{ztx439;4t7KJIA$2-YHt?T3Af9K~3^HBs6sw0+= z`d>=moUnK<&2Pm~zn==+`KPb(e|(Mq{cHSBB|HVd;erYbpl=OOI62Xg3J>6jX%4d` zT4{f7@A+Ku3kC!gV4>r`Rjl)}K?_2v*@pA3rSFpp7gl$G8U#pKH&14wjvEgtGY-Ct zytZ+y?N(Ss3WAXij!EO-q}|Hn-zt7-<#XqxSmM}w4|#{8w(Ka z{NCd3K6mQ(wc-t+_h*hlSkzuCw&#?EcmbXFGys+k32Bd*ZJy8e$a_R926l2~K_y@i zVyY3mf694|0X)x5n2$G1!^c*XnT$JZv(A1pU?FtEy%1$y2*7hb@8(=~+Nb54R?toa z7!d%)?KQ8UEhAkQ+~69t4Kn8oq0E9IQ%8*dfQwpo5d zzNC*yUv@Ceqo={(^9=7}j6$wFl$cdxo3VTBeeCbbD)Q?qR0uS-LXdAOGGOiy?+ZdtL=B{{<(Svh`ybsUSf4l=eC;1(d^(A?{Fwe5b z7DXIyv!Zbg!LeBR;QZ`nk`Do^UD#d`bH?FJYG-hW`8I8%NLSh~m?aAExX<2@mB~E@ z6$t3hgLS>I7o&2)=G-G76Z5>o$hw`bfa@|1GAP872Y)$Ml+vtQ(BXSf*4$fGzjr2- znYA)E1L*IRyWPmpmib&6&wbA7YSxd1spxw`=F%HeGpA8y2}QR zCn9fR7y=ZX9EW;3?0XNG;lbqj{~KyhfWY2E2}R*U=eZa=fB!Z9Los;XVFAfOou=Bh zXdcjhDrvSSuD?|rMC}_Wr7V-qa)JQ(rH+B-SCoya039RM3irv0f*-sQ0TAl=G z>67UN5yBVQM2LB$lxdJ|{+X_UNGkND{oWFWfa&u?fjmF9$@HnQzz#hBuNCXQ-%udE zwFkF1y8t+D_UDRb%bDgNFWB&MJR)0fa)V7)A~i*!fh6OAfXD8belW)N%MqlFBZv?k zTH}GBUl}>}9v$O+bG4FuBYkeL`*xla z6EDhQGG-BvB?4fJ01G$Y=m5`R+mP{I$82tJspV`yDI!8k59dTC&YL=etQp>i!ICc=XtT}d= z*Rb>g?Z8iu&Sg^+VmJ<;;^%DQ`eeHkP-jxvN|oX&$kFg6y;9JJe0%mT`y3fkgT7hvLGL0Ldtr7vJHpPd41 z4g#MT6K5z`pXP_fH&pJ!z_hypA2TYX5+K#m+W`=hm|$2bJH)^;xn^M-fIxx2)T`z* z%|`}gySXFgH#8fZXzQlaT#z4{P-R+fHZS)FH4%|6 z)>sGu@H$e^&j2I=G8_y+;3kdFIpFIc5AKTq0@HAny45t*bP|HE-&`=(9TCgDD`ehm z6ZZLdkvqNj{^SfE6C|)M1TBe+L^BM}DrSV67oE&E-3ivylirZa)6_=ZJraEXFM6Rtx;9UxDn zbTbH$nNLozKv+LhSfs}08HjaWOg0KAh4ZKRU$OOJpj@dY0-0X|av{LOcJc%*d@~KV z1kByC$AiFDEE*uFklz_FKDN3nVu<7Q!+{59nV2t4f?{HxZyVE^OgX9R1AcRmH|8mIdGtQuzz_3P!P%3ev)e?;Zc!U5E{sXcc=n!$s+(hq3#H#`g}z| zR+vyIKLF0?Poe-mDFbUV*GeBs_?2$pS8L;5tYvgPG1>p{%`S_`!^W6y$_s~qXVUhQ zFdrt~9w1K`T!X7Sp~E8KCY1D9!u=2MQS+{R^L$hPuOissVs~M4&jkR@2`vsZ&8*Vr zilCwbaDnqK3{cca{ZK5WR|s}D5qP1zf}-e}k-3(f3rz!I1kar<5^$i|JJMH=7Oyx5 zAk8-~BAl~@(heL|yFie^fuGIEM$8f1i;?p$1>oG_$LFcBMRv8%@_kg3QfI0B#sU}j z94pW^MYyiEzYmyX1He=1x3X`5@dSW*r4I?AMfdy+VexFYaqg6TS){5E)5_XnumMza zW(M2^ea#)wyAW{;Q!8NpDHt&QdtD1U_OC;Yr!cUb1HT1Zo`G|EH|Z6C8%$%Hrk0dH z(iTB3%oGUoVp-m$ZlwPRpo#BSV90$5ZTYq-8_N-z4&i(@0%i*CbwM}t;DYSjER6$I z&dSke90(f$m?dekJ^5NKU|~RL=nMfWT8W8{jVLBN-5jAY*LWU6k6{ee5X@m1-nO0X zVdH1c#BnB;jQe1gMZV%jYF^pqEC-Vu95lczn_GO0!gUG$-3Dyt0FEnXSUE6?z)Qy) z8BY{^l;=f59{VW(JR-m|5c^2{!q_?Ur=Wm~vnK`x-jPC$E1oc4T(c|zvX-$?$ZU~1 zq|kJWkQ!?f^(#6g4}rPFCZk{*;@+CRifQJ_*U?ja-3?n0iFW)j%L(GYVpEVFn4=tA zyU7}88+eW}K-no$Pgh)9#u_4XiThb|h>v^ZyM%dd4Cj(*i9ez{iYKGy`T}7S7K=$q7MBt9v`++L8bn z>-P?mYNs%=m-yRSD08se0%o|(y#~OGVl2vDnLLx{1F(4tSek_W0C*lCPZ%75wuJ+l;Znh?j>OfG_vC%v^!5esuIye6+1hGzMu({BOZB5QCS!_pIa!AL%Z~L*2g_4%q&qXvx zu+tXT)TC4)51;XC+ca|xqziy=-0$O{Y;t6?V4LjRXGQ`V8c2kNKkYD2sT*hGxb~QX zAl|Q5!XBV-fZ(#0FZ|Pqq#T z{G3Qyrgg|R;zAAxUWS9&?rRFZ4Ac=n=Fvlx6>u->)Y(sg#`N@SZaRwZOWAZb#s&b@ zmbw&{p0nq@opG#m5W`saiLW=lJ5Xl8_j-t!w|?IJ)j3OWm{3JolP#2L5S^XT7hYfKxz)OVDB`!vCtl!pR(zP9#r#xSK{PhpTAoNKW!*kZwB zGhb$wl-I>S5XuPOM|0wJZ!#xKo;T+Y2%}+DA!2Cpzxy9_Vf5xcEf^~%)8camGMXh8 zvK!2Qpsz)pWg@^Af-@DF&$gbx-nZTX#+)cHfFTE^RP5DFVHf2o>oN9yH^ap!`&b*J zF9NzrzY+OU{b!wGk1+`(Q3=xXTH}rTBE9%J&%sr)nuI+D-$1g~fzM)mg8_oH-ZbcY zN0cB~--~G?jTW#jHP9D9fkXG>J$Wzl`Y_G*0C|4)z=b_%*<@q9HX-Rt19;xox!Laf z=4bW1v{M8Q?Kf|&8=equx+Ws7%0qDHJ#2e}Fw*soZDbVUY zI`5(nBWh!aQ(g%Rz_59bn7!jbdk9zsfQKfu9N;lT-b^xyO->O|>$h$< z6-EKPYfOl?*uY}@+8p{1yyp(eEAsu@3fyF&UbqRM6WHg*lv?fn8%!*2+1y>kw8oWw zZ|KhlW%H$907~KTJz!5em9qDOd4Y^`MmAQE1di78ETEoebC}GA&0Ye?7x^tFv0#r z)U|?uTG}rHq3Q{RCdQS5e)9d$tR$f3<~CU#smVXqXTTm!81UQPnXu?B|H7)0`7mcV zQKL5j@B@H3G7n;XIuSTJ)6b97xyrzrz)`%^Apil4?Gb?O48194br1Qy5ajW3)Xg0R z0ID#U5Y3#OuV3zB;$8B=aoX8P{4QA!9D$D@f+Yt{2?5klzz8yuOMeb!B=d-xeXHO; zkpzS@pM}nrneo`R|3 zizw!8W21|royrh7m)N|OmCsK%&GrC<9w3hf0243>udLLOxY| zt@JuraHZ3*inJ}<*)~6x|B@dXxO1=gg$xP`-T=sa>89T>NF1P?zY@&Cz;y2*4>h*P zf)b~iksyzo%TTB^5XF=UK!-YpK>OLgf#46K+O})FzS{XHw(UYZn!?$`K!|Rt39AD7Z$Y~n=UQ3@k}<- z2mqK*+$U!1a9%XVZLwk6JiJd9t~l?es?0$+`Yo}*tRUAfJr7Aam=4PC>Nm}=3V(l1&gKXTUY{=RwK?Ay={rd}r>&hnbB%!FyEc zOXj_-Tv!)b7@n>xU8Q-GZ)4wfr`qnLi}OrgMg%VWmutgmYelny5aeg#dC#l+_INUI zN`&Ey&j{$E(HJaN78I@5_Ya#p$t>%2k9KJaDP%~I{a<~j-cc4B?wZH0?*F{@VsHrxf^K1fvnE|*!}r50BQdv4|T1M-@zBpB;6Zk zF`#K$Q5N|Dz>_;5&CMW8_1RDnOq?6y6z!dh(<)tqP6sph_#CMKZVbO!A` zb6|E5=%t%2?9U73A5>?U=J}fq?qnPriY;X{*&YG#P&1htRP3jR_#*?`fOO}&-?c=} z7T+KQ!>uv^N7<)|e zDH_xQ1d2AaY;2gu;IlpATU09-$8jF<{RyLHnzpixf9|ubBFkB9&y&e;P?3Xy->nQ1 z*DmBYFRXD-)7DIE#shp26P{|ehK`C|3qLp4GyI(Ugv-ZEH$RAI?_UYP?ZLm%zt784 z!DR#i?c`lP?j_%nn21cu#hj@J-=KmS#EA73e7i-0O^5md>i_=_d*{02$dP1ex(CV3 ztSU*}GrRBqg!^NsTdE5g?%N9(1j7e>z`<0Hx+SFp4wAvRctrTsj-6xZ!yN2(i8-9t z+fyz}H=P~ev5qwEtIIiV8oH%C_RO|0;I@DmT+0mwt!tlj&ckt4WP-G85)l35Iglx5 zHjM>`?Fy&}>)#wJwzHqRz199K@BmjD;WlIsrQ2Us;jkNbYd@H4$%w$3Mwd(^|NvZZ9X^f=H#FaFz727Kfe@1X!Qjv zg0z1upyvZ)&7jd<;r0JQ1`mzE%t2t*kuF*yine?GR^~l%u;soSpq#}Mc=(w#+oBv3 zCki-zB(zD=(#2?v37^Q)K-LQs=LtE$Tlb_vJE`sPKKR2XmtZlY8wXZAa}1^!_yKCO zok~FLvvOlXV$$Y}yTvS?FJIe#tuBi35dDfQVwBAy(s)QN6@#o2X9RHg8xvFu`g(TY z9;{CBE;6vffnAAZVZ@yxSk_zuYnrj2h8&T42@+JUg znVdoBD+Y2z^a9h7-- z{cp~)78_dXE&zJihQR9M8d4Z}{Ww1VL&gZm)oH)B`CY)BOAnuc{D93&88d9hP)WKr zo0mDnPL;jm`meq-oCMUDSiV&manpsJWXak6i?%>@u5%XElyNX;_tf@xDB~UEk%R%i z8eC)Syf!Any;Zok+V)cU)>s~!AV=lb+IAR=2a|>q2r*;Dd@F$F?@h+7w)c2Fb!^4# zX~%ypkmn0x1-<@S*2?w!#sxeK_4jtqhyjAE13Hs0GQT4LxYhyF4#sblMB#vC9wFjs z7wOQWK0Hmr%_h{|$@~8nAddsHfR_MFYkTDeBz|aZ4S*sHI)BhaNj3punii`BW~}Gl z-zx`EoO1w-<3x9WHL?fP zx_ic0BSvfiPFimL_(K35EHV^4px2_y!clM70hlGa(kt*}PRE3uFi?Wv%;FuIF-6UQ zfn&~4t9T%btd1I++}PqE+_2kBoD5OWflUNqZ23 zy0A){6c?S0<@uYfU}}tMXGKrGN7=r4oP*u&d_Z}4tR1%~QK&kFtQ#-MI?=%=f_FZa z!Cr_E6L@~2j6ww(V>HQH?uK&nJs(o}+bZ@J?*Pv` z$fHR-8dv3)7Ki4we%~5f=8hOW92}2d!I1(%8o)!tjq6;%thnR*Rdg7s@qTOhP6;|3r-roe(7fKVkNwUkzi!}}$k@->JJFo1hL{*p+V5&c1Z(3Kzem6urPqs`n@sq`P%aFO zz=(SyKO2}&r+|}f+2X~BB$%Q zAD#2OA2t@8y{nW(mc?6g=8VZjI#bunX+~=SaJ7vZm_ycvu+3qpGf5bB%K)g%XOjXwX zu)1OEV1h7Mk8id|IJi=kvEeqps{aqWtU6hv|!`HJB#NX0>Dpu2)P8puMF$6Wq2+xAOXt!4$6~i6*h+@2_9w0K zrI!N?oGSq+Z0BH4;OCI@0OTbE-JBR zog+J2^PDDqSAu|EJBtgTAGf_W7SG29sva=>e?z<>NDckYQ!Wv*O{NVX>)_xiL9_Wj zZw#g?AW6Ny*5)y}M48pQ8SI^3s=qrqF;Zq&U7u~wXui@u9n5tQXCaGW+vU#iY~5H+ z_8w{5wT(6$x&7{9!iG6Mm@i#Gjg19jPIWPInW@-G|)GU*qB(y zD@d0mmlk0ey#5aYx+Yb`7(bMPVNh5BwBh7X=R%tf=0w4 zW%S)>GcvAv?4ZNuR)Mp@2>KrMIbsU{OMuKIu?wJHdp{vy$Ko@SSUl{DR%Kc5<9hO3 zO@iJSz2aCVtY6d(tdZ=!0-)+?Q=-a%=swS&Q=zcAxSoC5dQSQQ)}xcx6_!~`P|pxh z2>{QnA4B^MZHf3Uv3W(61J6q4NE#banDLA?eyu#fXc^LupCVH~DTh|s*F?_RJI!{6 zXF2H1&pPi?nPo?-q>LO_(b!^(P$|&Bp#o(P>G4pF!{pfB0iAb{=X=v@#n}0=ZnW(ZNy5Ux0v+;f0D7{xG`R<2s&$=7Kd>wg#2}emn=^oTUN@jF307a<3Gg_` zBVO@BCB_5?MlS=k0OCwxaCKnut%0C781feY9!QM+fdHO7NbwZFq|>!`^#SG00lkD1yegfV$>6FVAs`bd)WMj{qpV z2#6H0#`DFq)YFwQ8=WER^Lq!N0|rtopJuE^FB|`NmZV)fm1h-DF}cR%_b^EOHl~At zJ_2yTsOipaNe<*dl_BBn!DUGe;5Ft7K@DTONZ)6Sk0J8~1D8i4wQF&duPQYM%X!?1%YpOB`Xr!;f{LsMY`X@InjzMXGvHY2 z5Rd1xTLJ`2n?XGjHWHtQfC?P9A9#m@PPd$A6BrHJD(eEv&Y=D`^fg?!(J}A>``rP@ zbaldl1pi^OTNazl)*q>}7=!y$F{G`7DHTQ_d5iSm6z0pq=McG1*k9|MkiTQ!OHu$p zqRCi~tQ(N6n|QV-opjrFuGzP|8(Et%xw^wYTiNCv;CTmmz9(H9c=MnC$p29Q&JPW! zDFCQ`7kJYd2k2Bk6cF@@@ofYJUqG$DRQ^_(44#L0cCX5h%7d|UXe2?f``2cGzhhr? z{&gIfJ7j$@tF%s#a)ypCYTfYc9_-jp6CH;SmXXIT>d3Ozv8?^~@1mkrZIk z=SLlLU{MQ(4cJ3qjc@`j_4_O6!-s%8v$8S+WpNwC3Q;!Q@czI(25nLt1ml)m79pNGYAVGd+zupga8D3Rd{Xp>S%jazCw zcIi8h!Qgdv2opeq-IB%A_h(%R06rRr=!~+tzp!Cte#v&`fMMm8%mL!~JkAgr{{su| zNkf}}Cozc5a%OBEmm`h=(VX40{at$74nPQv;}9@}!Z>pdX2|)R*-|NZXTn;M@+)}f z?4m&adIsg)=15@UL7#_n?j1<*iL;*s-G{7XDsRaCH~?u*wsCtu5+`y=J5#2yfJrI= z8pIG}8HO>}C0lHgC5{Ax+pL`kfVJeA8!L1mR>jnIQYnyJzF)7%#!hiY1}F4>?4x7EVbm9(6k}W6OYLg|hgV*NFsN!^`vRw%^Hl zb-);Q=}bBh5SX^~+uQ2qJIM1VOrZR|6^Qdg%Re>1=g0c}3DRryh`+Q3^+RI=z18On zC2}~KYDV_Y#m4#4W9roJ2UtE|B$(IPz3y8cSS(LSYTX-)>7_Db@=)gO4#rPqr^kN> z8%I1EnOqAskw`;U!%7gFVG2c++cxS1_df;wUmPqb?IvK8EW#Q~Kw=G09~OBK@I(E( zfl*f9DcMr>yFNR}1sd`Vlj`3;>hDzb#X5{h-=#C5z`7U>Kj~V7v)_9 zQhKn)UYEsycj2D9tddu?o-49gHB&cZy@>1`vDg*Y9=)arW^TWQ;|_zNu^^nA@IobOjszVveK) zf~0+(1YIOf+l|aE%zd#2lR1&ZTyp0%Pk1MBB5;}lN$()fpDdyC`|vAMr$C;@>iH`G zpdZ0@xpgCUlc4%o`+jOMNA3)w-HmG)RDbDgp0@pSZTniEpBo5tcKTp#o1JY_dBgC3 zA+x8kdfwXo-i7sZYhVyMIrkh1JQ5Sh2-l;|wXrv|>8#4JgIVN&mK&1Ypoz}{_Wn?UtXEYBPGb1!p9Khu~n#uf6VY^3oVRUwWHq0P5sKw7&mglF1LqfOl(k zr3_EX;-QxKBKXnIk4epq+TA$rPVef<=>On32v2^ncn}**+c-h=!wdxuXt)%auP$T`R!s{WZLxPyE>3_CYoEdtF#Y+>g$)@+$jaad7(+bY%Gou%wi}8B2(Z4KX$1@( zrqwda=kZO<8&#j5A3IDs)C?$YcyRAGS#T!ItOIpZB_PP=v3N469FV+gHTq$@0z(0S zt5Pw1$5v12X&A6`b|CMj-p!_S|KADP4j^yHyoN!TN1GSyzOAwbX=O$kwgDz&DqCM7 z#v5#7n{!8nB&Jo$GK`LOk@8^k4tU9R)nFY;hw}KHFkNu0j!RK2 zpZW|KApxsFz!}ZH?n|=FAyi^bo}mI##s+0ypa6w~;MXp@7)aMH8KVWP(dWQ?`24|H zWCr;094${>SNj^B{q9rx@{?_aWf4ho+>^7tgjp(5%w7kL#Yj0O_5<=;rDE~S1xyeL z+JJRUBR!u@&(h~9*oSke8iF}9;eJ&LRHd`L-mg>=l=o!DZW!sssZYY z{Fj!0DsbnAGC;rf*gW<728PQ4aO6{u?bD3dL**z6;VonK-~W++uJ32%)ne}aT>E?# zL)(dkDFd<(nJgY;STI&feZCqSL#33M^2xpHk_Rb}Q&^-wI^%}l-NQBTl)^RDu-W&2 zR8|BXbxS9*D5I3G8S^XA_!fK;=%L~MwSka4o>DE>*1rB%wbx65qAkeYAG>4cyXQOZ z;08RM?)ABQKt%)(Yy&(%9sq`-TMN&4v=?WoZF=If|FXq}$(+c<-Mso2@`=p25M&_` zK%EBiFpdrlguOh%8Up1W|AC<(ee%@|uR;8ivuBhO{gCU-%lnf)Gd3W)A=Hf;1i+k) zi6ksd-A^R6wgXX2ZisPKk}q(Bm@_ig7>mrudHav!gMi91KXAs88y)?>H1=OCUF7v) zD`?YFP{u4?5x?`=NP3=19yHtKP=pnwF~ZotVc%1N?ux#;N-=H$Y`hIKIp}%fe_>Gg zbsVH~03&6O1d_)QlaVRtpNDzL2hIXP?5ki5FlJBw%%=P=DSk&IK91WZ<+o;GH+xCuPk1ev#zL?LWPU^uF4EJuCbBDm<~x%&yD5N_CZpHo3N!2H+6jdE;~G zOcCU0(cu5*i{M~C^`t-tiR@mBqDN}<#qHYWzR0Cin*c~BjL53@E9IlN$0n-pdbtkP z(TGQ|r+uHMkqf!8w+6(%RvY=b_JINZkCQl&F`$uw{@OFSK=O>wy6T8~LZR;<5V9E? zLD((~CP~oZkn0~9@Ri{Ls&i#brbVKS-c*n;d~%XzECSKBJ{j{WkU9XBU)Xysnemmt zKKD`SHTMd767b=90vh)YGT-|S7?Rr-zv)=yo|i5sg#R~>N#arn7=iP+@jUQC6loIt z56?+%^Tb%F=(kWlH#WNQ$Qc2~at?qN>0^Y66vhg~=afEw z2JcfrVvo<_(l@LNSTxgP^`va5v`GbeZGqeSJ)8qzH_z>HO<30jFqR~SOl5`m{4a?G z=GXKxC4FW95C_SDFq62} z%os;55hs065U-^tXEuv59q0Dv$2OXPciqs&{te$n{#bx?_VGwi4^hAgj%Wv^HOZV3 zSw3NntE0oRI5i2&G6YDM2k7@Xw$1a!8BV3Bl?B+7GY)nnf$c`vlg@8si|{yt8^R7z zW{->|t7jY!&_o<(;B1Qfc)~t$<-EfCU;>TKppvq-?;30LDNG*)v1N`LIHSq?Ro0

e~vq`MH5O3-K&&EkOB$#(&ZkyB9n=#CBhtSl!zFzjQ-Q{Spo7fXa<)h<6b(>WIav;65iNhKprkhiD*=c%`E3Vu(YBZ(t8Ja_{ht zc;ebetAm$nFaNjLJOz6G0TL&99z>87+p{w3-0ZP+Agfj&FuaMHp08sFWO@zscipG0ZwJgz7Kllfcn9E?|yI9}NSAv6Ccux!l`1knm~!hI@o1 z0o}vNwKG?JoL};vU1w5~K|Gz+J5?mQrZZgKa6LA;QFos-7v zVf&HuDHGF~?B3_3YXJF;=b159GEl;$7rhVia_92ZzV=Cipxb+8POcT4%a|ZJ%O{VC zHv*$1Qx1x;KIO4cwn={qkTMnI@iszZ`lOR3w(lUW0stxnc~%hjU}D~$#3bWy8hS?% zbCL`S8q7Twh}YpIppdi;fHIhq2kdp0&F6qfxKBuZ2?&Yt%$&{w+Xht#HrE=JC-)VH z8*QIBezTmxcr#5(%xolNW#LnpBtBNS!y0WSY|~yp7@Lr7!vHy#aCFrM*e82B*;XhC z2fM+93_R`~Yb{*U7?OO}R2P9wwk2iO&^9x<-tE7o?kstKhk*YQkD2YDpOf*AeH@fE zk;k^NoHBTjz!9yjNU<>e_fit<;83uZZUcOv%sDYm0ue>h14?|$2%qN*3R*U zSUk^egzPMy7K`WS7KdjcK-Ow=JW~Q{D<<{^>ezyr3l5cKe#uLWkPoGh6y|A(u+oI2CoK) zWZr~}^l_8Ep^O6BVgWAzcE~2-IsJO{Vtsb-2+X6%C?ZmC4@`3LE^!TMtNf1!MH*FE zw**k;{@W(9bTa5*7PgyL{APA}2R>hyOLM6Z2ioy`f`H*3C{=)p9gkV=skdc;ssW_s z8FJea5NnG3)FJ)&HV`L}*HHN^^R^83ooPAo{m z4&wh@7_$$qZ7x?wCk{Av0Q?c=fEY>XMax)oc-Dv2Gcj5!2!*z>M4nFCuuBgU7?zd2mO%IlLzpk13Pi8!6$y~+C7ffr{7 z8pDq31d)i+xt7c$Pw~8$Si4xCfH{kIfpS>?Og55(x&)}a^KDTD#eL1kpax7ts7>LOC z^%SR3IN1UumHt$K7y&rc5o!xdWw2ff!{@}j@CJu_+P#)IQsFa`eKjZT<~+J&J-m|G zQD90gs-K`AlfE|s>u+Q~hI6H*4idTjYrl(*{eD57@oxb%706QnWZTY6kKk&%hJV0=i$QBijwokSTkDC2+QUvBie4 zgm8~)_?tZ&qe)`)9D#Jy+1f{eJCGTxw$i}S;}3%9ZviQKqKC@pF>CNG`hKw4nnR#l_Nc80S)w9p#VU|q%pVae}h3o1W z$8Q2KQVtqad0qlJL)u_n4>95}4vJixc&5Y1cpwjHfua>r4m1yOCru0)vso(+do$u`FI78xu(xrQY;b zM<{=19-hWfmfte3X=A)O0daBZ?dz1j_JRDwJI1}@Ss{sh->|+4pFDPn(Q_DR8~{8! zHRAI)DQ}vV&X;p(vAoQzvsv26gE{G=aeTO5^=BhFVAuq#LYH#cIWs1Yy(7P48MWY8 zJy%w42ihga3K5i@lD~{m#L++7PnWqa{19p z9uLu3@a(fGMdu9YZO#_Bf8GP<`n8X<=7rI+% zfHHVC$=J#Gv6WRqpR!)^8-S)bjA5HF#{fKmr39=&VDBOgfAoe18Nd z$P(nc#IgEr30#mtrEGn7UKzAckP$`px!4c@{8bX>kpRaAR6ilDkGkaR6qNJe^Aj8D z!L^n#I4di-Zue}!Qo25V2Y7zZAkV-41>PDM@~J?X9~*mx@-I=vBpWAJ?pw)e^##>9Jl-kKB}rQlwFeI6B;M_3ju0}PW% zSP(op3ariF0+u*SMw4HQlaNrJWs67twgCVcpbLztkfeeP;0AkaiP2X8B=Kw)V~~J0 zhs6L8qyCUME~~L924vli#$Nhbd66oQGW>p%53tuZQP>1t8~FPIkLp_odd<5F@8#%{ zdRzpF)C}X+|1I0y$m)xf9Z1iJ z;abWT$MA5#MFQN5C!szQ_tv8U$bp>$|5q~4Q(oWW7v+Y~Tv`d|mupKodHvjIJk1D^ z-aq$~{V`^PJ%zr<;s^);tx{YB_!%b$eTc-Lz``9_=KFO%b0BemHfK?I95V%doRPEa zI?D9&VAxx(E%)u`^EQ*G9H4OtGj0o%CiEHRpo#Z2ISB9U%=FyyUlda-mA^A->jdy( z{+`%FR(5-iC}-*jQOTv56nyd+ahE_!O|LKYKx9Av8(^5nx@5UIrjR9iwRa^F#PS>E zl9(!t5i9l6>%G?l@0Z;1YYQ-B&LIRG`28wUXvQw?su?R_B4Lb&fj5}v2#aVsxZ`X~ zlu2WB65tg1)k#*nDy*=LR>GX-j$>7|!ro_}*N9Uc9Pt2ZU_E9rxkY*43Iz~jz$}|P zio774#H1grd*wZ_ZZo*Q#V@6^CGLHaWp4c#*S6-|Yyg~q?_AdsXdlAn$NmkRlRC%# z(f~}>1MaH~T}iM3Fy+#4`TTLbgFJuUbQH)@l5CB+QW9&O!6K6vN(AlN_uk{yptD$z zXgeDVq0UzzPsQVr#`dUVo&rK#U>HA8rq`BxneUZ9LauHSAOPSa?2VKXUpY>$lg-Xl zK2^@dy)@kzl4`y4H;&sJyhSF726F;JC$!<|2|LUvO=E8$n?x4JOoLb^0dB=xpK*IG z0C*k%z+|ur>RgrxRvo_?#nkm-pEAMf?X?5l2jnJ)X%d4ih<*{1FKCxjUQhr7wo^{N z2IbQ@;Wa~+8KZ?|1TbHMceq~uJF`g_2_!LhrfLTahz9pVrJl_grak&2y zxi0k-;zZiyGyt$kzY1Vi0$HYU#B9=;df>6VOeVc1<=pZ=l8;5k(m@%;a3L{{l(pra zw*tO}0E-*hc#Ifn1Zc>ZISH^Q$0&#s#*UfdB^on-Oman@xdg68Lc5k&5V($bhg#6# z%AIj6;IgqOhcXBW4powIVZ*86l)i9F-3!1FDW8l zWj-qXI~K|MA0$stM#V!%}v%9Vy!agKof%jaXXMG@Q+XR=LZjmr}|sBf$m#H`kL zApHyFsfVdRSbQjFo4R8g?KUijti90Qv|W&4J;>~mHPT{i9`0xJ9-IG3du7;xB{G;7 z)L~&a0!Tr;DvA4fkvScbvGE*`726-*6UV4cfQ`xu0K6U&o9STrY#k=gVeq{LDnA1L zOUi6$^Bv0glYu;;;dVAK@&CrkE_sU0mh7Er`(SugHH}bRMj(r0YETSfbu48NOGwI zXO_4jk$Jgc$YrU_gV3&vGE12xxe<8qGc+u*T&$}r3HHro;Jz6aU8~zAsDgMMTgt*T zWDJt;h}(i3jQ^HKI?u7#JCwpSc^r8^3Z(JAD3uj+k7AY~08^yh3J?>_`?7YGpVOQR z#8E76+o5rJ@H1cu1;9r3tU_RC+Idrl26KYTCF63|kFdft%e`t)d;iZ3x$KYmJ7M!B z#%Bv0?<`n8FORk2F|B-FChJrOEIba&#=LY0fRjj99wFE{jr}9Qh%m4e)Z+iV&ViV9 z!^$I$D$~Yj8ACOmlEqmOr-_sEf0;kv-J(rM%no4^4VF0s+ZkVJ2Eg~?8cP7Pf#Ca- z*AzyOFy>o^A`3FhvAw@?HaX^%lv-PC`+QdtS5YPR(EbV7ERlRCFvI?EiDR;^O~L3o z1%z6{-jbN(;;_@&xYX~>K3kZGoa;`L#fEJKta1G$SA5raUVa&;O~< z2NBYCok}LhRvl@Nzf-v}nBK7MwcSHWwUq%FS!j)d+&k+A+mU!KCXda4TuHu%6K$KE z2lZ~Tv*!6EP~H|J?qYP?5`>c6!00w{2PgnArJgM`teuI*w)&+qB+w9_#)I`e z0d@eez~r#~4_Qq-BTVQeh^8`7>KZ1)$o9%(;V`g_k`E&&%jOGRzS}yEMdP z=`?nW8{vy~XOS3N)5i-Ms2#L7U@91EP$s`OGdc=Drg7!M>u>Q8-IT77b5gT!*~vQj*C8hB>~(5fHwfbBsn9`_DPsLo*fWO*a!g#hAhB;D%IxyAO3%Ca0=>f zT}S1uwv@;E?I|oOw0n<;@ zix|t5NiOpYj>Bw|2U$Mi2m%>duysrZkuq(S46x#OrE!pX-wtAt>AO1MBJIfo{#Sy; zJXl5|^@;il#)<)lY=ct(47^v^G*FMssfF`UGNS^SSpvTYoSc@h)A9V>`H1I8#y@V8 z{S(>Jona68S}PdGBQ+eet^F}T9;DwX7&I+dVgo!r6qxg=z>Oc;_fK7_tQ$iS7>mXZ z#tp5>sN) zTTm$Rn2@5(m>kn6D6$+_NMh09Y=Gg$bDuclJPJJUeq}MIMo1hZAd9nim}RUDWLXq2 z+u>L_4Ja4ydVr0=SSa9?w2PjOguRr_GacmTGo`07&*w&3XLX3Ds4`Hvm`xeSJT{TJ zu=|l~nOFJbJQ&6kdiG7mKeAX=EU$r6atAv&CbN0*Sa2TS#SH2q z@q+7On=-7ueHbu>?c&kwY!h$G{y*pSf8m(L7yWnVJ9)J$f@#U z4jggci9DDM+*d%`0gXXt1=O8eeV#ghb%1BfyFO5EU$1&S9wx~pYq*2uv=}%do8B2F zlZIsJ=auQfITJpG3S3|3%XDINz{;Jf*k-!pY`{pzxQ_Eeeo(&SnP5E# zX?~6)oU5Qc5ilTg#+e!}LB7EFF&52|?@cPL*5y@xJ|F4-j)3_+3~Z*{B-q$=^n>i4%G5~QTnyHK-#S18WBmmK zqD?N&`YceV?yX`EAui9oWyBJyYr}w#-!s=P_WF}7nvK%R|5Buv|C=UvV91dF0{}@V zo17fT0v=^-b+s~>XqZ5?WD;~r+@0H)$XJ_%%q)ZC-aadM(xhP}U6{#^UQ zvm1srQiv&mI z><8ba$GyR^E*t2J>6sd->_1tcC=ejqYjlYGoT4ON%jA&J|z-jE$`54{WPy) z01_fEhWaOS7po7ctX;r37QiTH25I~wNtlO1QWf$M4rELN59AriZ)a?XG}!=PNu$eV z9~^ljI=g3y!NuE=$0*olFv0dG-=jDIND}EHab^kGg^M1%_ROT*EeTK+OZMjLhXj0* z{x1P|Xh@jB`%pHNfY=6{Zab$l%-pyWsT26T*ctZT*Y8mJ)cc>70u~Xyf+eEU1r!^4<|;`pfdYEoMI%z1vE!|NvhV|`gNcFEcjj;1g`+S}80qN@eK97L&B-@#fCrBd2jM#)NL9*9CAl@?^ zC>P2iaePpp>o@8c`Y31eiS5AV1IwH~S!R(&Y)l`Oo91y>vje0Yz^^KY8?vLQ$G*5dBm3(WXb+sNR#*YN}AfN{UJ0sLTacu+1m z_?-y44V_k0?ivgcFM^AZmj+t>RQXe5I=r;pnIAX&K8VD^XrOU;ayp+&b=mQ$6jnUR z=YP!kLdbJ2TfxZ)C%uOlq;AO9{+ngwBnA1ToD=GMxPbKc7#K=1n1W3t-fj5*Z1P}_ z*3No5I-p8V2jmOOhKV`a%WpDZ3@-ZU<<`pi+F6PWJlcaDir3$OC(0Gi`vMSTi2-2Y ze@?<4E$@Eq0(pKefahE5|BB&*cs1f3NAOP;iez7I$k%O=5i7CLRL+v0A`yo% zLS#r_I1J)s1nogm)PLseBDuCW8s*Y!Av$Dwi12f)LHUh={)uzE9$HOF%h+yGdGvBeokm4#&wM>iZxW&@uu+m9-ERALEB(tUj7704BiWJ zWk(r1e{6AhUeI`U17L1gFr)Ef@N87OKsJmljJHkQ5I;@vMqsCQSp560^U<)Qf-vH#)zHO0L z$5h^(mDuYn?@5ek51GhM$`07V(PV(ox0B}mgzSXN%T*F-KCqI!f5gI)+lFZlvmB)5 zn<3EpG?Trc^aID=04%ql(@Y0FO9Lj6#T*-4WCd~`a!+l05JWulp67Q5cx?Pc`Isl) zd%(7c8AAp*1uUYWQkFRi#{O2fCIE9Sw|V26 z0f*yDAIsaAa1h*TeGDWw!B|E6!uwiiNp!UDdqq&&Zx{;^N`8LLAlZ8Cfm|))C^0-_m<5kL+dZCU8WbnRg=W|S# zNRjgo`5vSoKC1Ma!CFvK^n7;sTv^^>3Lp}L1NW@+g=N5k8LYU55(Wp3MIXfZUCmB9vQCt3n6Ys9JkWO7pV02K5oc(N9HPmB(gBlkstGJE)(Llj@O6rf!D2HB66VLh zB|uZ{>7B{*+b4D!{*p^_tTA)$jotF0KJV~qe}RPARXPLcn^$g45Q((%^R>7h&F{M-X!% zW|}yI!VM>hXO>C9iT3TLA*55Mu`lJH9*yzv4c{M>z3M=ZQBScQ`(-j6Hb4e}#Sc!3 z8e8oG2|7q*AwXmSIeh{TV-276Rpiks!x%4%xIB4KI$3~_nRo5qy8#XQx|?P0Sv+Vu zBk@sr3*hxY-I`+fS}^843$XiI-{D15*LNLmww+DyDlWXxE*z+@XF!9mEKZ)JcUPwL z+){=P5*U3~0fe^H$HCZ0;-!eiD2&=}{61Jt@e~gF%yvHU0LFG0P?%XieUcl*7^>t3 zu>gSa*gt-qAs~rLWVyqHpUy~aSofa zuGa6LnS8J_Iz+-r|7Q}$&=w$k2~aILlWhyw&~YCYuy$jP|B(m-NKL^+R8uE;#?Rc9H*gwnGr6voM#wfye1>8iASe~m$s!4t1 zc*3=!%pGK`usD(a{v9~=wkTsa|0nZ-@$3$rGxD=C#x8&!sAD28h1k%mIe$G-Kj>Vw z(($LAL)jAsa^G;=nl@@7oy1 ze4i{k)|wXq^A@i`bs>UtGF}cakw&vKbx`an{lu;E0h@#Xn0&^S-JUuD2yK!5+pLEo z{YH>!v7QPGzK8N)fAJ)%5P&4?QW$H>#tU1CfKpWK@xDKgJ<4Nw-8|(n^uC{k^A7R^ zDSM%L0+moE94+onF=E>HTmAReD*hd@X9xlf=pRn=Dv-D*y#Etk+h;_{=b%Ucs-)T4 z{ryJ)JwLYCLw)}PKmY-Pt4Vxt@CoEvBC38cFsdhS(%FOyuAO#ZK_{g2bYwNcSWrvx zb_I1B<*$LZ0X&~8cNw!U^?9~@k$67vqC${3>>CGA^&sYF5R8};TrxLUcb;s+Fm&An z0DWt~@U_3A_WuoXjbzRcto6m3ry(Fxm9qfv0=rS}7YyW(3s5lakbZY{aEBlc-vuUi zjPta3F94FhHb&J6nYpj<2%E^fuhZi80GO}qiI*DWE5+*%K@yc+cM3~Qzz<=PvOc+t z!y$5qCLQyv%tZwITxR}v&~On*X0^#b_6g#Hp&vum!1YWciSxla5R6%>T`yqMHp7+N z|CV>Eu>)`}9E-Xu9?uClB_5v3K{hv3aSxf_6>AqE~{A}TO(LBBbrZiu`NenX}c4gfe|l4j}C%U^HxF?n*p47^+boVHFEQTH{i9J@tC+@g;d~ zX9?pO>o~>ZO-??Ls#V7cW~^tS$AxD2PP)V2$<{YwMd|&)-z<>Z_Ru^|Kr^Lju^*RB8$#s#8 z-vjG#-t#H*bh6=Q`+o4b$$dQyEMy%aLtC5?B@U#-Q3l(Qj7Mn)%7pFxg2XYNVq3cs zD3)~u)^_#I5I>k;rbuR*R43DnRx~nnTykzmeW}5(;HhaP#3bu=ZEA(V5x7)z{LycquefvxwaOdk2eTz$1GYXK&uwGU z%@{e8z`OLX0Gr^+*Vs0YY!hv6c$k+anr}0Es`0h$Pl94sHD?H zfHoxb$ztRcmWCLbQC=AfEXcKG0ZKh4hriF28EoBX8L-y2c^rCObVCkh%j9@mQ-BzW z8D&l>KBw{+24>i!V~fOYTZo@|u#TcE0dRrD|7z?LmglzL5>p5UB0di?KOA5rV3RTT zyv#v7jUCL96IsZe80-USFzvU4z5vPDfpQ*vcd$73lJzkz=m+gTUw`U7 zFmwEndjkO76gcX)ti$mBg&o|kFH*HsA9=zdafwr5pLbCKg>7~){v*z1z~$ojKybd# zW5IAb;gEkpA2Mx@js2%dZ!&BI${fv8bREUH z6FP7V;Nj@cxz7Kg+TTykqO0du{YQOYf#kwWj0A|o^9Eq2GCYDo`Ns1e666pvGoVN} zV>;_8BrXrWJJF#vuWW(`hcFSzY2V>J%DtO!|&SC%O-NVx~2FP2B?H?$0#(*@hjoOAW!S*DP;v{0;ymdk@Uh zfZX670>gF7LHPwR@v?JHUEcvZQ{rQ@f9+$ENOR7RWz1aThW^Z zIIQ5avvoY^zuZ6D%`~SR*1II0rT2vnI)vV(L)wA1Gh=8VM@EofzisDpkjFjnbB8yV z14KRomSD(!^#11i7+3z@{am>bOTD@CGJ9KhsjDgN&>T)Ck4qeJ14-J0;|a^?eW$+@ zbG(Gn)t)K;V_o!j=(Yj}zADN5doz}(F?jm60LVo==!~g`YcG^HVJY5hzDK7zajd!q z&J`+mM&>Q+eZpGB_^WMJm^xDo^-COT2j?F=Z&!z4y9fBU`?@P|Ze3j0k z-s)>(k!U>0h69+}~M$D4fs>^vLyQr8v<)24`{fLc9M zu=QI1fx0ac-*^|R)vxwkk$s)B z^}ElZ0eI^)y?)LptsS-Zc+QFg85{TNy7M3>JBr zQ?$a-2O)l1ZI?c%-fv5kzO46ROrw_RIYS9H$^$+D2tHW<`m>W@n1hQ9v>^hRXI<|p z@?xYr*Ryvv0)abv0+P9d_r?W_1GCNsbC2Q50^CZlcMF*BeWn0_R~~1EM<1`;L)GGt zSbr_$ccW2db1X4qI_l(iX!QpfiwF6yZHqkor|8DpJ8~~k&M%lHIV;6u4#;>k5|GEU z13Xq2%^@a}>GA7^(oqC~R{%Q6dqSNjAi%MW07-54*cmRJ6nGIYwl9tuVsP35_8-RR z6KNO#o$fTFz^5COmKoM??jT^jw0Q%Y9h9Dl!9E4#?O2JCq}NFuxB@39xmG3%m&r?d zES$ulB2Y;PrLH~Bk4Zgx^7<#gL;3l9Z7J=^jC6}_$NFa}7q*MXyf{OcBMl{uTyEAS zw!VX@7mO>hoa)6`__&_Ms9Sue;*h5X_R3eC-?=o}l}2Qa8I0e{zT4WP#eM=yXppsn zeQ%uKu^a-hOk7t0QH(7P@NBV#R94nG$I-!SvyF#Nqyy5y; zq@0Jyl3D!aQ(~?z97_ccZadpHaVjo51%M}sAB6A%(jTKPw(}7LoQNCr4R!to7Lhy4 zc&LMkxJCEMWbQrL$F{OE#`Co^l>kzS14){4FovKl>|HxO?{69w#?yEQdFAg%=l2JLO!%PhhQ;ag|!NE=#8A&F6p_gFd#g)WNlIs~T zv@>rU5B5nAMr3;ygS@vmJWXB;lf7!IjJv{tT!6hJlRk2qxOu1HSTi~-^&l(Y1N7H> z14e&n_uW}AoeA@?_WRU8;Mdj$f2#ZeY?)^f)X5@coAFKn6@WZ2(o0*z{cv*1G4QCYV7^I#SlpRFPIIsx zNPI93uM z<9Tep5qj2pEE1mE%bwR$-)B2O#q0RH3_zY~|3@1BzA!%xuz&%F?{ksuhI4KKhm%;4 z4;V*=1jvPChRkum^bxPy#X4;4aWRIqEc@dc2Cj*LYNa5lfSxKFIx;qU|H|)6 z+FqC(9IN(+(O(PHA~RoP%G^Xj#d7$iSJLttsz7WbAyrG}$S8=2~VS zFLJ5GTxy+t&9ki+M(Vtv>vkk#592>!^O{}38QbRI%6=Ws&E~gm6Y;vhGBI}MrUaJ& zZ32!->`86Br!Ns(3V`!~bv z8$I}O?a@IQPNHN1cN&^o`9>y^(eCgJD+7#z(G&pAB7g4NTR9J_06B|^L_Q%$ul4>MyvbuSc)Xd-Nhl6f(y7uBFjqA!lIn6j}(kq!> zelBy#%idoAsJXN_ zOee~Af`0u$*o3E0yD@=BA68&FSyLVCCIBRT+(G6e-rFmHtrzs2v$L`)UtuhKw9H98 zff~x~oh|lNJ!?$rJ42|pAx_pQi`(AD3w&~5)B8O3v25$!N4n9AeKr@wY{zamXn zHNJvT{asF!4h(sJ9xxL(+evElQNstH4dpq>0P|QqOXL_J$)lR~3;VCY`62*mGQ*_p zYvV;=wC7@Q@>vK=)yg|f#>NeT&eT=IcrL+oQP9PYb6^W8U@{#sV+qMTFe$|X36GON zDHxmtW>}{yyG)tL-wc9FY$tLL1gEGYPZ?3(`(hs<*GS?_x;jB6$^ny^%J#%-1dI=q zA(7b1S@yOc?p4?#ksR@`wq^j00HCohe!%u;8p5ueIZMZlL_WrO3S*^utW!UZpF08g zlJi?^7_whD_wnodI#3vY5r`-TQoV1!KLG=)3^d}^Z%LsvU?m^>c~9aw@9)Whtb=V; z0-_6W1GN^tmV+c5GvH`Rx#iu<%`rqkD==ulZg=+6B6G>!9oIc5!%h2qI&&lOzBooH zNZ{>*YnFkytei*BZeNk>(}CnkRR!1tPGQ@hvYxUzAlGs1gBT}>;}hQxo6 z&vJe6sLXcAc&`_>A;!`Xg@jfo`#W=I5w3Yeit!*IMHxMbZ+<}?S>jwVSZ{e>W>Z5w zD!V|hN3Ntm22e0&Ph)(BKnX$3NvHDQT*163OBz2y%1)1sHJ1#ra|N37sx5BU@(H6S+duG}jdZxv$t?{C{Zq$NlP^lT35%#d>w5>-d%Pn&n>(KOm6Cb@ zh2AOytgn3l{u99J?MW6?mj=?DWVqD!FKseQ4d%19f2n;}{r9&5c@SiiP5@*GvHs_Q zh^5^^ibCt&DNM`Vvl*yy{FXk&Vo+@9mlZr9xZ{)6eEdLpM2FsUz#Q5p`DdWHfcIx$ zIMa!H+h1kYeH$<<q?m;zcC|P+R1FbE{iXtx#5iheQ9eV?>3n{`a|v^43IMkVNa49Y&1r|*0D_>u zm9=r*GwRs_lM3rU*zE`)R}!}MrL(56pUU6{4vw2D6B#p@-}A)sq#%!tJ%1vQ$I*0F zPD&@Wpyzisa=c&y9v`|Pu$AGh;$!>(W8h26cl>^#)Em7VBrrH=kXY0s7#g?PUye=$* z0B>$AbLpf*3<5_48aQpglFt?7c~0aZpo`82Y>`Bd)=bKbW4tq#WxKLg12&MJ+pp)+ za;ZdJvZ%5C$0Q@^RVZ!H0>|=m_PIEWEHL6!hI5UyA(4WUHpX>9l|h)-Cv@ef7?qaA zm+SwCV+jwbjyJUZ!gOo*_?@GAW07NI^!;1 zH~DxMyq8!u&Bu{kQccb&pcIdHwpYL)fYDTB$00=-rX5IE*>l)D2mT8jK>}c3n6b{p zb$L%SiaMLsK~Z-Wq`kxIlf`}IyohyH*dWr50Teomjs55jWR$ngRUAm3Cbi7^B=J#i zm}jZuuz~6pKThsb>N)1fkRF$~cuVSBIgQjdGI^@ZUHY3&8N$9_l)M(%ydm*vmbJ-l z&y?o_@EL89Wn}xU^J4x*u+im&*>_f1V6OmU$H!K=k0}75_Ky-%5(-JWULRsJ9vw`R zwuyT}S8=W#bW(kxJj}O(geu}}bm!B-5p`Lo~ z{Yg2w!?dFl=;-x{d`BFvqp|&7`*ARGGGFP@)t>!q zV9o*%RK^pwn|fD4VtoX2rS1!!!(cw)-Cuk806^E4Z)EF$*~$EZ z{u@%8I9CIF0Mc#oNN(|QDpMwneNMF}uwWuL1rgW~7oHsIn=L`fc z@^YC32P3xu(Upf?&*nPPFAW$OXc$@S+xSj}(YPca@Di{a?HT|JfO%PvyQGN+lAdH+ zN}mLrgsTZ=ZUudX;H~zOE9*D=!aVFj{a7E-|wXYVVzN&xV z+<}UR#j7E^f-+DpwZ&}Z(YESwK88H+>1Q|2%>FQID2l`1S;i%WaL zI0)Hy^a1RD4A7LAJt}LM^sjo1zcU|avNVwYg(;8Y_zv>4^sAAOw7dd&>T@=u*@XZG zfSvje4XzBewT%X-qe-{jI$+d0I`NP=q*H;1wKE%pB72GVK1;$>-;59$z;y zaw-$l@0tA}fxH_gvcJn{))Mxe)-g(R|({^jyWj znHS#1^FaL0a>|__Y+K-bXU+!94uJUBHv(g{)(vU10l=ekuMyni4Bb zoIMeE@%e%DDVUd`Jz_s`(pmQI`CM5SGC4kHv~iAR05!im&_?MepD(5Lo{~_NH_jum zY=jLZl5^6JNnh(N+xC;^WZmfuVjL?n+pR1pzc98Bj-l9J>c9r!15vD9Qsy`OnRZ?@`bFOY6U$^&Wod%n`(Ji4)s8WY-8FEikVRPR!6xytexB z3LxgS)tf88pAXE4A2!KM1Ow+)r1B;~MQUjS;5k~}XdesD<_o|bf;}e}2F*+BhBoIAIve;=))=o;+|p>#BnXhZsprmi7_^L7q|% zW)Q1sBC9TQ5X)~QhSbENC6IkV`LVA8QtsI-@r7I>$(=i#h2TNp=_R@?J`Xb5pZy#)rWCuxomN69DVJMbkIaiM!lHLh+JNuXnbiP&(G~0KCoY$`aMI2ZqNfi ziO*H}))+kZE~nOjoY%JfQ*Hau%J4&Nf2Aa3t5bJMxV38#=(#$3u~MA}pM!A_I(@x^ zBlvvu|0wIj2|wB#|9cXoZu@ewNxkcr%DbOOWE%PV35xgwGDc6CtOJChV}pBe<9a6+ z++tX*B;W)49JF77PtcxZh$#M1M4Y9uu7C?;(6SYN<95!fh|&)%x-)bW~dHi zmo(mj$()d}EfPU$>=ZVS+_TT2vN#rQoC%y)jK#Af>APHdVVi;1i~|7^8f7#_nFsH6 zuXbnYpih5_4t~3qOHByCLuP&Ne-9Xt%mes>V^3rBv!0>9uB=AflTyHHAn~|ro8`x+ z#tft+jj<#hDD-);?1%faV871rVah=vFFXDd);f|f-ejEBek@QWkbTqRJpg#<7`Ib- zK3+Qjl_D8DoKKGrV=4{VM}Z5*Z(w~9cA$cq_|#a7e2%uRmog~38fz>_W6BsKY&MH? zBYpW{^BlCh3E6+OC1Bg(GGu&aysmFkFvIVIWjx7nmiNl?bHB-Q+PczMKzIiZ^yl;J zVe@M~2W(r`H^E+FZF&Ld;5>XmdTokBSdWGKWSbYeh)ESw28{1)o1%%SH2@G{n_V)7 z;~WA$aoqR1v23mzGA5VAFhAwKYa7ey4n-mbX=4Y$yMZ!oNG2|89|B?xV;*yjYx8kj zBTYw(AHB@M9*(zo=E9B&i2&Lsy{CVfT$?W>%Q)B!f`9o({;__qW`OQ8Q>!vj^J|j= zy4COA8Fr8CM{i`6?~L;x#$OtV*|=#qJ^FRbASv|j%Aj?EZ6k)Et1Ole0eOtQ!{5`` z6=GNkKn^@ZYv2y>xYwF@mP4`(jG0bk%phWNqmO$xc|IF}^S1&?OPZ|~jkQl>@Qh}R zf1?~0K>!%bQIFi<&N@TDQnGmaxF@^>fUKlI=x~`6dh=8w=;K~+3mX2j0c~GeKUeRm zGkE&_<;hqq^l+wF0AFMAt4=`!Pfua5n^x8#I675rBBGo6r83DD-yPN5d{}%fUV`7lO zo5i&Aa>-&&zyw?5`zJvFS;W*fk^Vw<&G?)J6Br|X;not%YD_9Alg}M^4N20jHEu)r z4u|wJ#*mouxkWDQAu^ck!ynY>XTL)OCd*BR1&wfE!9E21UdT_dBd7HN>^GP#&^CS> z5W7__rkAm@7{V-&{3OGfpGm9+&ifL8HHHGHp;E8h*d&lCb7{DXOn2C(*i+4y8d{|Qs^0?Ph9z%%7$hNNg`bz_OzEZlaKA$Zp z|HsO2%_x76nhB#Rko!vNG4Jh2juCI<*b{S#_VW}_Z^@(xAewp<9Sj)S`XI7%zP9)D zxxNF?mir1*XUEhh+xv~iX*{QAZJUh0gzq%CzW_9^+~S>~FY2ry0E`5a4rKOKn{^7T zpabvjX$x_9Ydr~$(va4fboG4$ygAKi#YG~X?E$*<9s6%MG<;snvqhZJ7iG=(9R4-yuOd|WJkE9nS**)19>E00t3kmq!)%{9Gk_`5xKPCMjj9L z958RyFfR4i>L)Yjc|#Z{0C_YX&I5fOq%73fL#EC}m@_nFP}Xyk350A0t!e!L`x_q9 z%VVDG5-VeyC$nT_Q(67SbB`nh114EJ#tqIv8*^42QegC&at&ii@w2i6C|paLaW{G% zEa&$FCPT~a5=6f4s}u&VUve&e=hzpylJ`TGfGwobZ`jY++k?N`K3Y$Vg&Q!QcHn$? z5;zZ4DJJ_mnKKL-zinQyZJmi@4v7on&(6PZf_lP=7JCQoS;}wy9QVaF)plS8VRJIE z=i^LnI%xWw+nf)?4kmS3VmVo&qZS#IF1h@~dJ63makOij#IM0PjI~!KNWDEI{uSz` zw(-G?^(O01mB-nt7DtFqcAWnR6N>%k!{l#83U^6+YDr>DsobHfB=CQ(55bJuU>pvv zDy{(0$WpveIM;od+fPHMS`yViL+)A8Qk@2$Oag4TjP^HNPuXHB zr&Q-5Fk*%*k$KpCA5QQwW}T#qqn^PKiAiidQy6h-f3UI&c*1!1tpivc>;d>wb*|mN z$VixnYULroTM|oQ;yXPppLicPN~_{}Xy*br1MHB=u6Xb|SOX~t1@#038uI-VFhQl_ z_^$X2MG_N>Ei7on!&>AEjQvROx1}JD8;d6w(Bf4#gL%qCdGH^;=kYr{x%|{3+qO$ZN?$)qjkjd=vn&pR=+i7CFPDz#3`I@Uap2JVMuKNH)b3-`xwsgMI>0! zPl&f7<`w|DH2#M$01g8dP!<9@tH%ozkLS(y$M=bS0{Ja5ys)17_AM-fupQ^}mB<>j znPWjP&IrKdPKO5a@)KDYmjTctW##?>i&Kup_DG77_v6QzJP_Li_eq&PT(8LW%=o?1 zc>tebB1?#DeLR`#!zJGsztqPzVTBw5cA%|k9GZ)<3KF)NIky^s1II3UvoC9__B;f* z6K54SIvX%0^{idPpB0VE_|IgSc>hcKDDC@Y>_1sw#Sq`-#x6ZtS!2_YQ6o(H(bq5p zD=-%y9Ai1hMV#J6@PPd{9X4y*!=yBg!DDMb&anXSR05j=4w0A(q2B<=!#QAO=~I`? zFQu;s8;E6fsnjLTqRtj_=fNfSaIw%C()Lb~Eu;-%B`jtGyAp>)z8e9jRjmSXu7j~T zNU@5PGn(H)9wY?dd##uZe<^_HBV^VlB&U9v?Eaxg~3 zA%U4ESuwm<7`_i-?o8w$FdiB^4C=gQqRnm~P@u_=Spo78_#tb|0WGwZK$!os zU)nVv01|5(7)ebln*F)}uAFd<{;n!33lCtiF;fTT0=qj-064sgMkXjn#){(mX(BS~VsD(fKdj13GP5g@Y-6fmYwe9hdjv!smjuI1j4`3~hC z%~@fHq`UOx>TtM1rWDgK5A;C3NzU(USN69eEu?XT=rm^%a-7qN+@oa9r=0mK^8+=u zVqUv#&0y0rRRC~Njs_aWs4 zIW>%n%CMvybDx)x!O_-X-(#*gq7w^B(|tFmc8}PsWTJ7H<7NW8}d2YDSPP1NpUsJ22)lS8|Ysza1pW zw@K$4H;hb@jb+UZFnJyJL51fF^{QR}7|A4ou)N)qgPAz{1GYsWu8PWn4vrW7@CmDFyHq5xs{@j2E z%ppEhz5;wZgpmr62YnH)XHs->58Dj1|Bt;Z2J4kGcndj1bxJn=p1>ZUo|{JJJt z?@@N^Ybe3_9r;DEEHRC$xJ26`!(?4fjliaZWzJe*A(-i7fPgSr<0^ltFMx&$K@+ ze3+tQB)QiA>Ba4i6*=rDlP4TBQ2}?bdah|OW@pDqh9t8(W7{M4Js2rp*N`%Czf}2& zj%^yx1bOg326P2=HmrmGB^nY?WRt|x&00^8q%vM?I4QouCwqEtad4&>Xw^!e_^9Pp&tXJmf#l@op34Q$IxfIOdq za(=DSMXKc@(Rn=`_a;v%j#~o)H;Dj0#A(*ayo`E&gzTZ}oCj#Sfw6T0bgkzc4#AX2 z24SqY^oC4!X^_+D zGa1oOXDAJBwtEQz5Z0!!W+Z}4q9iu~Hgud`a2%);d?C6$A}7TW=#z=T_e#bRkAZ^i zRsbHis}|&db{@xM%GQ#eJ1co1-=6|E8fDM-?B{d9kKcJutl#eFJJ8q7w1x9H!df|` zzrp-Mos%Pj7NT^MnW3E_;NHZ&7_)_uu_I-yv96&^b6z2@96@R_k%vW_CcrM^%Jlcl zK7c_f4y4*##BFg05j<~?w=Z8PFT_6H!F-WCffO?4ei~5Wa~t^`MHs9L?7&5-XnJUKkX8VM+^hVip z&QHjkgKR6We?qeD4(ZcLsWbviWc5I?q4ur07Z_&(V$@s^?4Zn9Q$+w@`XhJtv8-3d zdJ`%8f#ShLE~`422NpK`wEmOKZ2Z;$YGKkfn^y^IYg!v5;Q;$vFkTZ$GTcAvxQyQf zd1zZ&zawm&mcgs3UIV2qU7R)S%xko19}Aw#9|q)US>R0UnM@muml2)aTa|Co$&Hj_ zLq3ad^_`9MgnZQJ0C;{5_QDMvB$+Xcn%W7{XeIXn(UZQqM9dyV5MF1m$fQ87&F;-n zU3`j>wT_amGIC@c%k*Qq#DT0|a}>JN+A#X0<(T32lJLLL0hR+rCbzHh8WHjVc82^` z?Z=!ctS7>N!-B~RjAdcg;Qr}g;C3EGC}mi*QW}`p&La5V!g$Gjn*9&0{>mNpLzYHI zOBNc6Fg9m8u>$0I5wNeLpF}+ty=4H^!q&9Nd>+du$7D}x>F#UnH6H-Mt_5v; z=wu@j6wgN-7}2+R(q=i9F>G9t)F)U?3hrl3(sp4x-N)*UNzvN%IpfXiIvohu-(+k~ zI)bcSXS)L^w26e-DUsBu%VbG-!e$h-Ls6ML*Md&FU{EFMdiZUE1MnuIU`<(jkV58#wByM~hpo zRWb$y;1!mm=4)vZ^s3{j$$txAArfS`W{d~on=uFFF7L3P&GiXIr7Hhf7k(F<%4Cv;Qv!LwDtRxekL$Oe;4}|Wmj@sC|eQrf$VQC z1XQ{O*b{`IoyY&EoWlFe>n8{#fGK<*mXgC|IK}Rs1e*A~9rW=a1hFM_z9jiEJCARy zv=!xpC&;t7m_bR0VMOJ=hVyDm>6ywW@>`X6O54Ol=MJhQ^&VTD>r0*YmjHOaRLMLO zEPEAW%+%seEfz_THZ5vPD0^fE?E-@A;`Ci)$?$l-ZZ-GK+Hw6@)`_g>+pi{9)pM_d zR#T{@U%OU%~L9^R^aGu#Z0W zg7c!^6@6T21jBGT%-E#lL<0N*@- zos?pn2-3Kd(I@tbI=>SD;=O~3w2`sHs8|A|!t{JlibkYt1^k9%5@UsE`FB?w@cLqP z8_!Bv5Y~RI{<8h^xk_6ypyM>)0{xKzqm6S>mymQOk8HD?Z&c)rJZGIO>`!crWs!g@ zky?%QDBmGN>1-dr<{`ij9g4#ooeRKWlRn%5-dJeg$uk0r5JkpZkU7 z-;U!>VPTUEPJpsUokhO`8S4ljF~A3`;782q2is`a92u0td`)*=8v#wtQCahF*!PsO z9borP_CJ!7#p|W}a2m>xanJIVc7l3rt9i_0kU3{T| z%Ea1LS~!^Q!OBU1=Z*=8BlP=uXEjr!B5#&kLMcy?rgH-a6i2sUEEnGiP(l1T+L|MlGj5M ztnxUJYWoxpdP(5Q{XLh4Ap9n2by|cji$tkQbg{g1nTVU?lIMl7(aEA9FhUS0{||QJ zw_JzB`R5O2RWe`D2O@Qk=yyLv$NvIJORyR-Fe92v*j|!n41I5vMNpNFxIv&z>Qa-O zRg84r11NosYfm8NUO@_PIq^tYwvZm9tW8hW4jDypeFNZNFm~X4guDyB(}_RGR>9bf zaSRFb>s~~+9OGtyZ_ccGg)}6~CSdeMq6ih-5yxC7$O9mZ`H&{`rLkWXiC{*t2+RW! zwwUw~Tyyyzwh04?=~OZ&Fy(6`CsE_JGJZ9Y++z^S&H59uC5G8L>$0!wGDnh_6pW|u ziYl92!DIV2>&eS$?`m!;XE0Ohgur_YJ53!2l!9lEkeyBBbS*X{CKB3F7a`M4?{)M8*DkIy2k zht>S4!|F)9a|ORBdt{&t>W+e8npeuN+hyN;-aw~p7nZEaZBAERa2a^!CF7VaW9j6! zlH;W@N?x3qB1yayP}NvlAh0iOKMdL>{8SRnu{(SFgs)mM^j zitH=wBhNZY^g%HPC$Kqi_z6X0B^&ZEEjDGoDkQ?{2%WeOUl4#wg3lds)bPhX7N6Lval}x6Kp9zhG$!s*bqd=*i^y5etpG z`Ya&lZ`B#n0s2NzhYdnlDlUmW;w_70tH9uekJ6GH|bM6Wu?gZA+f zCF*wU=M(6Gh!bk!K4g#*Ep<0=@w`+DB0ha!@Vav4&vd|K#9II;Z`I%Z zS|$6rw*Yv4=)gl-IHQmgEQs|xfs6nK1>lv$q|gc$kmr(x+ew@6Y%3;-B{`6$9McVs zx>iaS;vQza(*e1*mtgSh%taLczXl+CCG#m+BqFy{B|x2!NfkEd7RSST(d4v4*gDAL zC3zLu0NtDaW8sJn~Y7yZfIhg0DJ@}g#JU@xl04|RhvehFN!d3tRAlCpqQ4I55JcZ znH%dz#@;oi4*TTVhwNBPqE%WE(X;@HByt_&9*(WRSxTrT+mZWW5>5v#89hV5gdTk* zoly^7zvY#Qc0gN)fYnK6l(3V9fk@4E2O207X|j$U-S5m31-KlX9OmR&0u0t!bj%ys z#m;QbHsClw7N#?COtC?p<=VW*%O-sf?{{*{g7fsF$^+w_D*!mqa&kO1Su5}7oFT-* z^NQnn2s=j=3xvTVY(&;+2mWabSdI>YZR|HiHntOp=JOMl9FMs?4R}!fhP~>7aJGH z)Hws|AVH;WOk?;d_?~tNEP#}|16Wad6l0rGvVIr2?Ga@@bMpL)7dEz_GXA~Fy~;;+ zI-I1e+#mF*Kf`H8egCUC8g~?7JFw9>fFVwtHCc@+at+eq(uv{uWmYxl&5-0aD7r|b zYhQ_ym1AHZStl%@9~ng}#@}Y^Y%(H_5|7|?E$VJg^!m>Kn{~R3gaW`(1@94y82D2h zH<%cs#C~{|mUrXCCni@a19RYclGaX%!~@QfuL1DfdCfms9~AclLlm+%oS;-c8K3`E zpS#s1Cherd-lUjNXIPp6Ni<*$;IV}FmdD82s74ag5Uq&mjJl5T=!MaQweF{oF@23^ zf0KX#P7nsiNid^skeD;cr4E_63)!C%wF!_X&K=GIf-1O&OUg(Jt2RFAxN_&9F&|AU zn*F#*H>6A+9apG}7TteH8^w85`}&mnuExH|SvIjXjKOhK0g%jo&GWsm4WmA6mOl#Y z@QX05`N{E&wq0My>EnP4gKK#GwaD?ioI{qiz!o`{)iM9>?S)P*j+6XO;FI@pn0~&D zS*K0>bK7(zOJ1rWxusHuG375`=p>Qo(kA_~N`^|b=^f7R1O3jF;2Z6;a;$`Ni;gnd zpVOFu*-i;apv~!gg!@U`#lm%LJI?keJ5!>_E!ug)N9cGp`GS%ozbb*!K+tCY}k>U_EWg!R=|UR8iJ?6q94* zew~Us*GJ)_gXKLat4f{+_Vep7 zK+Yxu2J+Z`GD*QiMI$MO$fESk6PDjlE?L+HBtOrdj%0D3b*b9~c_wMzoRC`)piLzb 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≤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

BEW~ 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%0DEQut?$uwd9p%iK&*gQ;xXSy-fVNNy@tfBb%ze%0A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 JrRq<@{tvSp(3b!J diff --git a/apps/desktop/src/assets/illustrations/cloud-3.png b/apps/desktop/src/assets/illustrations/cloud-3.png deleted file mode 100644 index d4b14e4b50fd6548cd72f87bfdebd1a94ccb9786..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 612983 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 z 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

cO2sjfW>#bfm4~wb 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 z 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

SXF}e4Gj`={{GCq-AA1PS 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

M6ZMaq#B=xAM9?BcFE` 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�fW=V}9- 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%0D)seNi$UJDKk*jnKA9M0x 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 j@xF diff --git a/apps/desktop/src/assets/illustrations/cloud-2.png b/apps/desktop/src/assets/illustrations/cloud-2.png deleted file mode 100644 index 962d16953919b1a440cb7f9f09fa979ef2deca9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362822 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�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 z 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