Skip to content

Commit

Permalink
double-click to open / open from cli (#3643)
Browse files Browse the repository at this point in the history
* fixes

Signed-off-by: Jess Frazelle <[email protected]>

* add tests

Signed-off-by: Jess Frazelle <[email protected]>

* updates

Signed-off-by: Jess Frazelle <[email protected]>

* Look at this (photo)Graph *in the voice of Nickelback*

* remove unneeded rust

Signed-off-by: Jess Frazelle <[email protected]>

* remove dep on clap

Signed-off-by: Jess Frazelle <[email protected]>

* fixups for imports

Signed-off-by: Jess Frazelle <[email protected]>

* updates

Signed-off-by: Jess Frazelle <[email protected]>

* fix types

Signed-off-by: Jess Frazelle <[email protected]>

* bump

Signed-off-by: Jess Frazelle <[email protected]>

---------

Signed-off-by: Jess Frazelle <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
jessfraz and github-actions[bot] authored Aug 25, 2024
1 parent fbf0d3d commit 590a647
Show file tree
Hide file tree
Showing 31 changed files with 415 additions and 1,540 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cargo-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
# We specifically want to test the disable-println feature
# Since it is not enabled by default, we need to specify it
# This is used in kcl-lsp
cargo check --all --features disable-println --features pyo3
cargo check --all --features disable-println --features pyo3 --features cli
1 change: 1 addition & 0 deletions interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface IElectronAPI {
sep: typeof path.sep
rename: (prev: string, next: string) => typeof fs.rename
setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: {
name: string
}
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",
"minimist": "^1.2.8",
"openid-client": "^5.6.5",
"re-resizable": "^6.9.11",
"react": "^18.3.1",
Expand Down Expand Up @@ -89,7 +90,7 @@
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src e2e",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen",
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
Expand Down Expand Up @@ -129,6 +130,7 @@
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/rebuild": "^3.6.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.46.1",
Expand All @@ -137,6 +139,7 @@
"@types/d3-force": "^3.0.10",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6",
"@types/node": "^22.5.0",
"@types/pixelmatch": "^5.2.6",
Expand Down
15 changes: 5 additions & 10 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/desktop'
import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
Expand Down Expand Up @@ -71,17 +70,13 @@ const router = createRouter([
loader: async () => {
const onDesktop = isDesktop()
if (onDesktop) {
const appState = await getState()

if (appState) {
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// else we can ignore.
await setState(undefined)
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (appState.current_file) {
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE + '/' + encodeURIComponent(appState.current_file)
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FileEntry, IndexLoaderData } from 'lib/types'
import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
Expand All @@ -20,6 +20,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'

function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
Expand Down
2 changes: 1 addition & 1 deletion src/components/LspProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Extension } from '@codemirror/state'
import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { FileEntry } from 'lib/types'
import { FileEntry } from 'lib/project'
import Worker from 'editor/plugins/lsp/worker.ts?worker'
import {
KclWorkerOptions,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProjectCard/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'

function ProjectCard({
project,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProjectCard/ProjectCardRenameForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip'
import { HTMLProps, forwardRef } from 'react'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'

interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
project: Project
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProjectSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
import { CustomIcon } from './CustomIcon'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProjectSidebarMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'

const now = new Date()
const projectWellFormed = {
Expand Down
16 changes: 1 addition & 15 deletions src/lib/desktop.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { err } from 'lib/trap'
import { Models } from '@kittycad/lib'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { Project, FileEntry } from 'lib/project'

import {
defaultAppSettings,
Expand Down Expand Up @@ -477,18 +475,6 @@ export const writeAppSettingsFile = async (tomlStr: string) => {
return window.electron.writeFile(appSettingsFilePath, tomlStr)
}

let appStateStore: ProjectState | undefined = undefined

export const getState = async (): Promise<ProjectState | undefined> => {
return Promise.resolve(appStateStore)
}

export const setState = async (
state: ProjectState | undefined
): Promise<void> => {
appStateStore = state
}

export const getUser = async (
token: string,
hostname: string
Expand Down
2 changes: 1 addition & 1 deletion src/lib/desktopFS.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isDesktop } from './isDesktop'
import type { FileEntry } from 'lib/types'
import type { FileEntry } from 'lib/project'
import {
FILE_EXT,
INDEX_IDENTIFIER,
Expand Down
98 changes: 98 additions & 0 deletions src/lib/getCurrentProjectFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { promises as fs } from 'fs'
import path from 'path'
import os from 'os'
import { v4 as uuidv4 } from 'uuid'
import getCurrentProjectFile from './getCurrentProjectFile'

describe('getCurrentProjectFile', () => {
test('with explicit open file with space (URL encoded)', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)

await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')

const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i%20have%20a%20space.kcl')
)

expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))

await fs.rm(tmpProjectDir, { recursive: true, force: true })
})

test('with explicit open file with space', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)

await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')

const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i have a space.kcl')
)

expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))

await fs.rm(tmpProjectDir, { recursive: true, force: true })
})

test('with source path dot', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })

// Set the current directory to the temp project directory.
const originalCwd = process.cwd()
process.chdir(tmpProjectDir)

try {
const state = await getCurrentProjectFile('.')

if (state instanceof Error) {
throw state
}

expect(state.replace('/private', '')).toBe(
path.join(tmpProjectDir, 'main.kcl')
)
} finally {
process.chdir(originalCwd)
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})

test('with main.kcl not existing', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })

try {
const state = await getCurrentProjectFile(tmpProjectDir)

expect(state).toBe(path.join(tmpProjectDir, 'main.kcl'))
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})

test('with directory, main.kcl not existing, other.kcl does', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'other.kcl'), '')

try {
const state = await getCurrentProjectFile(tmpProjectDir)

expect(state).toBe(path.join(tmpProjectDir, 'other.kcl'))

// make sure we didn't create a main.kcl file
await expect(
fs.access(path.join(tmpProjectDir, 'main.kcl'))
).rejects.toThrow()
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
})
116 changes: 116 additions & 0 deletions src/lib/getCurrentProjectFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import { Models } from '@kittycad/lib/dist/types/src'
import { PROJECT_ENTRYPOINT } from './constants'

// Create a const object with the values
const FILE_IMPORT_FORMATS = {
fbx: 'fbx',
gltf: 'gltf',
obj: 'obj',
ply: 'ply',
sldprt: 'sldprt',
step: 'step',
stl: 'stl',
} as const

// Extract the values into an array
const fileImportFormats: Models['FileImportFormat_type'][] =
Object.values(FILE_IMPORT_FORMATS)
export const allFileImportFormats: string[] = [
...fileImportFormats,
'stp',
'fbxb',
'glb',
]
export const relevantExtensions = ['kcl', ...allFileImportFormats]

/// Get the current project file from the path.
/// This is used for double-clicking on a file in the file explorer,
/// or the command line args, or deep linking.
export default async function getCurrentProjectFile(
pathString: string
): Promise<string | Error> {
// Fix for "." path, which is the current directory.
let sourcePath = pathString === '.' ? process.cwd() : pathString

// URL decode the path.
sourcePath = decodeURIComponent(sourcePath)

// If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path.
sourcePath = path.isAbsolute(sourcePath)
? sourcePath
: path.join(process.cwd(), sourcePath)

// If the path is a directory, let's assume it is a project directory.
const stats = await fs.stat(sourcePath)
if (stats.isDirectory()) {
// Walk the directory and look for a kcl file.
const files = await fs.readdir(sourcePath)
const kclFiles = files.filter((file) => path.extname(file) === '.kcl')

if (kclFiles.length === 0) {
let projectFile = path.join(sourcePath, PROJECT_ENTRYPOINT)
// Check if we have a main.kcl file in the project.
try {
await fs.access(projectFile)
} catch {
// Create the default file in the project.
await fs.writeFile(projectFile, '')
}

return projectFile
}

// If a project entrypoint file exists, use it.
// Otherwise, use the first kcl file in the project.
const gotMain = files.filter((file) => file === PROJECT_ENTRYPOINT)
if (gotMain.length === 0) {
return path.join(sourcePath, kclFiles[0])
}
return path.join(sourcePath, PROJECT_ENTRYPOINT)
}

// Check if the extension on what we are trying to open is a relevant file type.
const extension = path.extname(sourcePath).slice(1)

if (!relevantExtensions.includes(extension) && extension !== 'toml') {
return new Error(
`File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${relevantExtensions.join(
', '
)}`
)
}

// We were given a file path, not a directory.
// Let's get the parent directory of the file.
const parent = path.dirname(sourcePath)

// If we got an import model file, we need to check if we have a file in the project for
// this import model.
if (allFileImportFormats.includes(extension)) {
const importFileName = path.basename(sourcePath)
// Check if we have a file in the project for this import model.
const kclWrapperFilename = `${importFileName}.kcl`
const kclWrapperFilePath = path.join(parent, kclWrapperFilename)

try {
await fs.access(kclWrapperFilePath)
} catch {
// Create the file in the project with the default import content.
const content = `// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("${importFileName}")`
await fs.writeFile(kclWrapperFilePath, content)
}

return kclWrapperFilePath
}

return sourcePath
}
Loading

0 comments on commit 590a647

Please sign in to comment.