Skip to content
Open
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
10 changes: 10 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ interface Window {
getCursorTelemetry: (videoPath?: string) => Promise<{
success: boolean;
samples: CursorTelemetryPoint[];
display?: {
boundsX: number;
boundsY: number;
boundsWidth: number;
boundsHeight: number;
workAreaX: number;
workAreaY: number;
workAreaWidth: number;
workAreaHeight: number;
};
message?: string;
error?: string;
}>;
Expand Down
43 changes: 41 additions & 2 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,20 @@ async function storeRecordedSessionFiles(payload: StoreRecordedSessionInput) {
if (pendingCursorSamples.length > 0) {
await fs.writeFile(
telemetryPath,
JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples }, null, 2),
JSON.stringify(
{
version: CURSOR_TELEMETRY_VERSION,
samples: pendingCursorSamples,
display: captureDisplayInfo ?? undefined,
},
null,
2,
),
"utf-8",
);
}
pendingCursorSamples = [];
captureDisplayInfo = null;

const sessionManifestPath = path.join(
RECORDINGS_DIR,
Expand Down Expand Up @@ -185,6 +194,19 @@ function stopCursorCapture() {
}
}

// Store the capture display info so we can save it to cursor.json for correct mapping
let captureDisplayInfo: {
boundsX: number;
boundsY: number;
boundsWidth: number;
boundsHeight: number;
workAreaX: number;
workAreaY: number;
workAreaWidth: number;
workAreaHeight: number;
scaleFactor: number;
} | null = null;

function sampleCursorPoint() {
const cursor = screen.getCursorScreenPoint();
const sourceDisplayId = Number(selectedSource?.display_id);
Expand All @@ -196,6 +218,21 @@ function sampleCursorPoint() {
const width = Math.max(1, bounds.width);
const height = Math.max(1, bounds.height);

// Save display info on first sample
if (!captureDisplayInfo) {
captureDisplayInfo = {
boundsX: bounds.x,
boundsY: bounds.y,
boundsWidth: bounds.width,
boundsHeight: bounds.height,
workAreaX: display.workArea.x,
workAreaY: display.workArea.y,
workAreaWidth: display.workArea.width,
workAreaHeight: display.workArea.height,
scaleFactor: display.scaleFactor,
};
}

const cx = clamp((cursor.x - bounds.x) / width, 0, 1);
const cy = clamp((cursor.y - bounds.y) / height, 0, 1);

Expand Down Expand Up @@ -369,6 +406,7 @@ export function registerIpcHandlers(

ipcMain.handle("set-recording-state", (_, recording: boolean) => {
if (recording) {
captureDisplayInfo = null;
stopCursorCapture();
activeCursorSamples = [];
pendingCursorSamples = [];
Expand Down Expand Up @@ -426,7 +464,8 @@ export function registerIpcHandlers(
})
.sort((a: CursorTelemetryPoint, b: CursorTelemetryPoint) => a.timeMs - b.timeMs);

return { success: true, samples };
const display = parsed?.display ?? undefined;
return { success: true, samples, display };
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === "ENOENT") {
Expand Down
168 changes: 168 additions & 0 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import Block from "@uiw/react-color-block";
import {
Bug,
Circle,
Crop,
Crosshair,
Download,
Film,
FolderOpen,
Image,
Lock,
Mouse,
Palette,
Save,
Sparkles,
Star,
Target,
Trash2,
Unlock,
Upload,
Expand Down Expand Up @@ -41,6 +45,7 @@ import type {
AnnotationRegion,
AnnotationType,
CropRegion,
CursorStyle,
FigureData,
PlaybackSpeed,
ZoomDepth,
Expand Down Expand Up @@ -132,6 +137,23 @@ interface SettingsPanelProps {
selectedSpeedValue?: PlaybackSpeed | null;
onSpeedChange?: (speed: PlaybackSpeed) => void;
onSpeedDelete?: (id: string) => void;
// Cursor settings
hasCursorTelemetry?: boolean;
showCursorHighlight?: boolean;
onShowCursorHighlightChange?: (show: boolean) => void;
cursorStyle?: CursorStyle;
onCursorStyleChange?: (style: CursorStyle) => void;
cursorColor?: string;
onCursorColorChange?: (color: string) => void;
cursorSize?: number;
onCursorSizeChange?: (size: number) => void;
onCursorSizeCommit?: () => void;
cursorOpacity?: number;
onCursorOpacityChange?: (opacity: number) => void;
onCursorOpacityCommit?: () => void;
cursorStrokeWidth?: number;
onCursorStrokeWidthChange?: (width: number) => void;
onCursorStrokeWidthCommit?: () => void;
}

export default SettingsPanel;
Expand Down Expand Up @@ -197,6 +219,23 @@ export function SettingsPanel({
selectedSpeedValue,
onSpeedChange,
onSpeedDelete,
// Cursor settings
hasCursorTelemetry = false,
showCursorHighlight = false,
onShowCursorHighlightChange,
cursorStyle = "dot",
onCursorStyleChange,
cursorColor = "#ffcc00",
onCursorColorChange,
cursorSize = 32,
onCursorSizeChange,
onCursorSizeCommit,
cursorOpacity = 0.6,
onCursorOpacityChange,
onCursorOpacityCommit,
cursorStrokeWidth = 2,
onCursorStrokeWidthChange,
onCursorStrokeWidthCommit,
}: SettingsPanelProps) {
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
const [customImages, setCustomImages] = useState<string[]>([]);
Expand Down Expand Up @@ -665,6 +704,135 @@ export function SettingsPanel({
</AccordionContent>
</AccordionItem>

<AccordionItem value="cursor" className="border-white/5 rounded-xl bg-white/[0.02] px-3">
<AccordionTrigger className="py-2.5 hover:no-underline">
<div className="flex items-center gap-2">
<Mouse className="w-4 h-4 text-[#34B27B]" />
<span className="text-xs font-medium">Cursor Highlight</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-3">
{!hasCursorTelemetry && (
<div className="text-[10px] text-slate-500 mb-2 p-2 rounded-lg bg-white/5 border border-white/5">
No cursor data — re-record to enable cursor effects.
</div>
)}

<div className={cn(!hasCursorTelemetry && "opacity-40 pointer-events-none")}>
<div className="flex items-center justify-between p-2 rounded-lg bg-white/5 border border-white/5 mb-2">
<div className="text-[10px] font-medium text-slate-300">
Show Cursor Highlight
</div>
<Switch
checked={showCursorHighlight}
onCheckedChange={onShowCursorHighlightChange}
className="data-[state=checked]:bg-[#34B27B] scale-90"
/>
</div>

<div className={cn(!showCursorHighlight && "opacity-40 pointer-events-none")}>
<div className="text-[10px] font-medium text-slate-400 mb-1.5 px-0.5">Style</div>
<div className="grid grid-cols-4 gap-1 mb-2">
{(["dot", "circle", "ring", "glow"] as const).map((style) => (
<button
key={style}
type="button"
onClick={() => onCursorStyleChange?.(style)}
className={cn(
"flex items-center justify-center gap-1 p-1.5 rounded-md text-[10px] font-medium border transition-all",
cursorStyle === style
? "bg-[#34B27B]/20 border-[#34B27B] text-[#34B27B]"
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10",
)}
>
{style === "dot" && <Circle className="w-3 h-3" />}
{style === "circle" && <Crosshair className="w-3 h-3" />}
{style === "ring" && <Target className="w-3 h-3" />}
{style === "glow" && <Sparkles className="w-3 h-3" />}
<span className="capitalize">{style}</span>
</button>
))}
</div>

<div className="text-[10px] font-medium text-slate-400 mb-1.5 px-0.5">Color</div>
<div className="mb-2">
<Block
color={cursorColor}
colors={[
"#ffcc00",
"#ffffff",
"#000000",
"#ff0000",
"#ff6600",
"#00ff00",
"#0088ff",
"#ff66cc",
"#34B27B",
"#8b5cf6",
]}
onChange={(color) => onCursorColorChange?.(color.hex)}
className="!bg-transparent !shadow-none !border-0 !p-0"
/>
</div>

<div className="grid grid-cols-2 gap-2">
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">Size</div>
<span className="text-[10px] text-slate-500 font-mono">{cursorSize}px</span>
</div>
<Slider
value={[cursorSize]}
onValueChange={(values) => onCursorSizeChange?.(values[0])}
onValueCommit={() => onCursorSizeCommit?.()}
min={16}
max={64}
step={1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">Opacity</div>
<span className="text-[10px] text-slate-500 font-mono">
{Math.round(cursorOpacity * 100)}%
</span>
</div>
<Slider
value={[cursorOpacity]}
onValueChange={(values) => onCursorOpacityChange?.(values[0])}
onValueCommit={() => onCursorOpacityCommit?.()}
min={0}
max={1}
step={0.01}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
</div>
{(cursorStyle === "circle" || cursorStyle === "ring") && (
<div className="p-2 rounded-lg bg-white/5 border border-white/5 mt-2">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">Stroke</div>
<span className="text-[10px] text-slate-500 font-mono">
{cursorStrokeWidth}px
</span>
</div>
<Slider
value={[cursorStrokeWidth]}
onValueChange={(values) => onCursorStrokeWidthChange?.(values[0])}
onValueCommit={() => onCursorStrokeWidthCommit?.()}
min={1}
max={6}
step={0.5}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>

<AccordionItem
value="background"
className="border-white/5 rounded-xl bg-white/[0.02] px-3"
Expand Down
Loading