double-click to open / open from cli (#3643)
* fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * add tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * Look at this (photo)Graph *in the voice of Nickelback* * remove unneeded rust Signed-off-by: Jess Frazelle <github@jessfraz.com> * remove dep on clap Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixups for imports Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix types Signed-off-by: Jess Frazelle <github@jessfraz.com> * bump Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
2
.github/workflows/cargo-check.yml
vendored
2
.github/workflows/cargo-check.yml
vendored
@ -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
interface.d.ts
vendored
1
interface.d.ts
vendored
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
@ -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",
|
||||
@ -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",
|
||||
@ -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",
|
||||
|
@ -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'
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
@ -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})`
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
@ -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
|
||||
|
@ -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,
|
||||
|
98
src/lib/getCurrentProjectFile.test.ts
Normal file
98
src/lib/getCurrentProjectFile.test.ts
Normal file
@ -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
src/lib/getCurrentProjectFile.ts
Normal file
116
src/lib/getCurrentProjectFile.ts
Normal file
@ -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
|
||||
}
|
46
src/lib/project.d.ts
vendored
Normal file
46
src/lib/project.d.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* The permissions of a file.
|
||||
*/
|
||||
export type FilePermission = 'read' | 'write' | 'execute'
|
||||
|
||||
/**
|
||||
* The type of a file.
|
||||
*/
|
||||
export type FileType = 'file' | 'directory' | 'symlink'
|
||||
|
||||
/**
|
||||
* Metadata about a file or directory.
|
||||
*/
|
||||
export type FileMetadata = {
|
||||
accessed: string | null
|
||||
created: string | null
|
||||
type: FileType | null
|
||||
size: number
|
||||
modified: string | null
|
||||
permission: FilePermission | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a file or directory.
|
||||
*/
|
||||
export type FileEntry = {
|
||||
path: string
|
||||
name: string
|
||||
children: Array<FileEntry> | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about project.
|
||||
*/
|
||||
export type Project = {
|
||||
metadata: FileMetadata | null
|
||||
kcl_file_count: number
|
||||
directory_count: number
|
||||
/**
|
||||
* The default file to open on load.
|
||||
*/
|
||||
default_file: string
|
||||
path: string
|
||||
name: string
|
||||
children: Array<FileEntry> | null
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
const DESC = ':desc'
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
|
||||
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||
import { Project, FileEntry } from 'lib/project'
|
||||
|
||||
export type IndexLoaderData = {
|
||||
code: string | null
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import type { FileEntry } from 'lib/types'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project, FileEntry } from 'lib/project'
|
||||
|
||||
export const fileMachine = createMachine(
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
export const homeMachine = createMachine(
|
||||
{
|
||||
|
108
src/main.ts
108
src/main.ts
@ -8,6 +8,11 @@ import { Issuer } from 'openid-client'
|
||||
import { Bonjour, Service } from 'bonjour-service'
|
||||
// @ts-ignore: TS1343
|
||||
import * as kittycad from '@kittycad/lib/import'
|
||||
import minimist from 'minimist'
|
||||
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
||||
|
||||
// Check the command line arguments for a project path
|
||||
const args = parseCLIArgs()
|
||||
|
||||
// If it's not set, scream.
|
||||
const NODE_ENV = process.env.NODE_ENV || 'production'
|
||||
@ -22,6 +27,10 @@ if (require('electron-squirrel-startup')) {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
// Global app listeners
|
||||
// Must be done before ready event
|
||||
registerListeners()
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
autoHideMenuBar: true,
|
||||
@ -159,3 +168,102 @@ ipcMain.handle('find_machine_api', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('loadProjectAtStartup', async () => {
|
||||
// If we are in development mode, we don't want to load a project at
|
||||
// startup.
|
||||
// Since the args passed are always '.'
|
||||
if (NODE_ENV !== 'production') {
|
||||
return null
|
||||
}
|
||||
|
||||
let projectPath: string | null = null
|
||||
// macOS: open-file events that were received before the app is ready
|
||||
const macOpenFiles: string[] = (global as any).macOpenFiles
|
||||
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
|
||||
projectPath = macOpenFiles[0] // We only do one project at a time
|
||||
}
|
||||
// Reset this so we don't accidentally use it again.
|
||||
const macOpenFilesEmpty: string[] = []
|
||||
// @ts-ignore
|
||||
global['macOpenFiles'] = macOpenFilesEmpty
|
||||
|
||||
// macOS: open-url events that were received before the app is ready
|
||||
const getOpenUrls: string[] = ((global as any).getOpenUrls() ||
|
||||
[]) as string[]
|
||||
if (getOpenUrls && getOpenUrls.length > 0) {
|
||||
projectPath = getOpenUrls[0] // We only do one project at a
|
||||
}
|
||||
// Reset this so we don't accidentally use it again.
|
||||
// @ts-ignore
|
||||
global['getOpenUrls'] = function () {
|
||||
return []
|
||||
}
|
||||
|
||||
// Check if we have a project path in the command line arguments
|
||||
// If we do, we will load the project at that path
|
||||
if (args._.length > 1) {
|
||||
if (args._[1].length > 0) {
|
||||
projectPath = args._[1]
|
||||
// Reset all this value so we don't accidentally use it again.
|
||||
args._[1] = ''
|
||||
}
|
||||
}
|
||||
|
||||
if (projectPath) {
|
||||
// We have a project path, load the project information.
|
||||
console.log(`Loading project at startup: ${projectPath}`)
|
||||
try {
|
||||
const currentFile = await getCurrentProjectFile(projectPath)
|
||||
console.log(`Project loaded: ${currentFile}`)
|
||||
return currentFile
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
function parseCLIArgs(): minimist.ParsedArgs {
|
||||
return minimist(process.argv, {})
|
||||
}
|
||||
|
||||
function registerListeners() {
|
||||
/**
|
||||
* macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
|
||||
* the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
|
||||
*/
|
||||
const macOpenFiles: string[] = []
|
||||
// @ts-ignore
|
||||
global['macOpenFiles'] = macOpenFiles
|
||||
app.on('open-file', function (event, path) {
|
||||
macOpenFiles.push(path)
|
||||
})
|
||||
|
||||
/**
|
||||
* macOS: react to open-url requests.
|
||||
*/
|
||||
const openUrls: string[] = []
|
||||
const onOpenUrl = function (
|
||||
event: { preventDefault: () => void },
|
||||
url: string
|
||||
) {
|
||||
event.preventDefault()
|
||||
|
||||
openUrls.push(url)
|
||||
}
|
||||
|
||||
app.on('will-finish-launching', function () {
|
||||
app.on('open-url', onOpenUrl)
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
global['getOpenUrls'] = function () {
|
||||
app.removeListener('open-url', onOpenUrl)
|
||||
|
||||
return openUrls
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,9 @@ const listMachines = async (): Promise<MachinesListing> => {
|
||||
const getMachineApiIp = async (): Promise<String | null> =>
|
||||
ipcRenderer.invoke('find_machine_api')
|
||||
|
||||
const loadProjectAtStartup = async (): Promise<string | null> =>
|
||||
ipcRenderer.invoke('loadProjectAtStartup')
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
login,
|
||||
// Passing fs directly is not recommended since it gives a lot of power
|
||||
@ -93,6 +96,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
isWindows,
|
||||
isLinux,
|
||||
},
|
||||
loadProjectAtStartup,
|
||||
// IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is
|
||||
// no way to set it across the bridge boundary. We need to make it a command.
|
||||
setBaseUrl: (value: string) => (process.env.BASE_URL = value),
|
||||
|
@ -31,13 +31,13 @@ import { kclManager } from 'lib/singletons'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||
import { LowerRightControls } from 'components/LowerRightControls'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import {
|
||||
createNewProjectDirectory,
|
||||
listProjects,
|
||||
renameProjectDirectory,
|
||||
} from 'lib/desktop'
|
||||
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
// This route only opens in the desktop context for now,
|
||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||
|
70
src/wasm-lib/Cargo.lock
generated
70
src/wasm-lib/Cargo.lock
generated
@ -70,54 +70,12 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
@ -426,12 +384,8 @@ version = "4.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim 0.11.0",
|
||||
"unicase",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -452,12 +406,6 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.1.0"
|
||||
@ -642,7 +590,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"strsim",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
@ -1397,7 +1345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1492,7 +1440,6 @@ dependencies = [
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"data-encoding",
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
@ -2796,12 +2743,6 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
|
||||
|
||||
[[package]]
|
||||
name = "structmeta"
|
||||
version = "0.3.0"
|
||||
@ -3469,12 +3410,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
@ -3626,7 +3561,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
"clap",
|
||||
"console_error_panic_hook",
|
||||
"data-encoding",
|
||||
"futures",
|
||||
|
@ -11,7 +11,6 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
clap = "4.5.16"
|
||||
data-encoding = "2.6.0"
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -16,7 +16,7 @@ async-recursion = "1.1.1"
|
||||
async-trait = "0.1.81"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.16", default-features = false, optional = true }
|
||||
clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] }
|
||||
convert_case = "0.6.0"
|
||||
dashmap = "6.0.1"
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
@ -27,7 +27,7 @@ git_rev = "0.1.0"
|
||||
gltf-json = "1.4.1"
|
||||
http = { workspace = true }
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
kittycad = { workspace = true, features = ["clap"] }
|
||||
kittycad = { workspace = true }
|
||||
lazy_static = "1.5.0"
|
||||
measurements = "0.11.0"
|
||||
mime_guess = "2.0.5"
|
||||
@ -66,7 +66,7 @@ tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"]
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
|
||||
[features]
|
||||
default = ["cli", "engine"]
|
||||
default = ["engine"]
|
||||
cli = ["dep:clap"]
|
||||
# For the lsp server, when run with stdout for rpc we want to disable println.
|
||||
# This is used for editor extensions that use the lsp server.
|
||||
|
134
src/wasm-lib/kcl/fuzz/Cargo.lock
generated
134
src/wasm-lib/kcl/fuzz/Cargo.lock
generated
@ -70,55 +70,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
@ -377,54 +328,6 @@ dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"unicase",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.1.0"
|
||||
@ -596,7 +499,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"convert_case",
|
||||
@ -906,12 +809,6 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -1090,12 +987,6 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@ -1140,7 +1031,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.6"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1149,7 +1040,6 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bson",
|
||||
"chrono",
|
||||
"clap",
|
||||
"convert_case",
|
||||
"dashmap 6.0.1",
|
||||
"databake",
|
||||
@ -1158,6 +1048,7 @@ dependencies = [
|
||||
"futures",
|
||||
"git_rev",
|
||||
"gltf-json",
|
||||
"http 0.2.12",
|
||||
"image",
|
||||
"js-sys",
|
||||
"kittycad",
|
||||
@ -1198,9 +1089,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.14"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5e9c51976882cdf6777557fd8c3ee68b00bb53e9307fc1721acb397f2ece9a"
|
||||
checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1208,7 +1099,6 @@ dependencies = [
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"data-encoding",
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
@ -2197,7 +2087,7 @@ version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
@ -2649,12 +2539,6 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@ -2685,12 +2569,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
|
@ -1,5 +1,3 @@
|
||||
//! This module contains settings for kcl projects as well as the modeling app.
|
||||
|
||||
pub mod types;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod utils;
|
||||
|
@ -1,893 +0,0 @@
|
||||
//! Types for interacting with files in projects.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// State management for the application.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProjectState {
|
||||
pub project: Project,
|
||||
pub current_file: Option<String>,
|
||||
}
|
||||
|
||||
impl ProjectState {
|
||||
/// Create a new project state from a path.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> {
|
||||
// Fix for "." path, which is the current directory.
|
||||
let source_path = if path == Path::new(".") {
|
||||
std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
// Url decode the path.
|
||||
let source_path =
|
||||
std::path::Path::new(&urlencoding::decode(&source_path.display().to_string())?.to_string()).to_path_buf();
|
||||
|
||||
// If the path does not start with a slash, it is a relative path.
|
||||
// We need to convert it to an absolute path.
|
||||
let source_path = if source_path.is_relative() {
|
||||
std::env::current_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
|
||||
.join(source_path)
|
||||
} else {
|
||||
source_path
|
||||
};
|
||||
|
||||
// If the path is a directory, let's assume it is a project directory.
|
||||
if source_path.is_dir() {
|
||||
// Load the details about the project from the path.
|
||||
let project = Project::from_path(&source_path)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
|
||||
|
||||
// Check if we have a main.kcl file in the project.
|
||||
let project_file = source_path.join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
|
||||
|
||||
if !project_file.exists() {
|
||||
// Create the default file in the project.
|
||||
// Write the initial project file.
|
||||
tokio::fs::write(&project_file, vec![]).await?;
|
||||
}
|
||||
|
||||
return Ok(ProjectState {
|
||||
project,
|
||||
current_file: Some(project_file.display().to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the extension on what we are trying to open is a relevant file type.
|
||||
// Get the extension of the file.
|
||||
let extension = source_path
|
||||
.extension()
|
||||
.ok_or_else(|| anyhow::anyhow!("Error getting the extension of the file: `{}`", source_path.display()))?;
|
||||
let ext = extension.to_string_lossy().to_string();
|
||||
|
||||
// Check if the extension is a relevant file type.
|
||||
if !crate::settings::utils::RELEVANT_EXTENSIONS.contains(&ext) || ext == "toml" {
|
||||
return Err(anyhow::anyhow!(
|
||||
"File type ({}) cannot be opened with this app: `{}`, try opening one of the following file types: {}",
|
||||
ext,
|
||||
source_path.display(),
|
||||
crate::settings::utils::RELEVANT_EXTENSIONS.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
// We were given a file path, not a directory.
|
||||
// Let's get the parent directory of the file.
|
||||
let parent = source_path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Error getting the parent directory of the file: {}",
|
||||
source_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
// If we got a import model file, we need to check if we have a file in the project for
|
||||
// this import model.
|
||||
if crate::settings::utils::IMPORT_FILE_EXTENSIONS.contains(&ext) {
|
||||
let import_file_name = source_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("Error getting the file name of the file: {}", source_path.display()))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
// Check if we have a file in the project for this import model.
|
||||
let kcl_wrapper_filename = format!("{}.kcl", import_file_name);
|
||||
let kcl_wrapper_file_path = parent.join(&kcl_wrapper_filename);
|
||||
|
||||
if !kcl_wrapper_file_path.exists() {
|
||||
// Create the file in the project.
|
||||
// With the default import content.
|
||||
tokio::fs::write(
|
||||
&kcl_wrapper_file_path,
|
||||
format!(
|
||||
r#"// 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("{}")"#,
|
||||
import_file_name
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Load the details about the project from the parent directory.
|
||||
// We do this after we generate the import file so that the file is included in the project.
|
||||
let project = Project::from_path(&parent)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
|
||||
|
||||
return Ok(ProjectState {
|
||||
project,
|
||||
current_file: Some(kcl_wrapper_file_path.display().to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// Load the details about the project from the parent directory.
|
||||
let project = Project::from_path(&parent)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
|
||||
|
||||
Ok(ProjectState {
|
||||
project,
|
||||
current_file: Some(source_path.display().to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about project.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Project {
|
||||
#[serde(flatten)]
|
||||
pub file: FileEntry,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<FileMetadata>,
|
||||
#[serde(default)]
|
||||
#[ts(type = "number")]
|
||||
pub kcl_file_count: u64,
|
||||
#[serde(default)]
|
||||
#[ts(type = "number")]
|
||||
pub directory_count: u64,
|
||||
/// The default file to open on load.
|
||||
pub default_file: String,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Populate a project from a path.
|
||||
pub async fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
|
||||
// Check if they are using '.' as the path.
|
||||
let path = if path.as_ref() == std::path::Path::new(".") {
|
||||
std::env::current_dir()?
|
||||
} else {
|
||||
path.as_ref().to_path_buf()
|
||||
};
|
||||
|
||||
// Make sure the path exists.
|
||||
if !path.exists() {
|
||||
return Err(anyhow::anyhow!("Path does not exist"));
|
||||
}
|
||||
|
||||
let file = crate::settings::utils::walk_dir(&path).await?;
|
||||
let metadata = std::fs::metadata(&path).ok().map(|m| m.into());
|
||||
let mut project = Self {
|
||||
file: file.clone(),
|
||||
metadata,
|
||||
kcl_file_count: 0,
|
||||
directory_count: 0,
|
||||
default_file: get_default_kcl_file_for_dir(path, file).await?,
|
||||
};
|
||||
project.populate_kcl_file_count()?;
|
||||
project.populate_directory_count()?;
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
/// Populate the number of KCL files in the project.
|
||||
pub fn populate_kcl_file_count(&mut self) -> Result<()> {
|
||||
let mut count = 0;
|
||||
if let Some(children) = &self.file.children {
|
||||
for entry in children.iter() {
|
||||
if entry.name.ends_with(".kcl") {
|
||||
count += 1;
|
||||
} else {
|
||||
count += entry.kcl_file_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.kcl_file_count = count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the number of directories in the project.
|
||||
pub fn populate_directory_count(&mut self) -> Result<()> {
|
||||
let mut count = 0;
|
||||
if let Some(children) = &self.file.children {
|
||||
for entry in children.iter() {
|
||||
count += entry.directory_count();
|
||||
}
|
||||
}
|
||||
|
||||
self.directory_count = count;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default KCL file for a directory.
|
||||
/// This determines what the default file to open is.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn get_default_kcl_file_for_dir<P>(dir: P, file: FileEntry) -> Result<String>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
// Make sure the dir is a directory.
|
||||
if !dir.as_ref().is_dir() {
|
||||
return Err(anyhow::anyhow!("Path `{}` is not a directory", dir.as_ref().display()));
|
||||
}
|
||||
|
||||
let default_file = dir.as_ref().join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
|
||||
if !default_file.exists() {
|
||||
// Find a kcl file in the directory.
|
||||
if let Some(children) = file.children {
|
||||
for entry in children.iter() {
|
||||
if entry.name.ends_with(".kcl") {
|
||||
return Ok(dir.as_ref().join(&entry.name).display().to_string());
|
||||
} else if entry.children.is_some() {
|
||||
// Recursively find a kcl file in the directory.
|
||||
return get_default_kcl_file_for_dir(entry.path.clone(), entry.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a kcl file, create one.
|
||||
tokio::fs::write(&default_file, vec![]).await?;
|
||||
}
|
||||
|
||||
Ok(default_file.display().to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Rename a directory for a project.
|
||||
/// This returns the new path of the directory.
|
||||
pub async fn rename_project_directory<P>(path: P, new_name: &str) -> Result<std::path::PathBuf>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
if new_name.is_empty() {
|
||||
return Err(anyhow::anyhow!("New name for project cannot be empty"));
|
||||
}
|
||||
|
||||
// Make sure the path is a directory.
|
||||
if !path.as_ref().is_dir() {
|
||||
return Err(anyhow::anyhow!("Path `{}` is not a directory", path.as_ref().display()));
|
||||
}
|
||||
|
||||
// Make sure the new name does not exist.
|
||||
let new_path = path
|
||||
.as_ref()
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("Parent directory of `{}` not found", path.as_ref().display()))?
|
||||
.join(new_name);
|
||||
if new_path.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Path `{}` already exists, cannot rename to an existing path",
|
||||
new_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
tokio::fs::rename(path.as_ref(), &new_path).await?;
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
/// Information about a file or directory.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FileEntry {
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub children: Option<Vec<FileEntry>>,
|
||||
}
|
||||
|
||||
impl FileEntry {
|
||||
/// Recursively get the number of kcl files in the file entry.
|
||||
pub fn kcl_file_count(&self) -> u64 {
|
||||
let mut count = 0;
|
||||
if let Some(children) = &self.children {
|
||||
for entry in children.iter() {
|
||||
if entry.name.ends_with(".kcl") {
|
||||
count += 1;
|
||||
} else {
|
||||
count += entry.kcl_file_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Recursively get the number of directories in the file entry.
|
||||
pub fn directory_count(&self) -> u64 {
|
||||
let mut count = 0;
|
||||
if let Some(children) = &self.children {
|
||||
for entry in children.iter() {
|
||||
if entry.children.is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata about a file or directory.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FileMetadata {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub accessed: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub r#type: Option<FileType>,
|
||||
#[serde(default)]
|
||||
#[ts(type = "number")]
|
||||
pub size: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub modified: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permission: Option<FilePermission>,
|
||||
}
|
||||
|
||||
/// The type of a file.
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[display(style = "snake_case")]
|
||||
pub enum FileType {
|
||||
/// A file.
|
||||
File,
|
||||
/// A directory.
|
||||
Directory,
|
||||
/// A symbolic link.
|
||||
Symlink,
|
||||
}
|
||||
|
||||
/// The permissions of a file.
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[display(style = "snake_case")]
|
||||
pub enum FilePermission {
|
||||
/// Read permission.
|
||||
Read,
|
||||
/// Write permission.
|
||||
Write,
|
||||
/// Execute permission.
|
||||
Execute,
|
||||
}
|
||||
|
||||
impl From<std::fs::FileType> for FileType {
|
||||
fn from(file_type: std::fs::FileType) -> Self {
|
||||
if file_type.is_file() {
|
||||
FileType::File
|
||||
} else if file_type.is_dir() {
|
||||
FileType::Directory
|
||||
} else if file_type.is_symlink() {
|
||||
FileType::Symlink
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::fs::Permissions> for FilePermission {
|
||||
fn from(permissions: std::fs::Permissions) -> Self {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = permissions.mode();
|
||||
if mode & 0o400 != 0 {
|
||||
FilePermission::Read
|
||||
} else if mode & 0o200 != 0 {
|
||||
FilePermission::Write
|
||||
} else if mode & 0o100 != 0 {
|
||||
FilePermission::Execute
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if permissions.readonly() {
|
||||
FilePermission::Read
|
||||
} else {
|
||||
FilePermission::Write
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::fs::Metadata> for FileMetadata {
|
||||
fn from(metadata: std::fs::Metadata) -> Self {
|
||||
Self {
|
||||
accessed: metadata.accessed().ok().map(|t| t.into()),
|
||||
created: metadata.created().ok().map(|t| t.into()),
|
||||
r#type: Some(metadata.file_type().into()),
|
||||
size: metadata.len(),
|
||||
modified: metadata.modified().ok().map(|t| t.into()),
|
||||
permission: Some(metadata.permissions().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_kcl_file_for_dir_non_exist() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
|
||||
|
||||
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
|
||||
assert_eq!(default_file, dir.join("main.kcl").display().to_string());
|
||||
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_kcl_file_for_dir_main_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("main.kcl"), vec![]).unwrap();
|
||||
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
|
||||
|
||||
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
|
||||
assert_eq!(default_file, dir.join("main.kcl").display().to_string());
|
||||
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_kcl_file_for_dir_thing_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("thing.kcl"), vec![]).unwrap();
|
||||
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
|
||||
|
||||
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
|
||||
assert_eq!(default_file, dir.join("thing.kcl").display().to_string());
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_kcl_file_for_dir_nested_main_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::create_dir_all(dir.join("assembly")).unwrap();
|
||||
std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();
|
||||
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
|
||||
|
||||
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
|
||||
assert_eq!(
|
||||
default_file,
|
||||
dir.join("assembly").join("main.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_kcl_file_for_dir_nested_thing_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::create_dir_all(dir.join("assembly")).unwrap();
|
||||
std::fs::write(dir.join("assembly").join("thing.kcl"), vec![]).unwrap();
|
||||
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
|
||||
|
||||
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
|
||||
assert_eq!(
|
||||
default_file,
|
||||
dir.join("assembly").join("thing.kcl").display().to_string()
|
||||
);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename_project_directory_empty_dir() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
|
||||
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
|
||||
|
||||
std::fs::remove_dir_all(new_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename_project_directory_empty_name() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let result = super::rename_project_directory(&dir, "").await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().to_string(), "New name for project cannot be empty");
|
||||
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename_project_directory_non_empty_dir() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("main.kcl"), vec![]).unwrap();
|
||||
|
||||
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
|
||||
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
|
||||
|
||||
std::fs::remove_dir_all(new_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename_project_directory_non_empty_dir_recursive() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::create_dir_all(dir.join("assembly")).unwrap();
|
||||
std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();
|
||||
|
||||
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
|
||||
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
|
||||
|
||||
std::fs::remove_dir_all(new_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename_project_directory_dir_is_file() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::write(&dir, vec![]).unwrap();
|
||||
|
||||
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let result = super::rename_project_directory(&dir, &new_name).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
format!("Path `{}` is not a directory", dir.display())
|
||||
);
|
||||
|
||||
std::fs::remove_file(dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename_project_directory_new_name_exists() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let new_dir = std::env::temp_dir().join(&new_name);
|
||||
std::fs::create_dir_all(&new_dir).unwrap();
|
||||
|
||||
let result = super::rename_project_directory(&dir, &new_name).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
format!(
|
||||
"Path `{}` already exists, cannot rename to an existing path",
|
||||
new_dir.display()
|
||||
)
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(new_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_source_path_dot() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
// Set the current directory to the temp project directory.
|
||||
// This is to simulate the "." path.
|
||||
std::env::set_current_dir(&tmp_project_dir).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(std::path::PathBuf::from("."))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(
|
||||
state
|
||||
.project
|
||||
.file
|
||||
.path
|
||||
// macOS adds /private to the path i think because we changed curdirs
|
||||
.trim_start_matches("/private"),
|
||||
tmp_project_dir.display().to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.current_file
|
||||
.unwrap()
|
||||
// macOS adds /private to the path i think because we changed curdirs
|
||||
.trim_start_matches("/private"),
|
||||
tmp_project_dir.join("main.kcl").display().to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.project
|
||||
.default_file
|
||||
// macOS adds /private to the path i think because we changed curdirs
|
||||
.trim_start_matches("/private"),
|
||||
tmp_project_dir.join("main.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_main_kcl_not_exists() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("main.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("main.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_main_kcl_exists() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("main.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("main.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_main_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.join("main.kcl"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("main.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("main.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_thing_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("thing.kcl"), vec![]).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.join("thing.kcl"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("thing.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("thing.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_model_obj() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("model.obj"), vec![]).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.join("model.obj"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("model.obj.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("model.obj.kcl").display().to_string()
|
||||
);
|
||||
|
||||
// Get the contents of the generated kcl file.
|
||||
let kcl_file_contents = tokio::fs::read(tmp_project_dir.join("model.obj.kcl")).await.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&kcl_file_contents),
|
||||
r#"// 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("model.obj")"#
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_settings_toml() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("settings.toml"), vec![]).unwrap();
|
||||
|
||||
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.toml")).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().to_string(), format!("File type (toml) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.toml").display()));
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_non_relevant_file() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("settings.docx"), vec![]).unwrap();
|
||||
|
||||
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.docx")).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().to_string(), format!("File type (docx) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.docx").display()));
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_no_file_extension() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("file"), vec![]).unwrap();
|
||||
|
||||
let result = super::ProjectState::new_from_path(tmp_project_dir.join("file")).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
format!(
|
||||
"Error getting the extension of the file: `{}`",
|
||||
tmp_project_dir.join("file").display()
|
||||
)
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i have a space.kcl"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("i have a space.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl_url_encoded() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
let tmp_project_dir = std::env::temp_dir().join(&name);
|
||||
std::fs::create_dir_all(&tmp_project_dir).unwrap();
|
||||
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
|
||||
|
||||
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i%20have%20a%20space.kcl"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
state.project.default_file,
|
||||
tmp_project_dir.join("i have a space.kcl").display().to_string()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
//! Types for kcl project and modeling-app settings.
|
||||
|
||||
pub mod file;
|
||||
pub mod project;
|
||||
|
||||
use anyhow::Result;
|
||||
@ -61,120 +60,6 @@ impl Configuration {
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Initialize the project directory.
|
||||
pub async fn ensure_project_directory_exists(&self) -> Result<std::path::PathBuf> {
|
||||
let project_dir = &self.settings.project.directory;
|
||||
|
||||
// Check if the directory exists.
|
||||
if !project_dir.exists() {
|
||||
// Create the directory.
|
||||
tokio::fs::create_dir_all(project_dir).await?;
|
||||
}
|
||||
|
||||
Ok(project_dir.clone())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Create a new project directory.
|
||||
pub async fn create_new_project_directory(
|
||||
&self,
|
||||
project_name: &str,
|
||||
initial_code: Option<&str>,
|
||||
) -> Result<crate::settings::types::file::Project> {
|
||||
let main_dir = &self.ensure_project_directory_exists().await?;
|
||||
|
||||
if project_name.is_empty() {
|
||||
return Err(anyhow::anyhow!("Project name cannot be empty."));
|
||||
}
|
||||
|
||||
// Create the project directory.
|
||||
let project_dir = main_dir.join(project_name);
|
||||
|
||||
// Create the directory.
|
||||
if !project_dir.exists() {
|
||||
tokio::fs::create_dir_all(&project_dir).await?;
|
||||
}
|
||||
|
||||
// Write the initial project file.
|
||||
let project_file = project_dir.join(DEFAULT_PROJECT_KCL_FILE);
|
||||
tokio::fs::write(&project_file, initial_code.unwrap_or_default()).await?;
|
||||
|
||||
Ok(crate::settings::types::file::Project {
|
||||
file: crate::settings::types::file::FileEntry {
|
||||
path: project_dir.to_string_lossy().to_string(),
|
||||
name: project_name.to_string(),
|
||||
// We don't need to recursively get all files in the project directory.
|
||||
// Because we just created it and it's empty.
|
||||
children: None,
|
||||
},
|
||||
default_file: project_file.to_string_lossy().to_string(),
|
||||
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
|
||||
kcl_file_count: 1,
|
||||
directory_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// List all the projects for the configuration.
|
||||
pub async fn list_projects(&self) -> Result<Vec<crate::settings::types::file::Project>> {
|
||||
// Get all the top level directories in the project directory.
|
||||
let main_dir = &self.ensure_project_directory_exists().await?;
|
||||
let mut projects = vec![];
|
||||
|
||||
let mut entries = tokio::fs::read_dir(main_dir).await?;
|
||||
while let Some(e) = entries.next_entry().await? {
|
||||
if !e.file_type().await?.is_dir() || e.file_name().to_string_lossy().starts_with('.') {
|
||||
// We don't care it's not a directory
|
||||
// or it's a hidden directory.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the project has at least one kcl file in it.
|
||||
let project = self.get_project_info(&e.path().display().to_string()).await?;
|
||||
if project.kcl_file_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
projects.push(project);
|
||||
}
|
||||
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Get information about a project.
|
||||
pub async fn get_project_info(&self, project_path: &str) -> Result<crate::settings::types::file::Project> {
|
||||
// Check the directory.
|
||||
let project_dir = std::path::Path::new(project_path);
|
||||
if !project_dir.exists() {
|
||||
return Err(anyhow::anyhow!("Project directory does not exist: {}", project_path));
|
||||
}
|
||||
|
||||
// Make sure it is a directory.
|
||||
if !project_dir.is_dir() {
|
||||
return Err(anyhow::anyhow!("Project path is not a directory: {}", project_path));
|
||||
}
|
||||
|
||||
let walked = crate::settings::utils::walk_dir(project_dir).await?;
|
||||
|
||||
let mut project = crate::settings::types::file::Project {
|
||||
file: walked.clone(),
|
||||
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
|
||||
kcl_file_count: 0,
|
||||
directory_count: 0,
|
||||
default_file: crate::settings::types::file::get_default_kcl_file_for_dir(project_dir, walked).await?,
|
||||
};
|
||||
|
||||
// Populate the number of KCL files in the project.
|
||||
project.populate_kcl_file_count()?;
|
||||
|
||||
//Populate the number of directories in the project.
|
||||
project.populate_directory_count()?;
|
||||
|
||||
Ok(project)
|
||||
}
|
||||
}
|
||||
|
||||
/// High level settings.
|
||||
@ -954,196 +839,4 @@ color = 1567.4"#;
|
||||
.to_string()
|
||||
.contains("color: Validation error: color"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_new_project_directory_no_initial_code() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
|
||||
let project = settings
|
||||
.create_new_project_directory(&project_name, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(project.file.name, project_name);
|
||||
assert_eq!(
|
||||
project.file.path,
|
||||
settings
|
||||
.settings
|
||||
.project
|
||||
.directory
|
||||
.join(&project_name)
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(project.kcl_file_count, 1);
|
||||
assert_eq!(project.directory_count, 0);
|
||||
assert_eq!(
|
||||
project.default_file,
|
||||
std::path::Path::new(&project.file.path)
|
||||
.join(super::DEFAULT_PROJECT_KCL_FILE)
|
||||
.to_string_lossy()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_new_project_directory_empty_name() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = "";
|
||||
let project = settings.create_new_project_directory(project_name, None).await;
|
||||
|
||||
assert!(project.is_err());
|
||||
assert_eq!(project.unwrap_err().to_string(), "Project name cannot be empty.");
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_new_project_directory_with_initial_code() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
|
||||
let initial_code = "initial code";
|
||||
let project = settings
|
||||
.create_new_project_directory(&project_name, Some(initial_code))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(project.file.name, project_name);
|
||||
assert_eq!(
|
||||
project.file.path,
|
||||
settings
|
||||
.settings
|
||||
.project
|
||||
.directory
|
||||
.join(&project_name)
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(project.kcl_file_count, 1);
|
||||
assert_eq!(project.directory_count, 0);
|
||||
assert_eq!(
|
||||
project.default_file,
|
||||
std::path::Path::new(&project.file.path)
|
||||
.join(super::DEFAULT_PROJECT_KCL_FILE)
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::fs::read_to_string(&project.default_file).await.unwrap(),
|
||||
initial_code
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_projects() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
|
||||
let project = settings
|
||||
.create_new_project_directory(&project_name, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_projects_with_rando_files() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
|
||||
let project = settings
|
||||
.create_new_project_directory(&project_name, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a random file in the root project directory.
|
||||
let random_file = std::path::Path::new(&settings.settings.project.directory).join("random_file.txt");
|
||||
tokio::fs::write(&random_file, "random file").await.unwrap();
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_projects_with_hidden_dir() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
|
||||
let project = settings
|
||||
.create_new_project_directory(&project_name, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a hidden directory in the project directory.
|
||||
let hidden_dir = std::path::Path::new(&settings.settings.project.directory).join(".git");
|
||||
tokio::fs::create_dir_all(&hidden_dir).await.unwrap();
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_projects_with_dir_not_containing_kcl_file() {
|
||||
let mut settings = Configuration::default();
|
||||
settings.settings.project.directory =
|
||||
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
|
||||
|
||||
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
|
||||
let project = settings
|
||||
.create_new_project_directory(&project_name, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a directory in the project directory that doesn't contain a KCL file.
|
||||
let random_dir = std::path::Path::new(&settings.settings.project.directory).join("random_dir");
|
||||
tokio::fs::create_dir_all(&random_dir).await.unwrap();
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
|
||||
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -1,93 +0,0 @@
|
||||
//! Utility functions for settings.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
|
||||
use crate::settings::types::file::FileEntry;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
|
||||
let mut import_file_extensions = vec!["stp".to_string(), "glb".to_string(), "fbxb".to_string()];
|
||||
let named_extensions = kittycad::types::FileImportFormat::value_variants()
|
||||
.iter()
|
||||
.map(|x| format!("{}", x))
|
||||
.collect::<Vec<String>>();
|
||||
// Add all the default import formats.
|
||||
import_file_extensions.extend_from_slice(&named_extensions);
|
||||
import_file_extensions
|
||||
};
|
||||
|
||||
pub static ref RELEVANT_EXTENSIONS: Vec<String> = {
|
||||
let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
|
||||
relevant_extensions.push("kcl".to_string());
|
||||
relevant_extensions
|
||||
};
|
||||
}
|
||||
|
||||
/// Walk a directory recursively and return a list of all files.
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn walk_dir<P>(dir: P) -> Result<FileEntry>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
// Make sure the path is a directory.
|
||||
if !dir.as_ref().is_dir() {
|
||||
return Err(anyhow::anyhow!("Path `{}` is not a directory", dir.as_ref().display()));
|
||||
}
|
||||
|
||||
// Make sure the directory exists.
|
||||
if !dir.as_ref().exists() {
|
||||
return Err(anyhow::anyhow!("Directory `{}` does not exist", dir.as_ref().display()));
|
||||
}
|
||||
|
||||
let mut entry = FileEntry {
|
||||
name: dir
|
||||
.as_ref()
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("No file name"))?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
path: dir.as_ref().display().to_string(),
|
||||
children: None,
|
||||
};
|
||||
|
||||
let mut children = vec![];
|
||||
|
||||
let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?;
|
||||
while let Some(e) = entries.next_entry().await? {
|
||||
// ignore hidden files and directories (starting with a dot)
|
||||
if e.file_name().to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if e.file_type().await?.is_dir() {
|
||||
children.push(walk_dir(e.path()).await?);
|
||||
} else {
|
||||
if !is_relevant_file(e.path())? {
|
||||
continue;
|
||||
}
|
||||
children.push(FileEntry {
|
||||
name: e.file_name().to_string_lossy().to_string(),
|
||||
path: e.path().display().to_string(),
|
||||
children: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We don't set this to none if there are no children, because it's a directory.
|
||||
entry.children = Some(children);
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
/// Check if a file is relevant for the application.
|
||||
fn is_relevant_file<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||
if let Some(ext) = path.as_ref().extension() {
|
||||
Ok(RELEVANT_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
@ -1667,7 +1667,7 @@
|
||||
semver "^7.1.3"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
"@electron/rebuild@^3.2.10":
|
||||
"@electron/rebuild@^3.2.10", "@electron/rebuild@^3.6.0":
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.0.tgz#60211375a5f8541a71eb07dd2f97354ad0b2b96f"
|
||||
integrity sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==
|
||||
@ -2525,6 +2525,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
|
||||
|
||||
"@types/minimist@^1.2.5":
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
|
||||
integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
|
||||
|
||||
"@types/mocha@^10.0.6":
|
||||
version "10.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.7.tgz#4c620090f28ca7f905a94b706f74dc5b57b44f2f"
|
||||
|
Reference in New Issue
Block a user