diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d79a2b24..4225b839 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,12 @@ name: Build Electron App on: workflow_dispatch: +permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + jobs: build-windows: runs-on: windows-latest @@ -19,6 +25,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Install app dependencies run: npx electron-builder install-app-deps @@ -28,17 +41,43 @@ jobs: npm run build:platform-native-helpers npx tsc npx vite build - npx electron-builder --win nsis --x64 --publish never + npx electron-builder --win dir nsis --x64 --publish never + + - name: Smoke test packaged Windows paths + run: npm run smoke:packaged-binaries + + - name: Generate Windows artifact checksums + run: npm run checksums:release -- SHA256SUMS-windows.txt + + - name: Attest Windows artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-windows.txt - name: Upload Windows build uses: actions/upload-artifact@v4 with: - name: windows-installer - path: release/**/*.exe + name: windows-x64-release + path: | + release/*.exe + release/*.blockmap + release/latest*.yml + release/SHA256SUMS*.txt + if-no-files-found: error retention-days: 30 build-macos: - runs-on: macos-latest + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + arch_tag: darwin-x64 + runner: macos-15-intel + - arch: arm64 + arch_tag: darwin-arm64 + runner: macos-14 steps: - name: Checkout code uses: actions/checkout@v4 @@ -56,6 +95,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Install app dependencies run: npx electron-builder install-app-deps @@ -64,16 +110,32 @@ jobs: npm run build:platform-native-helpers npx tsc npx vite build - npx electron-builder --mac dmg zip --publish never + npx electron-builder --mac dir dmg zip --${{ matrix.arch }} --publish never + + - name: Smoke test packaged macOS paths + env: + PACKAGED_SMOKE_ARCH_TAGS: ${{ matrix.arch_tag }} + run: npm run smoke:packaged-binaries + + - name: Generate macOS artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-${{ matrix.arch }}.txt + + - name: Attest macOS artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-${{ matrix.arch }}.txt - name: Upload macOS build uses: actions/upload-artifact@v4 with: - name: macos-installer + name: macos-${{ matrix.arch }}-release path: | release/**/*.dmg release/**/*.zip + release/**/*.blockmap release/latest-mac.yml + release/SHA256SUMS*.txt + if-no-files-found: error retention-days: 30 build-linux: @@ -90,6 +152,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Install Linux native build dependencies run: | sudo apt-get update @@ -106,15 +175,27 @@ jobs: npm run build:platform-native-helpers npx tsc npx vite build - npx electron-builder --linux AppImage --x64 --publish never + npx electron-builder --linux dir AppImage --x64 --publish never + + - name: Smoke test packaged Linux paths + run: npm run smoke:packaged-binaries + + - name: Generate Linux artifact checksums + run: npm run checksums:release -- SHA256SUMS-linux.txt + + - name: Attest Linux artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-linux.txt - name: Upload Linux build uses: actions/upload-artifact@v4 with: - name: linux-installer + name: linux-x64-release path: | release/**/*.AppImage + release/**/*.blockmap release/latest-linux.yml + release/SHA256SUMS*.txt + if-no-files-found: error retention-days: 30 - - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd5ec255..5787afbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,9 @@ on: permissions: contents: write actions: write + id-token: write + attestations: write + artifact-metadata: write concurrency: group: release-${{ github.event.release.tag_name || github.event.inputs.tag_name || github.run_id }} @@ -129,6 +132,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Install app dependencies run: npx electron-builder install-app-deps @@ -139,7 +149,20 @@ jobs: run: | npx tsc npx vite build - npx electron-builder --mac dmg zip --x64 --publish never + npx electron-builder --mac dir dmg zip --x64 --publish never + + - name: Smoke test packaged macOS x64 paths + env: + PACKAGED_SMOKE_ARCH_TAGS: darwin-x64 + run: npm run smoke:packaged-binaries + + - name: Generate macOS x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-x64.txt + + - name: Attest macOS x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-x64.txt - name: Upload macOS x64 artifacts uses: actions/upload-artifact@v4 @@ -150,6 +173,7 @@ jobs: release/*.zip release/*.blockmap release/latest-mac.yml + release/SHA256SUMS*.txt if-no-files-found: error build-macos-arm64: @@ -209,6 +233,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Install app dependencies run: npx electron-builder install-app-deps @@ -219,7 +250,20 @@ jobs: run: | npx tsc npx vite build - npx electron-builder --mac dmg zip --arm64 --publish never + npx electron-builder --mac dir dmg zip --arm64 --publish never + + - name: Smoke test packaged macOS arm64 paths + env: + PACKAGED_SMOKE_ARCH_TAGS: darwin-arm64 + run: npm run smoke:packaged-binaries + + - name: Generate macOS arm64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-arm64.txt + + - name: Attest macOS arm64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-arm64.txt - name: Upload macOS arm64 artifacts uses: actions/upload-artifact@v4 @@ -230,6 +274,7 @@ jobs: release/*.zip release/*.blockmap release/latest-mac.yml + release/SHA256SUMS*.txt if-no-files-found: error merge-macos-update-metadata: @@ -347,6 +392,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Remove stale uiohook build output shell: pwsh run: | @@ -363,7 +415,18 @@ jobs: npm run build:platform-native-helpers npx tsc npx vite build - npx electron-builder --win nsis --x64 --publish never + npx electron-builder --win dir nsis --x64 --publish never + + - name: Smoke test packaged Windows x64 paths + run: npm run smoke:packaged-binaries + + - name: Generate Windows x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-windows-x64.txt + + - name: Attest Windows x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-windows-x64.txt - name: Upload Windows x64 artifacts uses: actions/upload-artifact@v4 @@ -373,6 +436,7 @@ jobs: release/*.exe release/*.blockmap release/latest*.yml + release/SHA256SUMS*.txt if-no-files-found: error build-linux-x64: @@ -406,6 +470,13 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + - name: Remove stale uiohook build output run: rm -rf node_modules/uiohook-napi/build @@ -418,7 +489,18 @@ jobs: npm run build:platform-native-helpers npx tsc npx vite build - npx electron-builder --linux AppImage --x64 --publish never + npx electron-builder --linux dir AppImage --x64 --publish never + + - name: Smoke test packaged Linux x64 paths + run: npm run smoke:packaged-binaries + + - name: Generate Linux x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-linux-x64.txt + + - name: Attest Linux x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-linux-x64.txt - name: Upload Linux x64 artifacts uses: actions/upload-artifact@v4 @@ -428,6 +510,7 @@ jobs: release/*.AppImage release/*.blockmap release/latest-linux.yml + release/SHA256SUMS*.txt if-no-files-found: error publish-release-assets: @@ -471,12 +554,13 @@ jobs: name: linux-x64-release path: release-assets/linux-x64 - - name: Upload release assets to GitHub + - name: Stage release assets shell: bash run: | set -euo pipefail shopt -s nullglob + mkdir -p release-assets/upload assets=( release-assets/macos-x64/*.dmg release-assets/macos-x64/*.zip @@ -493,6 +577,43 @@ jobs: release-assets/linux-x64/latest-linux.yml ) + if [ ${#assets[@]} -eq 0 ]; then + echo "No release assets found to checksum." + exit 1 + fi + + for file in "${assets[@]}"; do + target="release-assets/upload/$(basename "$file")" + if [ -e "$target" ]; then + echo "Duplicate release asset name: $(basename "$file")" + exit 1 + fi + cp "$file" "$target" + done + + - name: Generate release asset checksums + shell: bash + run: | + set -euo pipefail + cd release-assets/upload + sha256sum * | sort -k2,2 > SHA256SUMS.txt + sha256sum -c SHA256SUMS.txt + + - name: Attest release assets + uses: actions/attest@v4 + with: + subject-checksums: release-assets/upload/SHA256SUMS.txt + + - name: Upload release assets to GitHub + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + + assets=( + release-assets/upload/* + ) + if [ ${#assets[@]} -eq 0 ]; then echo "No release assets found to upload." exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7f71ae4..947599ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,6 @@ Thank you for considering contributing to this project! By contributing, you help make this project better for everyone. Please take a moment to review these guidelines to ensure a smooth contribution process. Areas where help is especially valuable: - - Export optimisations - Native screen recording for Linux - Wallpaper submissions @@ -17,7 +16,7 @@ Areas where help is especially valuable: 2. **Clone Your Fork** - Clone your forked repository to your local machine: ```bash - git clone https://github.com/your-username/Recordly.git + git clone https://github.com/your-username/Recordly.git ``` 3. **Create a New Branch** diff --git a/RELEASING.md b/RELEASING.md index 7af64644..4ec8e22a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -73,7 +73,20 @@ Optional repository variables: 1. Bump `package.json` to the version you want to ship. 2. Commit and push that version. 3. Create a Git tag in the form `vX.Y.Z`. -4. In GitHub, create and publish a release for that tag. +4. Create and publish a GitHub release for that tag. Prefer the helper so custom notes are prepended while GitHub still generates the contributor section: + +```bash +npm run release:create -- --tag v1.2.3 --title "v1.2.3" --notes-file ./release-notes.md +``` + +For prereleases: + +```bash +npm run release:create -- --tag v1.2.0-beta.2 --title "v1.2.0 beta-2" --prerelease --notes-file ./release-notes.md +``` + +This uses `gh release create --generate-notes`, which keeps GitHub's generated change summary and contributor list instead of replacing it with a fully manual release body. + 5. The `Publish Release` workflow builds, signs, notarizes, uploads, and publishes update metadata. That is the normal path if you want “click new release and let CI do the rest.” @@ -87,4 +100,4 @@ If you need to rerun publishing for an existing tag, use the manual dispatch for - macOS auto-updates require the `zip` target in addition to `dmg`, because `latest-mac.yml` is generated from the zipped build. - macOS arm64 and x64 builds both publish updater zips, and the release workflow merges them into one `latest-mac.yml` so `electron-updater` can choose the correct architecture automatically. - The release workflow publishes versioned artifact names so the generated update metadata matches the uploaded files. -- `build.yml` is intentionally forced to `--publish never` so ad hoc CI builds do not accidentally upload to a draft release. \ No newline at end of file +- `build.yml` is intentionally forced to `--publish never` so ad hoc CI builds do not accidentally upload to a draft release. diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts index e6fa1253..dfc59fbb 100644 --- a/electron/ipc/ffmpeg/filters.ts +++ b/electron/ipc/ffmpeg/filters.ts @@ -1,6 +1,7 @@ import type { AudioSyncAdjustment, PauseSegment } from "../types"; const MAX_AUDIO_SYNC_DELAY_MS = 15000; +export const ATEMPO_FILTER_EPSILON = 0.0005; export function buildAtempoFilters(tempoRatio: number): string[] { if (!Number.isFinite(tempoRatio) || tempoRatio <= 0) { @@ -20,7 +21,7 @@ export function buildAtempoFilters(tempoRatio: number): string[] { remaining /= 2.0; } - if (Math.abs(remaining - 1) > 0.0005) { + if (Math.abs(remaining - 1) > ATEMPO_FILTER_EPSILON) { filters.push(`atempo=${remaining.toFixed(6)}`); } diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts index 855e3c07..6f7fbb4a 100644 --- a/electron/ipc/nativeVideoExport.test.ts +++ b/electron/ipc/nativeVideoExport.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { ATEMPO_FILTER_EPSILON } from "./ffmpeg/filters"; import { buildEditedTrackSourceAudioFilter, buildTrimmedSourceAudioFilter, @@ -20,7 +21,7 @@ describe("buildTrimmedSourceAudioFilter", () => { }); describe("buildEditedTrackSourceAudioFilter", () => { - it("builds a concat filtergraph that pitch-shifts via asetrate for speed changes", () => { + it("builds a concat filtergraph that applies tempo filters for speed changes", () => { const filter = buildEditedTrackSourceAudioFilter( [ { startMs: 0, endMs: 2_000, speed: 1 }, @@ -31,23 +32,49 @@ describe("buildEditedTrackSourceAudioFilter", () => { expect(filter).toBe( "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + - "[1:a]atrim=start=2.000:end=6.000,asetpts=PTS-STARTPTS,asetrate=66150,aresample=44100[edited_audio_1];" + + "[1:a]atrim=start=2.000:end=6.000,asetpts=PTS-STARTPTS,atempo=1.500000[edited_audio_1];" + "[edited_audio_0][edited_audio_1]concat=n=2:v=0:a=1[aout]", ); }); - it("builds a filtergraph for slowdown segments by lowering then resampling the source rate", () => { + it("builds a filtergraph for slowdown segments with a tempo filter", () => { const filter = buildEditedTrackSourceAudioFilter( [{ startMs: 0, endMs: 2_000, speed: 0.5 }], 44_100, ); expect(filter).toBe( - "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS,asetrate=22050,aresample=44100[edited_audio_0];" + + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS,atempo=0.500000[edited_audio_0];" + "[edited_audio_0]anull[aout]", ); }); + it("treats near-unity speed changes as unchanged audio", () => { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: 1.0002 }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + }); + + it("treats exact epsilon speed changes as unchanged audio", () => { + for (const speed of [1 - ATEMPO_FILTER_EPSILON, 1 + ATEMPO_FILTER_EPSILON]) { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + } + }); + it("returns null when the edited-track filtergraph inputs are incomplete", () => { expect(buildEditedTrackSourceAudioFilter([], 44_100)).toBeNull(); expect( diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts index 5ca8fa08..3a8d812b 100644 --- a/electron/ipc/nativeVideoExport.ts +++ b/electron/ipc/nativeVideoExport.ts @@ -1,4 +1,8 @@ +import { ATEMPO_FILTER_EPSILON, buildAtempoFilters } from "./ffmpeg/filters"; + const NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL = 4; +const MIN_EDITED_TRACK_TEMPO_SPEED = 0.5; +const MAX_EDITED_TRACK_TEMPO_SPEED = 2; export type NativeExportEncodingMode = "fast" | "balanced" | "quality"; @@ -207,7 +211,11 @@ export function buildEditedTrackSourceAudioFilter( const label = `edited_audio_${index}`; const speed = segment.speed; - if (!Number.isFinite(speed) || speed <= 0) { + if ( + !Number.isFinite(speed) || + speed < MIN_EDITED_TRACK_TEMPO_SPEED || + speed > MAX_EDITED_TRACK_TEMPO_SPEED + ) { hasInvalidSegment = true; return; } @@ -217,17 +225,12 @@ export function buildEditedTrackSourceAudioFilter( "asetpts=PTS-STARTPTS", ]; - if (Math.abs(speed - 1) > 0.0001) { - const adjustedSampleRate = Math.round(normalizedSourceSampleRate * speed); - if (!Number.isSafeInteger(adjustedSampleRate) || adjustedSampleRate < 1) { - hasInvalidSegment = true; - return; - } - - segmentFilter.push( - `asetrate=${adjustedSampleRate}`, - `aresample=${normalizedSourceSampleRate}`, - ); + const tempoFilters = buildAtempoFilters(speed); + if (tempoFilters.length > 0) { + segmentFilter.push(...tempoFilters); + } else if (Math.abs(speed - 1) > ATEMPO_FILTER_EPSILON) { + hasInvalidSegment = true; + return; } filterParts.push(`${segmentFilter.join(",")}[${label}]`); diff --git a/electron/ipc/recording/ffmpeg.ts b/electron/ipc/recording/ffmpeg.ts index 2d486d16..ab5d436c 100644 --- a/electron/ipc/recording/ffmpeg.ts +++ b/electron/ipc/recording/ffmpeg.ts @@ -16,6 +16,14 @@ export function getDisplayBoundsForSource(source: SelectedSource) { ).bounds; } +export function getDisplayWorkAreaForSource(source: SelectedSource) { + const allDisplays = getScreen().getAllDisplays(); + const primaryDisplay = getScreen().getPrimaryDisplay(); + const { displayId } = resolveWindowsCaptureDisplay(source, allDisplays, primaryDisplay); + const matched = allDisplays.find((d) => d.id === displayId) ?? primaryDisplay; + return matched.workArea; +} + export async function buildFfmpegCaptureArgs(source: SelectedSource, outputPath: string) { const commonOutputArgs = [ "-an", diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts index 43398912..a5e6d601 100644 --- a/electron/ipc/register/sources.ts +++ b/electron/ipc/register/sources.ts @@ -5,7 +5,7 @@ import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants"; import { selectedSource, setSelectedSource } from "../state"; import type { SelectedSource } from "../types"; import { getScreen, parseWindowId } from "../utils"; -import { getDisplayBoundsForSource } from "../recording/ffmpeg"; +import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; import { getNativeMacWindowSources, resolveMacWindowBounds, @@ -337,7 +337,10 @@ export function registerSourceHandlers({ let bounds: { x: number; y: number; width: number; height: number } | null = null; if (source.id?.startsWith("screen:")) { - bounds = getDisplayBoundsForSource(source); + bounds = + process.platform === "darwin" + ? getDisplayWorkAreaForSource(source) + : getDisplayBoundsForSource(source); } else if (isWindow) { if (process.platform === "darwin") { bounds = await resolveMacWindowBounds(source); @@ -363,7 +366,12 @@ export function registerSourceHandlers({ const resolvedBounds = bounds; // ── 3. Show traveling wave highlight ── - const pad = 6; + // On macOS, screen highlights use workArea and no outward padding — + // macOS clamps window positions below the menu bar so outward + // padding only works on the left/top while right/bottom run off-screen. + const isScreen = source.id?.startsWith("screen:"); + const isMacScreen = isScreen && process.platform === "darwin"; + const pad = isMacScreen ? 0 : 6; const highlightWin = new BrowserWindow({ x: resolvedBounds.x - pad, y: resolvedBounds.y - pad, @@ -381,13 +389,18 @@ export function registerSourceHandlers({ highlightWin.setIgnoreMouseEvents(true); + const borderRadius = isMacScreen ? 0 : 10; + const glowInset = isMacScreen ? 0 : -4; + const glowRadius = isMacScreen ? 0 : 14; + const glowPad = isMacScreen ? 3 : 6; + const html = `