Skip to content

Fix cursor duplication and offset on windowed screens#659

Open
dylan-sloan wants to merge 3 commits into
webadderallorg:mainfrom
dylan-sloan:fix/windows-default-cursor-duplication
Open

Fix cursor duplication and offset on windowed screens#659
dylan-sloan wants to merge 3 commits into
webadderallorg:mainfrom
dylan-sloan:fix/windows-default-cursor-duplication

Conversation

@dylan-sloan
Copy link
Copy Markdown

@dylan-sloan dylan-sloan commented Jun 5, 2026

Pull Request Template

Description

I'm proposing these fixes for two distinct bugs, both related to recording windowed applications on Windows machines. This fixes the Windows default cursor showing along with the rendered cursor, and also fixes a mouse cursor telemetry issue that was causing the rendered cursor to show in incorrect positions.

Motivation

When recording windowed screens with any dimensions smaller than fullscreen, the windows default cursor would show along side the rendered cursor. In addition, the rendered cursor would be far off from where it actually was during the recording.

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

#634

Screenshots / Video

Video:

recordly-bugfix.mp4

Testing Guide

  1. On a Windows machine, launch Recordly
  2. Choose a windowed source
  3. Size it down so it's dimensions aren't fullscreen
  4. Record a video where the cursor was moved in the window.
  5. Ensure "Show Cursor" is enabled in Cursor settings
  6. Play video and see a) cursor is properly aligned, and b) there is no duplicated windows-native cursor

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Thank you for contributing!

Summary by CodeRabbit

  • New Features

    • DPI-aware window bounds for more accurate cursor positioning on Windows.
    • Broadened native Windows capture to include window sources.
    • Added Windows bounds retrieval support and packaging so native capture works reliably.
  • Bug Fixes

    • Cursor capture now validates state during startup and avoids invalid samples.
    • Cursor capture sessions are anchored to native video start for better sync.
  • Tests

    • Added tests covering Windows bounds parsing and normalization.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Ready to act? Review this PR in Change Stack to turn feedback into patch suggestions you can inspect and refine.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9157d96-7f55-421c-bf64-092dbea2d1d1

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9b4b3 and cb476e0.

📒 Files selected for processing (1)
  • electron/ipc/register/recording.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • electron/ipc/register/recording.ts

📝 Walkthrough

Walkthrough

Windows window bounds are now retrieved via PowerShell script with DPI awareness and normalized to Electron DIP coordinates. Cursor telemetry is refactored with explicit nullable returns and a centralized session initialization helper. Recording-state management includes native start timestamps and WGC capture sizes. The native recording pipeline coordinates bounds telemetry and cursor capture sessions using the native start time. Cursor presentation logic generalizes across Linux portal and Windows browser capture. Native Windows capture is now available for window sources.

Changes

Windows bounds telemetry and cursor capture session coordination

Layer / File(s) Summary
Windows bounds implementation and build integration
electron/ipc/cursor/get-window-bounds.ps1, electron/ipc/cursor/bounds.ts, vite.config.ts
PowerShell script retrieves DPI-aware window bounds via DWM with Win32 fallback and outputs JSON. Bounds utilities normalize physical coordinates to Electron DIP using display scale factors and align to WGC capture size. Runtime script path resolution includes APP_ROOT and dist-electron fallback. Vite plugin ensures dist-electron directory exists and copies the script to the build output.
Windows bounds utility tests
electron/ipc/cursor/bounds.test.ts
Tests verify parseWgcCaptureSize extracts width/height from log format, normalizeWindowsWindowBoundsToElectronDip scales DPI-based values to DIP coordinates, and alignWindowBoundsToWgcCaptureSize preserves origin while aligning capture dimensions.
Cursor telemetry type safety and session management
electron/ipc/cursor/telemetry.ts
getNormalizedCursorPoint explicitly returns { cx: number; cy: number } | null. Window source normalization includes a Windows path that clamps cursor position to window bounds. sampleCursorPoint skips sampling when normalized point is null. New beginCursorCaptureSession helper encapsulates capture initialization with optional sample reset and immediate first sample.
Recording coordination state
electron/ipc/state.ts
New exported state: nativeVideoRecordingStartedAtMs (recording start timestamp) and selectedWindowsWgcCaptureSize (WGC dimensions). Corresponding setters enable cross-module updates.
Native recording start and cursor session coordination
electron/ipc/register/recording.ts
Windows and macOS native start flows record native video timestamp, parse/apply WGC telemetry bounds for window sources, start window-bounds capture, and begin cursor sessions anchored to native start time. set-recording-state refactored to manage cursor-capture enable/disable around recording toggle, including bounds-readiness waits and native cursor monitor.
Cursor presentation and native Windows capture selection
src/hooks/useScreenRecorder.ts
New exported resolveBrowserCursorPresentation evaluates cursor hiding for both Linux portal and Windows browser capture with isWindowSource parameter for special window-source handling. shouldUseNativeWindowsCaptureForSource expanded to include window: prefixed sources alongside screen: sources for native Windows capture eligibility. startRecording computes cursor presentation for both Linux and Windows platforms.
Cursor presentation and native capture tests
src/hooks/useScreenRecorder.test.ts
Tests verify resolveBrowserCursorPresentation enables overlay only when capture confirms hidden cursor, disables for embedded cursor and window/non-window sources without cursor settings. Native Windows capture tests confirm window sources use native capture.
Interaction capture safety
electron/ipc/cursor/interaction.ts
startInteractionCapture adds early exit if capture disabled during/after uiohook module loading, before validating hook usability.

Sequence Diagram(s)

sequenceDiagram
  participant nativeRecordingStart
  participant applyWindowsWindowTelemetryBounds
  participant beginCursorCaptureSession
  participant startWindowBoundsCapture
  nativeRecordingStart->>nativeRecordingStart: record videoStartedAtMs
  nativeRecordingStart->>nativeRecordingStart: set nativeVideoRecordingStartedAtMs
  alt window source
    nativeRecordingStart->>applyWindowsWindowTelemetryBounds: apply bounds telemetry
    nativeRecordingStart->>startWindowBoundsCapture: start periodic capture
  end
  nativeRecordingStart->>beginCursorCaptureSession: begin with startTimeMs from native
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • webadderallorg/Recordly#485: Both PRs modify electron/ipc/register/recording.ts in the Windows native recording-start flow to handle “window:” vs “screen:” targets.
  • webadderallorg/Recordly#414: Both PRs modify cursor telemetry sampling and session initialization in electron/ipc/cursor/telemetry.ts.
  • webadderallorg/Recordly#596: Both PRs change Windows source selection logic that affects window-bounds/telemetry setup during native recording start.

Poem

🐰 A script on Windows knows where windows lie,
DPI and DIP now waltz beneath the sky,
Bounds align to capture, cursor samples start,
Native time anchors telemetry at heart,
Hop—recording and cursor dance, synced part.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix cursor duplication and offset on windowed screens' directly matches the two main bugs being fixed in the PR as described in the PR description.
Description check ✅ Passed The PR description follows the template structure and includes all critical sections: description of both bugs, clear motivation, correct bug-fix type checkbox, related issue link (#634), a demonstration video, detailed testing guide with 6 steps, and a completed checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dylan-sloan dylan-sloan changed the title Fix/windows default cursor duplication Fix cursor duplication and offset on windowed screens Jun 5, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
electron/ipc/cursor/bounds.ts (1)

315-355: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate x and y coordinates are finite numbers.

Line 347 validates that width and height are positive, but doesn't verify that x and y are finite numbers. If the PowerShell script returns malformed JSON with NaN or Infinity for coordinates, these would pass through and potentially cause errors in downstream calculations.

🛡️ Proposed fix to add coordinate validation
 	const bounds = JSON.parse(trimmedStdout) as WindowsWindowBounds;
-	if (!bounds || bounds.width <= 0 || bounds.height <= 0) {
+	if (
+		!bounds ||
+		!Number.isFinite(bounds.x) ||
+		!Number.isFinite(bounds.y) ||
+		bounds.width <= 0 ||
+		bounds.height <= 0
+	) {
 		return null;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/cursor/bounds.ts` around lines 315 - 355, In
resolveWindowsWindowBounds, after parsing stdout into bounds (the
WindowsWindowBounds object), validate that bounds.x and bounds.y are finite
numeric values in addition to the existing width/height checks: ensure typeof
bounds.x === "number" && Number.isFinite(bounds.x) and similarly for bounds.y
(reject if not), so malformed JSON with NaN/Infinity is treated as invalid and
the function returns null; reference the parsed variable bounds and the function
resolveWindowsWindowBounds.
🧹 Nitpick comments (3)
electron/ipc/cursor/bounds.test.ts (1)

26-74: ⚡ Quick win

Consider expanding test coverage for edge cases.

The current tests validate happy paths effectively, but several edge cases and code paths remain untested:

  • parseWgcCaptureSize with zero, negative, or malformed dimensions
  • normalizeWindowsWindowBoundsToElectronDip when scale factors match (early return path at line 213-220)
  • alignWindowBoundsToWgcCaptureSize with zero or negative scale factor (tests line 240 guard)
  • resolveWindowsWindowTelemetryBounds, applyWindowsWindowTelemetryBounds, and ensureSelectedWindowBoundsReady are not tested

While not blocking for this PR, additional test coverage would improve confidence in the coordinate transformation pipeline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/cursor/bounds.test.ts` around lines 26 - 74, Add unit tests
covering edge cases noted: for parseWgcCaptureSize add cases with zero,
negative, and malformed strings to assert null or proper handling; for
normalizeWindowsWindowBoundsToElectronDip add a test where dpi results in scale
factor 1 to exercise the early-return path in
normalizeWindowsWindowBoundsToElectronDip; for alignWindowBoundsToWgcCaptureSize
add tests for zero and negative scale factors to hit the guard at
alignWindowBoundsToWgcCaptureSize; and add basic tests exercising
resolveWindowsWindowTelemetryBounds, applyWindowsWindowTelemetryBounds, and
ensureSelectedWindowBoundsReady to validate telemetry/ready-state behavior and
coordinate outputs. Ensure each test references the exact function names above
so reviewers can locate the new tests.
electron/ipc/cursor/bounds.ts (2)

28-46: 💤 Low value

Consider adding validation for script existence.

If none of the candidates exist, Line 45 returns a non-existent path, leading to a cryptic ENOENT error when execFile attempts to run the script. While the build process should ensure the script exists, a clearer error message would improve debuggability in edge cases.

🛡️ Optional improvement for error clarity
 	for (const candidate of candidates) {
 		if (existsSync(candidate)) {
 			return candidate;
 		}
 	}
 
+	console.warn("PowerShell script not found at any candidate path, using fallback");
 	return candidates[candidates.length - 1] ?? "get-window-bounds.ps1";
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/cursor/bounds.ts` around lines 28 - 46, The resolver function
resolveWindowsWindowBoundsScriptPath can return a non-existent path causing a
cryptic ENOENT when later executing the script; update
resolveWindowsWindowBoundsScriptPath to validate that a candidate was actually
found and if none exist throw a clear, descriptive Error (including the list of
attempted candidate paths and a hint about expected build output) instead of
returning a fallback string, so callers (e.g., code that calls execFile) fail
fast with a readable message.

294-305: 💤 Low value

Potential redundant Windows bounds resolution.

Lines 295 and 300-305 both invoke the same bounds-resolution logic on Windows without any delay between attempts. refreshSelectedWindowBounds calls resolveWindowsWindowTelemetryBounds, and if that fails to set bounds, applyWindowsWindowTelemetryBounds immediately retries the identical operation. Consider whether the second attempt adds value, or if the retry should only happen in the next loop iteration with the 200ms delay.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/cursor/bounds.ts` around lines 294 - 305, The code redundantly
attempts Windows bounds resolution twice in the same loop iteration:
refreshSelectedWindowBounds (which calls resolveWindowsWindowTelemetryBounds)
then immediately applyWindowsWindowTelemetryBounds; remove the immediate
duplicate by deleting the applyWindowsWindowTelemetryBounds call inside the same
iteration and let the loop retry in the next iteration (or alternatively, if you
must keep it, add an explicit delay before calling
applyWindowsWindowTelemetryBounds). Update the logic around
refreshSelectedWindowBounds, resolveWindowsWindowTelemetryBounds,
applyWindowsWindowTelemetryBounds and checks of
selectedWindowBounds/selectedSource so only one resolution attempt happens per
loop iteration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/register/recording.ts`:
- Around line 576-578: The code sets nativeVideoRecordingStartedAtMs via
setNativeVideoRecordingStartedAtMs(videoStartedAtMs) before attempting a native
start and does not clear it on failure, allowing the timestamp to leak into the
next session; update the native-start paths (including the branch guarded by
captureTarget.kind === "window" and corresponding failure branches around the
native start calls referenced in the diff) to clear the timestamp on any failure
by calling setNativeVideoRecordingStartedAtMs(undefined) (or a reset helper)
whenever native start fails or is aborted so nativeVideoRecordingStartedAtMs is
only retained for successful native starts.

---

Outside diff comments:
In `@electron/ipc/cursor/bounds.ts`:
- Around line 315-355: In resolveWindowsWindowBounds, after parsing stdout into
bounds (the WindowsWindowBounds object), validate that bounds.x and bounds.y are
finite numeric values in addition to the existing width/height checks: ensure
typeof bounds.x === "number" && Number.isFinite(bounds.x) and similarly for
bounds.y (reject if not), so malformed JSON with NaN/Infinity is treated as
invalid and the function returns null; reference the parsed variable bounds and
the function resolveWindowsWindowBounds.

---

Nitpick comments:
In `@electron/ipc/cursor/bounds.test.ts`:
- Around line 26-74: Add unit tests covering edge cases noted: for
parseWgcCaptureSize add cases with zero, negative, and malformed strings to
assert null or proper handling; for normalizeWindowsWindowBoundsToElectronDip
add a test where dpi results in scale factor 1 to exercise the early-return path
in normalizeWindowsWindowBoundsToElectronDip; for
alignWindowBoundsToWgcCaptureSize add tests for zero and negative scale factors
to hit the guard at alignWindowBoundsToWgcCaptureSize; and add basic tests
exercising resolveWindowsWindowTelemetryBounds,
applyWindowsWindowTelemetryBounds, and ensureSelectedWindowBoundsReady to
validate telemetry/ready-state behavior and coordinate outputs. Ensure each test
references the exact function names above so reviewers can locate the new tests.

In `@electron/ipc/cursor/bounds.ts`:
- Around line 28-46: The resolver function resolveWindowsWindowBoundsScriptPath
can return a non-existent path causing a cryptic ENOENT when later executing the
script; update resolveWindowsWindowBoundsScriptPath to validate that a candidate
was actually found and if none exist throw a clear, descriptive Error (including
the list of attempted candidate paths and a hint about expected build output)
instead of returning a fallback string, so callers (e.g., code that calls
execFile) fail fast with a readable message.
- Around line 294-305: The code redundantly attempts Windows bounds resolution
twice in the same loop iteration: refreshSelectedWindowBounds (which calls
resolveWindowsWindowTelemetryBounds) then immediately
applyWindowsWindowTelemetryBounds; remove the immediate duplicate by deleting
the applyWindowsWindowTelemetryBounds call inside the same iteration and let the
loop retry in the next iteration (or alternatively, if you must keep it, add an
explicit delay before calling applyWindowsWindowTelemetryBounds). Update the
logic around refreshSelectedWindowBounds, resolveWindowsWindowTelemetryBounds,
applyWindowsWindowTelemetryBounds and checks of
selectedWindowBounds/selectedSource so only one resolution attempt happens per
loop iteration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 32c85754-627e-4724-bcc8-5258ceb794ca

📥 Commits

Reviewing files that changed from the base of the PR and between 375620b and 8a9b4b3.

📒 Files selected for processing (10)
  • electron/ipc/cursor/bounds.test.ts
  • electron/ipc/cursor/bounds.ts
  • electron/ipc/cursor/get-window-bounds.ps1
  • electron/ipc/cursor/interaction.ts
  • electron/ipc/cursor/telemetry.ts
  • electron/ipc/register/recording.ts
  • electron/ipc/state.ts
  • src/hooks/useScreenRecorder.test.ts
  • src/hooks/useScreenRecorder.ts
  • vite.config.ts
💤 Files with no reviewable changes (1)
  • electron/ipc/cursor/interaction.ts

Comment thread electron/ipc/register/recording.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant