Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion electron/appPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ if (process.env["VITE_DEV_SERVER_URL"]) {
}

export const USER_DATA_PATH = app.getPath("userData");
export const RECORDINGS_DIR = path.join(USER_DATA_PATH, "recordings");
export const RECORDINGS_DIR = path.join(USER_DATA_PATH, "recordings");
149 changes: 76 additions & 73 deletions electron/cursorHider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawnSync } from 'node:child_process'
import { spawnSync } from "node:child_process";

const PY_HIDE_WIN = `
import ctypes, sys
Expand All @@ -25,7 +25,7 @@ for _ in range(32):
user32.ShowCursor(False)

sys.exit(0)
`.trim()
`.trim();

const PY_SHOW_WIN = `
import ctypes, sys
Expand All @@ -52,90 +52,93 @@ for _ in range(32):
user32.ShowCursor(True)

sys.exit(0)
`.trim()
`.trim();

function getPowerShellCommand(show: boolean) {
const desiredFlag = show ? 1 : 0
const showLiteral = show ? '$true' : '$false'

return [
'$signature = @"',
'using System;',
'using System.Runtime.InteropServices;',
'public struct POINT { public int X; public int Y; }',
'public struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT ptScreenPos; }',
'public static class CursorNative {',
' [DllImport("user32.dll")] public static extern int ShowCursor(bool show);',
' [DllImport("user32.dll")] public static extern bool GetCursorInfo(ref CURSORINFO info);',
'}',
'"@;',
'Add-Type -TypeDefinition $signature -Language CSharp -ErrorAction SilentlyContinue | Out-Null;',
'$info = New-Object CURSORINFO;',
'$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf([type]CURSORINFO);',
'for ($i = 0; $i -lt 32; $i++) {',
' if ([CursorNative]::GetCursorInfo([ref]$info) -and (($info.flags -band 1) -eq ' + desiredFlag + ')) { exit 0 }',
' [CursorNative]::ShowCursor(' + showLiteral + ') | Out-Null;',
'}',
'exit 0',
].join(' ')
const desiredFlag = show ? 1 : 0;
const showLiteral = show ? "$true" : "$false";

return [
'$signature = @"',
"using System;",
"using System.Runtime.InteropServices;",
"public struct POINT { public int X; public int Y; }",
"public struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT ptScreenPos; }",
"public static class CursorNative {",
' [DllImport("user32.dll")] public static extern int ShowCursor(bool show);',
' [DllImport("user32.dll")] public static extern bool GetCursorInfo(ref CURSORINFO info);',
"}",
'"@;',
"Add-Type -TypeDefinition $signature -Language CSharp -ErrorAction SilentlyContinue | Out-Null;",
"$info = New-Object CURSORINFO;",
"$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf([type]CURSORINFO);",
"for ($i = 0; $i -lt 32; $i++) {",
" if ([CursorNative]::GetCursorInfo([ref]$info) -and (($info.flags -band 1) -eq " +
desiredFlag +
")) { exit 0 }",
" [CursorNative]::ShowCursor(" + showLiteral + ") | Out-Null;",
"}",
"exit 0",
].join(" ");
}

function runPythonSnippet(code: string) {
for (const executable of ['python', 'python3', 'py']) {
const result = spawnSync(executable, ['-c', code], { timeout: 5000 })
if (!result.error && result.status === 0) {
return true
}
}

return false
for (const executable of ["python", "python3", "py"]) {
const result = spawnSync(executable, ["-c", code], { timeout: 5000 });
if (!result.error && result.status === 0) {
return true;
}
}

return false;
}

function runPowerShellSnippet(command: string) {
const result = spawnSync(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', command],
{ timeout: 8000 },
)
const result = spawnSync(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", command],
{ timeout: 8000 },
);

return !result.error && result.status === 0
return !result.error && result.status === 0;
}

let cursorHidden = false
let cursorHidden = false;

export function hideCursor() {
if (process.platform !== 'win32' || cursorHidden) {
return false
}

try {
const didHide = runPythonSnippet(PY_HIDE_WIN)
|| runPowerShellSnippet(getPowerShellCommand(false))

if (didHide) {
cursorHidden = true
}

return didHide
} catch (error) {
console.error('[cursorHider] Failed to hide Windows cursor:', error)
return false
}
if (process.platform !== "win32" || cursorHidden) {
return false;
}

try {
const didHide =
runPythonSnippet(PY_HIDE_WIN) || runPowerShellSnippet(getPowerShellCommand(false));

if (didHide) {
cursorHidden = true;
}

return didHide;
} catch (error) {
console.error("[cursorHider] Failed to hide Windows cursor:", error);
return false;
}
}

export function showCursor() {
if (process.platform !== 'win32' || !cursorHidden) {
return false
}

try {
const didShow = runPythonSnippet(PY_SHOW_WIN)
|| runPowerShellSnippet(getPowerShellCommand(true))
return didShow
} catch (error) {
console.error('[cursorHider] Failed to show Windows cursor:', error)
return false
} finally {
cursorHidden = false
}
}
if (process.platform !== "win32" || !cursorHidden) {
return false;
}

try {
const didShow =
runPythonSnippet(PY_SHOW_WIN) || runPowerShellSnippet(getPowerShellCommand(true));
if (didShow) {
cursorHidden = false;
}
return didShow;
} catch (error) {
console.error("[cursorHider] Failed to show Windows cursor:", error);
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
45 changes: 29 additions & 16 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="vite-plugin-electron/electron-env" />

// biome-ignore lint/style/noNamespace: NodeJS.ProcessEnv augmentation requires a namespace declaration.
declare namespace NodeJS {
interface ProcessEnv {
/**
Expand Down Expand Up @@ -60,6 +61,14 @@ interface UpdateStatusSummary {
detail?: string;
}

type RendererExtensionInfo = import("./extensions/extensionTypes").ExtensionInfo;
type RendererExtensionReview = import("./extensions/extensionTypes").ExtensionReview;
type RendererMarketplaceExtension = import("./extensions/extensionTypes").MarketplaceExtension;
type RendererMarketplaceReviewStatus =
import("./extensions/extensionTypes").MarketplaceReviewStatus;
type RendererMarketplaceSearchResult =
import("./extensions/extensionTypes").MarketplaceSearchResult;

interface Window {
electronAPI: {
hudOverlaySetIgnoreMouse: (ignore: boolean) => void;
Expand All @@ -73,15 +82,18 @@ interface Window {
setHudOverlayCaptureProtection: (
enabled: boolean,
) => Promise<{ success: boolean; enabled: boolean }>;
getAssetBasePath: () => Promise<string | null>;
getSources: (opts: Electron.SourcesOptions) => Promise<ProcessedDesktopSource[]>;
switchToEditor: () => Promise<void>;
openSourceSelector: () => Promise<void>;
selectSource: (source: any) => Promise<any>;
showSourceHighlight: (source: any) => Promise<{ success: boolean }>;
getSelectedSource: () => Promise<any>;
onSelectedSourceChanged: (callback: (source: any) => void) => () => void;
selectSource: (source: ProcessedDesktopSource) => Promise<ProcessedDesktopSource>;
showSourceHighlight: (source: ProcessedDesktopSource) => Promise<{ success: boolean }>;
getSelectedSource: () => Promise<ProcessedDesktopSource | null>;
onSelectedSourceChanged: (
callback: (source: ProcessedDesktopSource | null) => void,
) => () => void;
startNativeScreenRecording: (
source: any,
source: ProcessedDesktopSource,
options?: {
capturesSystemAudio?: boolean;
capturesMicrophone?: boolean;
Expand Down Expand Up @@ -123,7 +135,7 @@ interface Window {
error?: string;
}>;
startFfmpegRecording: (
source: any,
source: ProcessedDesktopSource,
) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>;
stopFfmpegRecording: () => Promise<{
success: boolean;
Expand All @@ -145,7 +157,6 @@ interface Window {
files?: string[];
error?: string;
}>;
getAssetBasePath: () => Promise<string | null>;
readLocalFile: (
filePath: string,
) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>;
Expand All @@ -158,6 +169,7 @@ interface Window {
frameRate: number;
bitrate: number;
encodingMode: "fast" | "balanced" | "quality";
inputMode?: "rawvideo" | "h264-stream";
}) => Promise<{
success: boolean;
sessionId?: string;
Expand Down Expand Up @@ -459,14 +471,14 @@ interface Window {
cancelCountdown: () => Promise<{ success: boolean }>;
getActiveCountdown: () => Promise<{ success: boolean; seconds: number | null }>;
onCountdownTick: (callback: (seconds: number) => void) => () => void;
extensionsDiscover: () => Promise<any[]>;
extensionsList: () => Promise<any[]>;
extensionsGet: (id: string) => Promise<any | null>;
extensionsDiscover: () => Promise<RendererExtensionInfo[]>;
extensionsList: () => Promise<RendererExtensionInfo[]>;
extensionsGet: (id: string) => Promise<RendererExtensionInfo | null>;
extensionsEnable: (id: string) => Promise<{ success: boolean; error?: string }>;
extensionsDisable: (id: string) => Promise<{ success: boolean; error?: string }>;
extensionsInstallFromFolder: () => Promise<{
success: boolean;
extension?: any;
extension?: RendererExtensionInfo;
message?: string;
error?: string;
canceled?: boolean;
Expand All @@ -480,8 +492,8 @@ interface Window {
sort?: string;
page?: number;
pageSize?: number;
}) => Promise<any>;
extensionsMarketplaceGet: (id: string) => Promise<any | null>;
}) => Promise<RendererMarketplaceSearchResult & { error?: string }>;
extensionsMarketplaceGet: (id: string) => Promise<RendererMarketplaceExtension | null>;
extensionsMarketplaceInstall: (
extensionId: string,
downloadUrl: string,
Expand All @@ -490,13 +502,13 @@ interface Window {
extensionId: string,
) => Promise<{ success: boolean; reviewId?: string; error?: string }>;
extensionsReviewsList: (params: {
status?: string;
status?: RendererMarketplaceReviewStatus;
page?: number;
pageSize?: number;
}) => Promise<{ reviews: any[]; total: number }>;
}) => Promise<{ reviews: RendererExtensionReview[]; total: number; error?: string }>;
extensionsReviewUpdate: (
reviewId: string,
status: string,
status: RendererMarketplaceReviewStatus,
notes?: string,
) => Promise<{ success: boolean; error?: string }>;
};
Expand All @@ -518,6 +530,7 @@ interface CursorTelemetryPoint {
timeMs: number;
cx: number;
cy: number;
pressure?: number;
interactionType?:
| "move"
| "click"
Expand Down
3 changes: 3 additions & 0 deletions electron/extensions/errorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
13 changes: 5 additions & 8 deletions electron/extensions/extensionIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
submitExtensionForReview,
updateReviewStatus,
} from "./extensionMarketplace";
import { getErrorMessage } from "./errorUtils";
import type { ExtensionInfo, MarketplaceReviewStatus } from "./extensionTypes";

/**
Expand Down Expand Up @@ -129,13 +130,13 @@ export function registerExtensionIpcHandlers(): void {
) => {
try {
return await searchMarketplace(params);
} catch (err: unknown) {
} catch (error: unknown) {
return {
extensions: [],
total: 0,
page: 1,
pageSize: 20,
error: err instanceof Error ? err.message : String(err),
error: getErrorMessage(error),
};
}
},
Expand Down Expand Up @@ -174,12 +175,8 @@ export function registerExtensionIpcHandlers(): void {
) => {
try {
return await fetchPendingReviews(params);
} catch (err: unknown) {
return {
reviews: [],
total: 0,
error: err instanceof Error ? err.message : String(err),
};
} catch (error: unknown) {
return { reviews: [], total: 0, error: getErrorMessage(error) };
}
},
);
Expand Down
Loading