Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 37 additions & 0 deletions apps/desktop/src/main/handlers/openSketchSourceFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import path from 'path'
import fs from 'fs'
import { shell } from 'electron'

/**
* Opens the source file (index.ts or index.js) for a sketch module in the default app.
* @param sketchesDir The absolute path to the sketches directory
* @param moduleId The sketch's moduleId
* @returns Promise<{ success: boolean; error?: string }>
*/
export const openSketchSourceFile = async (
sketchesDir: string,
moduleId: string,
): Promise<{ success: boolean; error?: string }> => {
try {
const tsPath = path.join(sketchesDir, moduleId, 'index.ts')
const jsPath = path.join(sketchesDir, moduleId, 'index.js')
let filePath = tsPath

@cale-bradbury cale-bradbury Mar 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

instead of all this (regenerating the path), do we want to just cache the path on load @funwithtriangles? might be handy if we ever support sketch root files with different names

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's discuss today, don't have my head in this one right now

if (fs.existsSync(tsPath)) {
filePath = tsPath
} else if (fs.existsSync(jsPath)) {
filePath = jsPath
} else {
return { success: false, error: 'Source file not found' }

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

moduleId is used to build a filesystem path without validation. Because the preload exposes a full ipcRenderer API, a compromised renderer could pass values like ../... (or use symlinks) to make the main process open arbitrary files. Please validate moduleId (e.g., reject path separators / ..), and ensure the resolved target path stays within sketchesDir before calling shell.openPath.

Copilot uses AI. Check for mistakes.
}
const openResult = await shell.openPath(filePath)
if (openResult) {
return { success: false, error: openResult }
}
return { success: true }
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : 'Unknown error opening file',
}
}
}
8 changes: 8 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { app, BrowserWindow, dialog, ipcMain, screen, session } from 'electron'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { REDUX_DEVTOOLS, installExtension } from '@tomjs/electron-devtools-installer'
import { ProjectData } from '@hedron-gl/app-store'
import { openSketchSourceFile } from './handlers/openSketchSourceFile'

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

Use the existing @main/handlers/... path alias here for consistency with the other handler imports in this file. Mixing relative and aliased imports makes refactors (like moving index.ts) harder and can lead to duplicate module resolution paths.

Suggested change
import { openSketchSourceFile } from './handlers/openSketchSourceFile'
import { openSketchSourceFile } from '@main/handlers/openSketchSourceFile'

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ignore this one @cale-bradbury. Our eslint rules dictate this sort of thing. They disallow going back up the tree (e.g. ../../foo) but this way round is fine.

import { saveFrameHandler, saveFrameSequenceHandler } from './handlers/frameHandlers'
import {
DialogEvents,
Expand Down Expand Up @@ -112,6 +113,13 @@ ipcMain.handle(FileEvents.OpenFolder, async (_, folderPath: string) => {
return await openFolder(folderPath)
})

ipcMain.handle(
FileEvents.OpenSketchSourceFile,
async (_, sketchesDir: string, moduleId: string) => {
return await openSketchSourceFile(sketchesDir, moduleId)
},
)

ipcMain.handle(SketchEvents.StartSketchesServer, async (_, sketchesDir: string) => {
return await startSketchesServer(sketchesDir)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

import { Node } from '@hedron-gl/engine'
import c from './ActiveSketch.module.css'
import { openSketchSourceFile } from './openSketchSourceFile'
import { useActiveSketch } from '@components/hooks/useActiveSketch'
import { engineStore } from '@renderer/engine'
import { SketchControls } from '@components/SketchControls/SketchControls'
Expand Down Expand Up @@ -63,6 +64,11 @@ export const ActiveSketch = () => {
icon: 'arrow_downward',
onClick: () => engineStore.getState().moveSketchDown(activeSketch.id),
},
{
label: 'Open Source',
icon: 'code',
onClick: () => openSketchSourceFile(activeSketch.moduleId),
},
{
label: 'Delete Sketch',
icon: 'delete',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// openSketchSourceFile.ts
// Utility to open the source file for a sketch in the default program
import { appStore } from '@renderer/appStore'
import { FileEvents } from '@shared/Events'

/**
* Attempts to open the source file (index.ts or index.js) for a sketch module in the default app.
* @param moduleId The sketch's moduleId
* @returns Promise<void>
*/
export async function openSketchSourceFile(moduleId: string) {
const sketchesDir = appStore.getState().sketchesDir
if (!sketchesDir) {
alert('No sketches directory set')
return
}
// Ask main process to open the file
const result = await window.electronApi.ipcRenderer.invoke(
FileEvents.OpenSketchSourceFile,
sketchesDir,
moduleId,
)
if (!result?.success) {
alert(result?.error || 'Source file not found for this sketch.')

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

ipcRenderer.invoke can reject (e.g., if the handler throws or IPC is unavailable). Right now that would surface as an unhandled promise rejection from a click handler. Wrap the invoke in a try/catch and surface a controlled error message (or return the error to the caller) so the UI can fail gracefully.

Suggested change
const result = await window.electronApi.ipcRenderer.invoke(
FileEvents.OpenSketchSourceFile,
sketchesDir,
moduleId,
)
if (!result?.success) {
alert(result?.error || 'Source file not found for this sketch.')
try {
const result = await window.electronApi.ipcRenderer.invoke(
FileEvents.OpenSketchSourceFile,
sketchesDir,
moduleId,
)
if (!result?.success) {
alert(result?.error || 'Source file not found for this sketch.')
}
} catch (error: unknown) {
const message =
(error && typeof error === 'object' && 'message' in error
? String((error as { message?: string }).message || '')
: '') || 'An unexpected error occurred.'
alert(`Failed to open source file: ${message}`)

Copilot uses AI. Check for mistakes.
}
}
1 change: 1 addition & 0 deletions apps/desktop/src/shared/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum DialogEvents {
export enum FileEvents {
SaveProject = 'save-project',
OpenFolder = 'open-folder',
OpenSketchSourceFile = 'open-sketch-source-file',
}

type ResponseCanceled = {
Expand Down
Loading