680 lines
20 KiB
TypeScript
680 lines
20 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { app, BrowserWindow, desktopCapturer, dialog, ipcMain, screen, shell } from "electron";
|
|
import {
|
|
normalizeProjectMedia,
|
|
normalizeRecordingSession,
|
|
type RecordingSession,
|
|
type StoreRecordedSessionInput,
|
|
} from "../../src/lib/recordingSession";
|
|
import { RECORDINGS_DIR } from "../main";
|
|
|
|
const PROJECT_FILE_EXTENSION = "openscreen";
|
|
const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json");
|
|
const RECORDING_SESSION_SUFFIX = ".session.json";
|
|
|
|
type SelectedSource = {
|
|
name: string;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
let selectedSource: SelectedSource | null = null;
|
|
let currentProjectPath: string | null = null;
|
|
let currentRecordingSession: RecordingSession | null = null;
|
|
|
|
function normalizePath(filePath: string) {
|
|
return path.resolve(filePath);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function isTrustedProjectPath(filePath?: string | null) {
|
|
if (!filePath || !currentProjectPath) {
|
|
return false;
|
|
}
|
|
return normalizePath(filePath) === normalizePath(currentProjectPath);
|
|
}
|
|
|
|
function setCurrentRecordingSessionState(session: RecordingSession | null) {
|
|
currentRecordingSession = session;
|
|
}
|
|
|
|
async function storeRecordedSessionFiles(payload: StoreRecordedSessionInput) {
|
|
const createdAt =
|
|
typeof payload.createdAt === "number" && Number.isFinite(payload.createdAt)
|
|
? payload.createdAt
|
|
: Date.now();
|
|
const screenVideoPath = path.join(RECORDINGS_DIR, payload.screen.fileName);
|
|
await fs.writeFile(screenVideoPath, Buffer.from(payload.screen.videoData));
|
|
|
|
let webcamVideoPath: string | undefined;
|
|
if (payload.webcam) {
|
|
webcamVideoPath = path.join(RECORDINGS_DIR, payload.webcam.fileName);
|
|
await fs.writeFile(webcamVideoPath, Buffer.from(payload.webcam.videoData));
|
|
}
|
|
|
|
const session: RecordingSession = webcamVideoPath
|
|
? { screenVideoPath, webcamVideoPath, createdAt }
|
|
: { screenVideoPath, createdAt };
|
|
setCurrentRecordingSessionState(session);
|
|
currentProjectPath = null;
|
|
|
|
const telemetryPath = `${screenVideoPath}.cursor.json`;
|
|
if (pendingCursorSamples.length > 0) {
|
|
await fs.writeFile(
|
|
telemetryPath,
|
|
JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples }, null, 2),
|
|
"utf-8",
|
|
);
|
|
}
|
|
pendingCursorSamples = [];
|
|
|
|
const sessionManifestPath = path.join(
|
|
RECORDINGS_DIR,
|
|
`${path.parse(payload.screen.fileName).name}${RECORDING_SESSION_SUFFIX}`,
|
|
);
|
|
await fs.writeFile(sessionManifestPath, JSON.stringify(session, null, 2), "utf-8");
|
|
|
|
return {
|
|
success: true,
|
|
path: screenVideoPath,
|
|
session,
|
|
message: "Recording session stored successfully",
|
|
};
|
|
}
|
|
|
|
const CURSOR_TELEMETRY_VERSION = 1;
|
|
const CURSOR_SAMPLE_INTERVAL_MS = 100;
|
|
const MAX_CURSOR_SAMPLES = 60 * 60 * 10; // 1 hour @ 10Hz
|
|
|
|
interface CursorTelemetryPoint {
|
|
timeMs: number;
|
|
cx: number;
|
|
cy: number;
|
|
}
|
|
|
|
let cursorCaptureInterval: NodeJS.Timeout | null = null;
|
|
let cursorCaptureStartTimeMs = 0;
|
|
let activeCursorSamples: CursorTelemetryPoint[] = [];
|
|
let pendingCursorSamples: CursorTelemetryPoint[] = [];
|
|
|
|
function clamp(value: number, min: number, max: number) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
|
|
function stopCursorCapture() {
|
|
if (cursorCaptureInterval) {
|
|
clearInterval(cursorCaptureInterval);
|
|
cursorCaptureInterval = null;
|
|
}
|
|
}
|
|
|
|
function sampleCursorPoint() {
|
|
const cursor = screen.getCursorScreenPoint();
|
|
const sourceDisplayId = Number(selectedSource?.display_id);
|
|
const sourceDisplay = Number.isFinite(sourceDisplayId)
|
|
? (screen.getAllDisplays().find((display) => display.id === sourceDisplayId) ?? null)
|
|
: null;
|
|
const display = sourceDisplay ?? screen.getDisplayNearestPoint(cursor);
|
|
const bounds = display.bounds;
|
|
const width = Math.max(1, bounds.width);
|
|
const height = Math.max(1, bounds.height);
|
|
|
|
const cx = clamp((cursor.x - bounds.x) / width, 0, 1);
|
|
const cy = clamp((cursor.y - bounds.y) / height, 0, 1);
|
|
|
|
activeCursorSamples.push({
|
|
timeMs: Math.max(0, Date.now() - cursorCaptureStartTimeMs),
|
|
cx,
|
|
cy,
|
|
});
|
|
|
|
if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) {
|
|
activeCursorSamples.shift();
|
|
}
|
|
}
|
|
|
|
export function registerIpcHandlers(
|
|
createEditorWindow: () => void,
|
|
createSourceSelectorWindow: () => BrowserWindow,
|
|
getMainWindow: () => BrowserWindow | null,
|
|
getSourceSelectorWindow: () => BrowserWindow | null,
|
|
onRecordingStateChange?: (recording: boolean, sourceName: string) => void,
|
|
) {
|
|
ipcMain.handle("get-sources", async (_, opts) => {
|
|
const sources = await desktopCapturer.getSources(opts);
|
|
return sources.map((source) => ({
|
|
id: source.id,
|
|
name: source.name,
|
|
display_id: source.display_id,
|
|
thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null,
|
|
appIcon: source.appIcon ? source.appIcon.toDataURL() : null,
|
|
}));
|
|
});
|
|
|
|
ipcMain.handle("select-source", (_, source: SelectedSource) => {
|
|
selectedSource = source;
|
|
const sourceSelectorWin = getSourceSelectorWindow();
|
|
if (sourceSelectorWin) {
|
|
sourceSelectorWin.close();
|
|
}
|
|
return selectedSource;
|
|
});
|
|
|
|
ipcMain.handle("get-selected-source", () => {
|
|
return selectedSource;
|
|
});
|
|
|
|
ipcMain.handle("open-source-selector", () => {
|
|
const sourceSelectorWin = getSourceSelectorWindow();
|
|
if (sourceSelectorWin) {
|
|
sourceSelectorWin.focus();
|
|
return;
|
|
}
|
|
createSourceSelectorWindow();
|
|
});
|
|
|
|
ipcMain.handle("switch-to-editor", () => {
|
|
const mainWin = getMainWindow();
|
|
if (mainWin) {
|
|
mainWin.close();
|
|
}
|
|
createEditorWindow();
|
|
});
|
|
|
|
ipcMain.handle("store-recorded-session", async (_, payload: StoreRecordedSessionInput) => {
|
|
try {
|
|
return await storeRecordedSessionFiles(payload);
|
|
} catch (error) {
|
|
console.error("Failed to store recording session:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to store recording session",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("store-recorded-video", async (_, videoData: ArrayBuffer, fileName: string) => {
|
|
try {
|
|
return await storeRecordedSessionFiles({
|
|
screen: { videoData, fileName },
|
|
createdAt: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to store recorded video:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to store recorded video",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("get-recorded-video-path", async () => {
|
|
try {
|
|
if (currentRecordingSession?.screenVideoPath) {
|
|
return { success: true, path: currentRecordingSession.screenVideoPath };
|
|
}
|
|
|
|
const files = await fs.readdir(RECORDINGS_DIR);
|
|
const videoFiles = files.filter(
|
|
(file) => file.endsWith(".webm") && !file.endsWith("-webcam.webm"),
|
|
);
|
|
|
|
if (videoFiles.length === 0) {
|
|
return { success: false, message: "No recorded video found" };
|
|
}
|
|
|
|
const latestVideo = videoFiles.sort().reverse()[0];
|
|
const videoPath = path.join(RECORDINGS_DIR, latestVideo);
|
|
|
|
return { success: true, path: videoPath };
|
|
} catch (error) {
|
|
console.error("Failed to get video path:", error);
|
|
return { success: false, message: "Failed to get video path", error: String(error) };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("read-binary-file", async (_, inputPath: string) => {
|
|
try {
|
|
const normalizedPath = normalizeVideoSourcePath(inputPath);
|
|
if (!normalizedPath) {
|
|
return { success: false, message: "Invalid file path" };
|
|
}
|
|
|
|
const data = await fs.readFile(normalizedPath);
|
|
return {
|
|
success: true,
|
|
data: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
|
|
path: normalizedPath,
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to read binary file:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to read binary file",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("set-recording-state", (_, recording: boolean) => {
|
|
if (recording) {
|
|
stopCursorCapture();
|
|
activeCursorSamples = [];
|
|
pendingCursorSamples = [];
|
|
cursorCaptureStartTimeMs = Date.now();
|
|
sampleCursorPoint();
|
|
cursorCaptureInterval = setInterval(sampleCursorPoint, CURSOR_SAMPLE_INTERVAL_MS);
|
|
} else {
|
|
stopCursorCapture();
|
|
pendingCursorSamples = [...activeCursorSamples];
|
|
activeCursorSamples = [];
|
|
}
|
|
|
|
const source = selectedSource || { name: "Screen" };
|
|
if (onRecordingStateChange) {
|
|
onRecordingStateChange(recording, source.name);
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
|
|
const targetVideoPath = normalizeVideoSourcePath(
|
|
videoPath ?? currentRecordingSession?.screenVideoPath,
|
|
);
|
|
if (!targetVideoPath) {
|
|
return { success: true, samples: [] };
|
|
}
|
|
|
|
const telemetryPath = `${targetVideoPath}.cursor.json`;
|
|
try {
|
|
const content = await fs.readFile(telemetryPath, "utf-8");
|
|
const parsed = JSON.parse(content);
|
|
const rawSamples = Array.isArray(parsed)
|
|
? parsed
|
|
: Array.isArray(parsed?.samples)
|
|
? parsed.samples
|
|
: [];
|
|
|
|
const samples: CursorTelemetryPoint[] = rawSamples
|
|
.filter((sample: unknown) => Boolean(sample && typeof sample === "object"))
|
|
.map((sample: unknown) => {
|
|
const point = sample as Partial<CursorTelemetryPoint>;
|
|
return {
|
|
timeMs:
|
|
typeof point.timeMs === "number" && Number.isFinite(point.timeMs)
|
|
? Math.max(0, point.timeMs)
|
|
: 0,
|
|
cx:
|
|
typeof point.cx === "number" && Number.isFinite(point.cx)
|
|
? clamp(point.cx, 0, 1)
|
|
: 0.5,
|
|
cy:
|
|
typeof point.cy === "number" && Number.isFinite(point.cy)
|
|
? clamp(point.cy, 0, 1)
|
|
: 0.5,
|
|
};
|
|
})
|
|
.sort((a: CursorTelemetryPoint, b: CursorTelemetryPoint) => a.timeMs - b.timeMs);
|
|
|
|
return { success: true, samples };
|
|
} catch (error) {
|
|
const nodeError = error as NodeJS.ErrnoException;
|
|
if (nodeError.code === "ENOENT") {
|
|
return { success: true, samples: [] };
|
|
}
|
|
console.error("Failed to load cursor telemetry:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to load cursor telemetry",
|
|
error: String(error),
|
|
samples: [],
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("open-external-url", async (_, url: string) => {
|
|
try {
|
|
await shell.openExternal(url);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error("Failed to open URL:", error);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
});
|
|
|
|
// Return base path for assets so renderer can resolve file:// paths in production
|
|
ipcMain.handle("get-asset-base-path", () => {
|
|
try {
|
|
if (app.isPackaged) {
|
|
const assetPath = path.join(process.resourcesPath, "assets");
|
|
return pathToFileURL(`${assetPath}${path.sep}`).toString();
|
|
}
|
|
const assetPath = path.join(app.getAppPath(), "public", "assets");
|
|
return pathToFileURL(`${assetPath}${path.sep}`).toString();
|
|
} catch (err) {
|
|
console.error("Failed to resolve asset base path:", err);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("save-exported-video", async (_, videoData: ArrayBuffer, fileName: string) => {
|
|
try {
|
|
// Determine file type from extension
|
|
const isGif = fileName.toLowerCase().endsWith(".gif");
|
|
const filters = isGif
|
|
? [{ name: "GIF Image", extensions: ["gif"] }]
|
|
: [{ name: "MP4 Video", extensions: ["mp4"] }];
|
|
|
|
const result = await dialog.showSaveDialog({
|
|
title: isGif ? "Save Exported GIF" : "Save Exported Video",
|
|
defaultPath: path.join(app.getPath("downloads"), fileName),
|
|
filters,
|
|
properties: ["createDirectory", "showOverwriteConfirmation"],
|
|
});
|
|
|
|
if (result.canceled || !result.filePath) {
|
|
return {
|
|
success: false,
|
|
canceled: true,
|
|
message: "Export canceled",
|
|
};
|
|
}
|
|
|
|
await fs.writeFile(result.filePath, Buffer.from(videoData));
|
|
|
|
return {
|
|
success: true,
|
|
path: result.filePath,
|
|
message: "Video exported successfully",
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to save exported video:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to save exported video",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("open-video-file-picker", async () => {
|
|
try {
|
|
const result = await dialog.showOpenDialog({
|
|
title: "Select Video File",
|
|
defaultPath: RECORDINGS_DIR,
|
|
filters: [
|
|
{ name: "Video Files", extensions: ["webm", "mp4", "mov", "avi", "mkv"] },
|
|
{ name: "All Files", extensions: ["*"] },
|
|
],
|
|
properties: ["openFile"],
|
|
});
|
|
|
|
if (result.canceled || result.filePaths.length === 0) {
|
|
return { success: false, canceled: true };
|
|
}
|
|
|
|
currentProjectPath = null;
|
|
return {
|
|
success: true,
|
|
path: result.filePaths[0],
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to open file picker:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to open file picker",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("reveal-in-folder", async (_, filePath: string) => {
|
|
try {
|
|
// shell.showItemInFolder doesn't return a value, it throws on error
|
|
shell.showItemInFolder(filePath);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error(`Error revealing item in folder: ${filePath}`, error);
|
|
// Fallback to open the directory if revealing the item fails
|
|
// This might happen if the file was moved or deleted after export,
|
|
// or if the path is somehow invalid for showItemInFolder
|
|
try {
|
|
const openPathResult = await shell.openPath(path.dirname(filePath));
|
|
if (openPathResult) {
|
|
// openPath returned an error message
|
|
return { success: false, error: openPathResult };
|
|
}
|
|
return { success: true, message: "Could not reveal item, but opened directory." };
|
|
} catch (openError) {
|
|
console.error(`Error opening directory: ${path.dirname(filePath)}`, openError);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(
|
|
"save-project-file",
|
|
async (_, projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
|
try {
|
|
const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath)
|
|
? existingProjectPath
|
|
: null;
|
|
|
|
if (trustedExistingProjectPath) {
|
|
await fs.writeFile(
|
|
trustedExistingProjectPath,
|
|
JSON.stringify(projectData, null, 2),
|
|
"utf-8",
|
|
);
|
|
currentProjectPath = trustedExistingProjectPath;
|
|
return {
|
|
success: true,
|
|
path: trustedExistingProjectPath,
|
|
message: "Project saved successfully",
|
|
};
|
|
}
|
|
|
|
const safeName = (suggestedName || `project-${Date.now()}`).replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
const defaultName = safeName.endsWith(`.${PROJECT_FILE_EXTENSION}`)
|
|
? safeName
|
|
: `${safeName}.${PROJECT_FILE_EXTENSION}`;
|
|
|
|
const result = await dialog.showSaveDialog({
|
|
title: "Save OpenScreen Project",
|
|
defaultPath: path.join(RECORDINGS_DIR, defaultName),
|
|
filters: [
|
|
{ name: "OpenScreen Project", extensions: [PROJECT_FILE_EXTENSION] },
|
|
{ name: "JSON", extensions: ["json"] },
|
|
],
|
|
properties: ["createDirectory", "showOverwriteConfirmation"],
|
|
});
|
|
|
|
if (result.canceled || !result.filePath) {
|
|
return {
|
|
success: false,
|
|
canceled: true,
|
|
message: "Save project canceled",
|
|
};
|
|
}
|
|
|
|
await fs.writeFile(result.filePath, JSON.stringify(projectData, null, 2), "utf-8");
|
|
currentProjectPath = result.filePath;
|
|
|
|
return {
|
|
success: true,
|
|
path: result.filePath,
|
|
message: "Project saved successfully",
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to save project file:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to save project file",
|
|
error: String(error),
|
|
};
|
|
}
|
|
},
|
|
);
|
|
|
|
ipcMain.handle("load-project-file", async () => {
|
|
try {
|
|
const result = await dialog.showOpenDialog({
|
|
title: "Open OpenScreen Project",
|
|
defaultPath: RECORDINGS_DIR,
|
|
filters: [
|
|
{ name: "OpenScreen Project", extensions: [PROJECT_FILE_EXTENSION] },
|
|
{ name: "JSON", extensions: ["json"] },
|
|
{ name: "All Files", extensions: ["*"] },
|
|
],
|
|
properties: ["openFile"],
|
|
});
|
|
|
|
if (result.canceled || result.filePaths.length === 0) {
|
|
return { success: false, canceled: true, message: "Open project canceled" };
|
|
}
|
|
|
|
const filePath = result.filePaths[0];
|
|
const content = await fs.readFile(filePath, "utf-8");
|
|
const project = JSON.parse(content);
|
|
currentProjectPath = filePath;
|
|
if (project && typeof project === "object") {
|
|
const rawProject = project as { media?: unknown; videoPath?: unknown };
|
|
const media =
|
|
normalizeProjectMedia(rawProject.media) ??
|
|
(typeof rawProject.videoPath === "string"
|
|
? {
|
|
screenVideoPath:
|
|
normalizeVideoSourcePath(rawProject.videoPath) ?? rawProject.videoPath,
|
|
}
|
|
: null);
|
|
setCurrentRecordingSessionState(media ? { ...media, createdAt: Date.now() } : null);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
path: filePath,
|
|
project,
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to load project file:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to load project file",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("load-current-project-file", async () => {
|
|
try {
|
|
if (!currentProjectPath) {
|
|
return { success: false, message: "No active project" };
|
|
}
|
|
|
|
const content = await fs.readFile(currentProjectPath, "utf-8");
|
|
const project = JSON.parse(content);
|
|
if (project && typeof project === "object") {
|
|
const rawProject = project as { media?: unknown; videoPath?: unknown };
|
|
const media =
|
|
normalizeProjectMedia(rawProject.media) ??
|
|
(typeof rawProject.videoPath === "string"
|
|
? {
|
|
screenVideoPath:
|
|
normalizeVideoSourcePath(rawProject.videoPath) ?? rawProject.videoPath,
|
|
}
|
|
: null);
|
|
setCurrentRecordingSessionState(media ? { ...media, createdAt: Date.now() } : null);
|
|
}
|
|
return {
|
|
success: true,
|
|
path: currentProjectPath,
|
|
project,
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to load current project file:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to load current project file",
|
|
error: String(error),
|
|
};
|
|
}
|
|
});
|
|
ipcMain.handle("set-current-recording-session", (_, session: RecordingSession | null) => {
|
|
const normalized = normalizeRecordingSession(session);
|
|
setCurrentRecordingSessionState(normalized);
|
|
currentProjectPath = null;
|
|
return { success: true, session: normalized ?? undefined };
|
|
});
|
|
|
|
ipcMain.handle("get-current-recording-session", () => {
|
|
return currentRecordingSession
|
|
? { success: true, session: currentRecordingSession }
|
|
: { success: false };
|
|
});
|
|
|
|
ipcMain.handle("set-current-video-path", (_, path: string) => {
|
|
setCurrentRecordingSessionState({
|
|
screenVideoPath: normalizeVideoSourcePath(path) ?? path,
|
|
createdAt: Date.now(),
|
|
});
|
|
currentProjectPath = null;
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle("get-current-video-path", () => {
|
|
return currentRecordingSession?.screenVideoPath
|
|
? { success: true, path: currentRecordingSession.screenVideoPath }
|
|
: { success: false };
|
|
});
|
|
|
|
ipcMain.handle("clear-current-video-path", () => {
|
|
setCurrentRecordingSessionState(null);
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle("get-platform", () => {
|
|
return process.platform;
|
|
});
|
|
|
|
ipcMain.handle("get-shortcuts", async () => {
|
|
try {
|
|
const data = await fs.readFile(SHORTCUTS_FILE, "utf-8");
|
|
return JSON.parse(data);
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("save-shortcuts", async (_, shortcuts: unknown) => {
|
|
try {
|
|
await fs.writeFile(SHORTCUTS_FILE, JSON.stringify(shortcuts, null, 2), "utf-8");
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error("Failed to save shortcuts:", error);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
});
|
|
}
|