Files
Recordly/electron/ipc/utils.ts

132 lines
3.7 KiB
TypeScript

import fs from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { app } from "electron";
import { RECORDINGS_DIR } from "../appPaths";
import { AUTO_RECORDING_PREFIX, RECORDINGS_SETTINGS_FILE } from "./constants";
import {
approvedLocalReadPaths,
customRecordingsDir,
recordingsDirLoaded,
setCustomRecordingsDir,
setRecordingsDirLoaded,
} from "./state";
const nodeRequire = createRequire(import.meta.url);
export function getScreen() {
if (!app.isReady()) {
throw new Error(
"getScreen() called before app is ready. Ensure all screen access happens after app.whenReady().",
);
}
return nodeRequire("electron").screen as typeof import("electron").screen;
}
export function normalizePath(filePath: string) {
return path.resolve(filePath);
}
export function normalizeVideoSourcePath(videoPath?: string | null): string | null {
if (typeof videoPath !== "string") {
return null;
}
const trimmed = videoPath.trim();
if (!trimmed) {
return null;
}
if (/^file:\/\//i.test(trimmed)) {
try {
return fileURLToPath(trimmed);
} catch {
// Fall through and keep best-effort string path below.
}
}
return trimmed;
}
export function stripJsonByteOrderMark(content: string) {
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
}
export function parseJsonWithByteOrderMark<T = unknown>(content: string): T {
return JSON.parse(stripJsonByteOrderMark(content)) as T;
}
export function parseWindowId(sourceId?: string) {
if (!sourceId) return null;
const match = sourceId.match(/^window:(\d+)/);
return match ? Number.parseInt(match[1], 10) : null;
}
export function getTelemetryPathForVideo(videoPath: string) {
return `${videoPath}.cursor.json`;
}
export function isAutoRecordingPath(filePath: string) {
return path.basename(filePath).startsWith(AUTO_RECORDING_PREFIX);
}
export async function moveFileWithOverwrite(sourcePath: string, destinationPath: string) {
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
await fs.rm(destinationPath, { force: true });
try {
await fs.rename(sourcePath, destinationPath);
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== "EXDEV") {
throw error;
}
await fs.copyFile(sourcePath, destinationPath);
await fs.unlink(sourcePath);
}
}
async function loadRecordingsDirectorySetting() {
if (recordingsDirLoaded) {
return;
}
setRecordingsDirLoaded(true);
try {
const content = await fs.readFile(RECORDINGS_SETTINGS_FILE, "utf-8");
const parsed = parseJsonWithByteOrderMark<{ recordingsDir?: unknown }>(content);
if (typeof parsed.recordingsDir === "string" && parsed.recordingsDir.trim()) {
setCustomRecordingsDir(path.resolve(parsed.recordingsDir));
}
} catch {
setCustomRecordingsDir(null);
}
}
export async function getRecordingsDir() {
await loadRecordingsDirectorySetting();
const targetDir = customRecordingsDir ?? RECORDINGS_DIR;
await fs.mkdir(targetDir, { recursive: true });
return targetDir;
}
export function getMacPrivacySettingsUrl(pane: "screen" | "accessibility" | "microphone"): string {
if (pane === "screen")
return "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
if (pane === "microphone")
return "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone";
return "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
}
export function approveUserPath(filePath: string | null | undefined): void {
if (!filePath) return;
try {
approvedLocalReadPaths.add(path.resolve(filePath));
} catch {
// Ignore invalid paths; later reads will surface the underlying error.
}
}