Most FS functions work now

This commit is contained in:
49lf
2024-07-30 15:58:27 -04:00
parent 397eb9bf5a
commit 1a1e358238
23 changed files with 235 additions and 138 deletions

View File

@ -65,6 +65,7 @@
"xstate": "^4.38.2"
},
"scripts": {
"start": "vite",
"start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",

View File

@ -15,7 +15,6 @@ import {
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { isDesktop } from 'lib/isDesktop'
import { join, sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
@ -87,7 +86,7 @@ export const FileMachineProvider = ({
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isDesktop()
? (await getProjectInfo(context.project.file.path)).children
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,
@ -99,15 +98,15 @@ export const FileMachineProvider = ({
let createdPath: string
if (event.data.makeDir) {
createdPath = await join(context.selectedDirectory.path, createdName)
await mkdir(createdPath)
createdPath = window.electron.path.join(context.selectedDirectory.path, createdName)
await window.electron.mkdir(createdPath)
} else {
createdPath =
context.selectedDirectory.path +
window.electron.path.sep +
createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath)
await window.electron.writeFile(createdPath, '')
}
return {
@ -121,14 +120,15 @@ export const FileMachineProvider = ({
) => {
const { oldName, newName, isDir } = event.data
const name = newName ? newName : DEFAULT_FILE_NAME
const oldPath = await join(context.selectedDirectory.path, oldName)
const newDirPath = await join(context.selectedDirectory.path, name)
const oldPath = window.electron.path.join(context.selectedDirectory.path, oldName)
const newDirPath = window.electron.path.join(context.selectedDirectory.path, name)
const newPath =
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
await rename(oldPath, newPath, {})
await window.electron.rename(oldPath, newPath, {})
if (oldPath === file?.path && project?.path) {
const currentFilePath = window.electron.path.join(file.path, file.name)
if (oldPath === currentFilePath && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(paths.FILE + '/' + encodeURIComponent(newPath))
} else if (file?.path.includes(oldPath)) {
@ -153,11 +153,11 @@ export const FileMachineProvider = ({
const isDir = !!event.data.children
if (isDir) {
await remove(event.data.path, {
await window.electron.rm(event.data.path, {
recursive: true,
}).catch((e) => console.error('Error deleting directory', e))
} else {
await remove(event.data.path).catch((e) =>
await window.electron.rm(event.data.path).catch((e) =>
console.error('Error deleting file', e)
)
}
@ -169,7 +169,7 @@ export const FileMachineProvider = ({
file?.path.includes(event.data.path)) &&
project?.path
) {
navigate(paths.FILE + '/' + encodeURIComponent(project.file.path))
navigate(paths.FILE + '/' + encodeURIComponent(project.path))
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${

View File

@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
@ -171,7 +171,7 @@ const FileTreeItem = ({
// Import non-kcl files
// We want to update both the state and editor here.
codeManager.updateCodeStateEditor(
`import("${fileOrDir.path.replace(project.file.path, '.')}")\n` +
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
codeManager.code
)
codeManager.writeToFile()

View File

@ -3,7 +3,7 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CustomIcon } from './CustomIcon'
import { useLocation, useNavigate } from 'react-router-dom'
import { createAndOpenNewProject } from 'lib/tauriFS'
import { createAndOpenNewProject } from 'lib/desktopFS'
import { paths } from 'lib/paths'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'

View File

@ -36,8 +36,8 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
function getDisplayedTime(dateTimeMs: number) {
const date = new Date(dateTimeMs)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
@ -103,7 +103,7 @@ function ProjectCard({
/>
) : (
<h3 className="font-sans relative z-0 p-2">
{project.file.name?.replace(FILE_EXT, '')}
{project.name?.replace(FILE_EXT, '')}
</h3>
)}
<span className="px-2 text-chalkboard-60 text-xs">
@ -113,8 +113,8 @@ function ProjectCard({
</span>
<span className="px-2 text-chalkboard-60 text-xs">
Edited{' '}
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
{project.metadata && project.metadata.mtimeMs
? getDisplayedTime(project.metadata.mtimeMs)
: 'never'}
</span>
</div>
@ -169,11 +169,11 @@ function ProjectCard({
onDismiss={() => setIsConfirmingDelete(false)}
>
<p className="my-4">
This will permanently delete "{project.file.name || 'this file'}
This will permanently delete "{project.name || 'this file'}
".
</p>
<p className="my-4">
Are you sure you want to delete "{project.file.name || 'this file'}
Are you sure you want to delete "{project.name || 'this file'}
"? This action cannot be undone.
</p>
</DeleteConfirmationDialog>

View File

@ -24,7 +24,7 @@ export const ProjectCardRenameForm = forwardRef(
required
autoCorrect="off"
autoCapitalize="off"
defaultValue={project.file.name}
defaultValue={project.name}
ref={ref}
onKeyDown={(e) => {
if (e.key === 'Escape') {

View File

@ -35,7 +35,7 @@ const ProjectSidebarMenu = ({
className="hidden select-none cursor-default text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
data-testid="project-name"
>
{project?.name ? project.file.name : APP_NAME}
{project?.name ? project.name : APP_NAME}
</span>
)}
</div>
@ -212,7 +212,7 @@ function ProjectMenuPopover({
</span>
{isDesktop() && project?.name && (
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
{project.file.name}
{project.name}
</span>
)}
</div>

View File

@ -12,10 +12,10 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput'
import { getInitialDefaultDir, showInFolder } from 'lib/desktop'
import { getInitialDefaultDir } from 'lib/desktop'
import toast from 'react-hot-toast'
import { APP_VERSION } from 'routes/Settings'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
import { paths } from 'lib/paths'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { sep } from '@tauri-apps/api/path'
@ -190,7 +190,7 @@ export const AllSettingsFields = forwardRef(
const paths = await getSettingsFolderPaths(
projectPath ? decodeURIComponent(projectPath) : undefined
)
showInFolder(paths[searchParamTab])
window.electron.showInFolder(paths[searchParamTab])
}}
iconStart={{
icon: 'folder',

View File

@ -209,7 +209,7 @@ export const SettingsAuthProviderBase = ({
},
services: {
'Persist settings': (context) =>
saveSettings(context, loadedProject?.project?.path),
saveSettings(context, loadedProject?.path),
},
}
)

View File

@ -206,6 +206,7 @@ export const Stream = () => {
if (!videoRef.current) return
if (!mediaStream) return
// The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream!
try {
videoRef.current.srcObject = mediaStream

View File

@ -1,12 +1,11 @@
import { Models } from '@kittycad/lib'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { components } from './machine-api'
import { isDesktop } from './isDesktop'
import { SaveSettingsPayload } from 'lib/settings/settingsUtils'
import {
defaultAppSettings,
@ -17,7 +16,6 @@ import {
export {
parseProjectRoute,
} from 'lang/wasm'
import { SaveSettingsPayload } from 'lib/settings/settingsUtils'
const DEFAULT_HOST = 'https://api.zoo.dev'
const SETTINGS_FILE_NAME = 'settings.toml'
@ -25,7 +23,6 @@ const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
const DEFAULT_PROJECT_KCL_FILE = "main.kcl"
// List machines on the local network.
export async function listMachines(): Promise<{
[key: string]: components['schemas']['Machine']
@ -43,15 +40,32 @@ export async function renameProjectDirectory(
projectPath: string,
newName: string
): Promise<string> {
debugger
if (!newName) {
return Promise.reject(new Error(`New name for project cannot be empty`))
}
export async function showInFolder(path: string | undefined): Promise<void> {
if (!path) {
console.error('path is undefined cannot call desktop showInFolder')
return
try { await window.electron.stat(projectPath) }
catch (e) {
if (e === 'ENOENT') {
return Promise.reject(new Error(`Path ${projectPath} is not a directory`))
}
debugger
}
// Make sure the new name does not exist.
const newPath = window.electron.path.join(projectPath.split('/').slice(0, -1).join('/'), newName)
try {
await window.electron.stat(newPath)
// If we get here it means the stat succeeded and there's a file already
// with the same name...
return Promise.reject(new Error(`Path ${newPath} already exists, cannot rename to an existing path`))
} catch (e) {
// Otherwise if it failed and the failure is "it doesnt exist" then rename it!
if (e === 'ENOENT') {
await window.electron.rename(projectPath, newPath)
return newPath
}
}
return Promise.reject(new Error('Unreachable'))
}
export async function ensureProjectDirectoryExists(
@ -62,7 +76,7 @@ export async function ensureProjectDirectoryExists(
await window.electron.stat(projectDir)
} catch (e) {
if (e === 'ENOENT') {
window.electron.mkdir(projectDir, { recursive: true }, (e) => {
await window.electron.mkdir(projectDir, { recursive: true }, (e) => {
console.log(e)
})
}
@ -92,7 +106,7 @@ export async function createNewProjectDirectory(
await window.electron.stat(projectDir)
} catch (e) {
if (e === 'ENOENT') {
window.electron.mkdir(projectDir, { recursive: true })
await window.electron.mkdir(projectDir, { recursive: true })
}
}
@ -101,17 +115,16 @@ export async function createNewProjectDirectory(
DEFAULT_PROJECT_KCL_FILE
)
await window.electron.writeFile(projectFile, initialCode ?? '')
const metadata = await window.electron.stat(projectFile)
return {
file: {
path: projectDir,
name: projectName,
// We don't need to recursively get all files in the project directory.
// Because we just created it and it's empty.
children: undefined,
},
default_file: projectFile,
metadata: undefined /* TODO */,
metadata,
kcl_file_count: 1,
directory_count: 0,
}
@ -120,6 +133,9 @@ export async function createNewProjectDirectory(
export async function listProjects(
configuration?: Partial<SaveSettingsPayload>
): Promise<Project[]> {
if (configuration === undefined) {
configuration = await readAppSettingsFile()
}
const projectDir = await ensureProjectDirectoryExists(configuration)
const projects = []
const entries = await window.electron.readdir(projectDir)
@ -140,7 +156,7 @@ export async function listProjects(
const IMPORT_FILE_EXTENSIONS = [
// TODO Use ImportFormat enum
"stp", "glb", "fbxb", "kcl"
];
]
const isRelevantFile = (filename: string): boolean => IMPORT_FILE_EXTENSIONS.some(
(ext) => filename.endsWith('.' + ext))
@ -157,13 +173,13 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
// Make sure the path is a directory.
const isPathDir = await window.electron.statIsDirectory(path)
if (!isPathDir) {
return Promise.reject(new Error(`Path ${path} is not a directory`));
return Promise.reject(new Error(`Path ${path} is not a directory`))
}
const pathParts = path.split('/')
let entry = /* FileEntry */ {
name: pathParts.slice(-1)[0],
path: pathParts.slice(0, -1).join('/'),
path,
children: [],
}
@ -185,9 +201,9 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
if (!isRelevantFile(ePath)) { continue }
children.push(/* FileEntry */ {
name: e,
path: ePath.split('/').slice(0, -1).join('/'),
path: ePath,
children: undefined,
});
})
}
}
@ -199,13 +215,13 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
const getDefaultKclFileForDir = async (projectDir, file) => {
// Make sure the dir is a directory.
const isFileEntryDir = await window.electron.statIsDirectory(file.path)
const isFileEntryDir = await window.electron.statIsDirectory(projectDir)
if (!isFileEntryDir) {
return Promise.reject(new Error(`Path ${file.path} is not a directory`))
return Promise.reject(new Error(`Path ${projectDir} is not a directory`))
}
let defaultFilePath = window.electron.path.join(file.path, DEFAULT_PROJECT_KCL_FILE)
try { await window.eletron.stat(defaultFilePath) }
let defaultFilePath = window.electron.path.join(projectDir, DEFAULT_PROJECT_KCL_FILE)
try { await window.electron.stat(defaultFilePath) }
catch (e) {
if (e === 'ENOENT') {
// Find a kcl file in the directory.
@ -213,7 +229,7 @@ const getDefaultKclFileForDir = async (projectDir, file) => {
for (let entry of file.children) {
if (entry.name.endsWith(".kcl")) {
return window.electron.path.join(projectDir, entry.name)
} else if (entry.children.is_some()) {
} else if (entry.children.length > 0) {
// Recursively find a kcl file in the directory.
return getDefaultKclFileForDir(entry.path, entry)
}
@ -228,7 +244,7 @@ const getDefaultKclFileForDir = async (projectDir, file) => {
}
const kclFileCount = (file /* fileEntry */) => {
let count = 0;
let count = 0
if (file.children) {
for (let entry of file.children) {
if (entry.name.endsWith(".kcl")) {
@ -263,38 +279,35 @@ export async function getProjectInfo(
try { await window.electron.stat(projectPath) }
catch (e) {
if (e === 'ENOENT') {
return Promise.reject(new Error(`Project directory does not exist: ${project_path}`));
return Promise.reject(new Error(`Project directory does not exist: ${project_path}`))
}
}
// Make sure it is a directory.
const projectPathIsDir = await window.electron.statIsDirectory(projectPath)
if (!projectPathIsDir) {
return Promise.reject(new Error(`Project path is not a directory: ${project_path}`));
return Promise.reject(new Error(`Project path is not a directory: ${project_path}`))
}
let walked = await collectAllFilesRecursiveFrom(projectPath)
let default_file = await getDefaultKclFileForDir(projectPath, walked)
const metadata = await window.electron.stat(projectPath)
let project = /* FileEntry */ {
file: walked,
metadata: undefined,
...walked,
metadata,
kcl_file_count: 0,
directory_count: 0,
default_file,
};
// Populate the number of KCL files in the project.
project.kcl_file_count = kclFileCount(project.file)
//Populate the number of directories in the project.
project.directory_count = directoryCount(project.file)
return project
}
export async function readDirRecursive(path: string): Promise<FileEntry[]> {
debugger
// Populate the number of KCL files in the project.
project.kcl_file_count = kclFileCount(project)
//Populate the number of directories in the project.
project.directory_count = directoryCount(project)
return project
}
// Write project settings file.
@ -320,8 +333,8 @@ const getAppSettingsFilePath = async () => {
await window.electron.stat(fullPath)
} catch (e) {
// File/path doesn't exist
if (e.code === 'ENOENT') {
window.electron.mkdir(fullPath, { recursive: true })
if (e === 'ENOENT') {
await window.electron.mkdir(fullPath, { recursive: true })
}
}
return window.electron.path.join(fullPath, SETTINGS_FILE_NAME)
@ -331,8 +344,8 @@ const getProjectSettingsFilePath = async (projectPath: string) => {
try {
await window.electron.stat(projectPath)
} catch (e) {
if (e.code === 'ENOENT') {
window.electron.mkdir(projectPath, { recursive: true })
if (e === 'ENOENT') {
await window.electron.mkdir(projectPath, { recursive: true })
}
}
return window.electron.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME)
@ -419,7 +432,7 @@ export const getUser = async (
}
// Use kittycad library to fetch the user info from /user/me
if (baseurl != DEFAULT_HOST) {
if (baseurl !== DEFAULT_HOST) {
// The TypeScript generated library uses environment variables for this
// because it was intended for NodeJS.
window.electron.process.env.BASE_URL(baseurl)

View File

@ -1,4 +1,3 @@
import { appConfigDir } from '@tauri-apps/api/path'
import { isDesktop } from './isDesktop'
import type { FileEntry } from 'lib/types'
import {
@ -64,9 +63,9 @@ function interpolateProjectName(projectName: string) {
}
// Returns the next available index for a project name
export function getNextProjectIndex(projectName: string, files: FileEntry[]) {
export function getNextProjectIndex(projectName: string, projects: FileEntry[]) {
const regex = interpolateProjectName(projectName)
const matches = files.map((file) => file.name?.match(regex))
const matches = projects.map((project) => project.name?.match(regex))
const indices = matches
.filter(Boolean)
.map((match) => match![1])
@ -108,7 +107,7 @@ function getPaddedIdentifierRegExp() {
}
export async function getSettingsFolderPaths(projectPath?: string) {
const user = isDesktop() ? await appConfigDir() : '/'
const user = isDesktop() ? await window.electron.getPath('appData') : '/'
const project = projectPath !== undefined ? projectPath : undefined
return {

View File

@ -3,7 +3,11 @@ import path from 'path'
import fs from 'node:fs/promises'
import packageJson from '../../package.json'
const open = (args: any) => ipcRenderer.invoke('dialog', args)
const showInFolder = (path: string) => ipcRenderer.invoke('shell.showItemInFolder', path)
const readFile = (path: string) => fs.readFile(path, 'utf-8')
const rename = (prev: string, next: string) => fs.rename(prev, next)
const writeFile = (path: string, data: string) =>
fs.writeFile(path, data, 'utf-8')
const readdir = (path: string) => fs.readdir(path, 'utf-8')
@ -27,13 +31,21 @@ const exposeProcessEnv = (varName: string) => {
import('@kittycad/lib').then((kittycad) => {
contextBridge.exposeInMainWorld('electron', {
// Passing fs directly is not recommended since it gives a lot of power
// to the browser side / potential malicious code. We restrict what is
// exported.
readFile,
writeFile,
readdir,
rename,
rm: fs.rm,
path,
stat,
statIsDirectory,
mkdir: fs.mkdir,
// opens a dialog
open,
showInFolder,
getPath,
packageJson,
platform: process.platform,

View File

@ -4,7 +4,6 @@ import { isDesktop } from './isDesktop'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './desktop'
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
import { err } from 'lib/trap'
@ -38,19 +37,17 @@ export async function getProjectMetaByRouteId(
): Promise<ProjectRoute | undefined> {
if (!id) return undefined
const inTauri = isDesktop()
const onDesktop = isDesktop()
if (configuration === undefined) {
configuration = inTauri
configuration = onDesktop
? await readAppSettingsFile()
: readLocalStorageAppSettingsFile()
}
if (err(configuration)) return Promise.reject(configuration)
const route = inTauri
? await parseProjectRoute(configuration, id)
: parseProjectRouteWasm(configuration, id)
const route = parseProjectRoute(configuration, id)
if (err(route)) return Promise.reject(route)

View File

@ -126,7 +126,7 @@ export const fileLoader: LoaderFunction = async ({
},
file: {
name: current_file_name,
path: current_file_path,
path: current_file_path.split('/').slice(0, -1).join('/'),
children: [],
},
}

View File

@ -14,7 +14,6 @@ import {
} from 'lib/cameraControls'
import { isDesktop } from 'lib/isDesktop'
import { useRef } from 'react'
import { open } from '@tauri-apps/plugin-dialog'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
@ -209,22 +208,20 @@ export function createSettings() {
onClick={async () => {
// In Tauri end-to-end tests we can't control the file picker,
// so we seed the new directory value in the element's dataset
const newValue =
inputRef.current && inputRef.current.dataset.testValue
? inputRef.current.dataset.testValue
: await open({
directory: true,
recursive: true,
const inputRefVal = inputRef.current?.dataset.testValue
if (inputRef.current && inputRefVal && !Array.isArray(inputRefVal)) {
updateValue(inputRefVal)
} else {
const newPath = await window.electron.open({
properties: [
'openDirectory',
'createDirectory',
],
defaultPath: value,
title: 'Choose a new project directory',
})
if (
newValue &&
newValue !== null &&
newValue !== value &&
!Array.isArray(newValue)
) {
updateValue(newValue)
if (newPath.canceled) return
updateValue(newPath.filePaths[0])
}
}}
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"

View File

@ -36,9 +36,9 @@ export function getSortFunction(sortBy: string) {
}
const sortByModified = (a: Project, b: Project) => {
if (a.metadata?.modified && b.metadata?.modified) {
const aDate = new Date(a.metadata.modified)
const bDate = new Date(b.metadata.modified)
if (a.metadata?.mtimeMs && b.metadata?.mtimeMs) {
const aDate = new Date(a.metadata.mtimeMs)
const bDate = new Date(b.metadata.mtimeMs)
return !sortBy || sortBy.includes('desc')
? bDate.getTime() - aDate.getTime()
: aDate.getTime() - bDate.getTime()

View File

@ -1,4 +1,4 @@
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS'
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './desktopFS'
import { MAX_PADDING } from './constants'
describe('Test project name utility functions', () => {

View File

@ -2,7 +2,7 @@
// template that ElectronJS provides.
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { app, BrowserWindow, ipcMain } from 'electron'
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'
import path from 'path'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
@ -12,6 +12,7 @@ if (require('electron-squirrel-startup')) {
const createWindow = () => {
let mainWindow = new BrowserWindow({
autoHideMenuBar: true,
width: 800,
height: 600,
webPreferences: {
@ -52,3 +53,11 @@ app.on('ready', createWindow)
ipcMain.handle('app.getPath', (event, data) => {
return app.getPath(data)
})
ipcMain.handle('dialog', (event, data) => {
return dialog.showOpenDialog(data)
})
ipcMain.handle('shell.showItemInFolder', (event, data) => {
return shell.showItemInFolder(data)
})

View File

@ -1,10 +1,9 @@
import { FormEvent, useEffect, useRef } from 'react'
import { remove } from '@tauri-apps/plugin-fs'
import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
} from 'lib/tauriFS'
} from 'lib/desktopFS'
import { ActionButton } from 'components/ActionButton'
import { toast } from 'react-hot-toast'
import { AppHeader } from 'components/AppHeader'
@ -131,7 +130,7 @@ const Home = () => {
}
await renameProjectDirectory(
await join(context.defaultDirectory, oldName),
window.electron.path.join(context.defaultDirectory, oldName),
name
)
return `Successfully renamed "${oldName}" to "${name}"`
@ -140,7 +139,7 @@ const Home = () => {
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Delete project'>
) => {
await remove(await join(context.defaultDirectory, event.data.name), {
await window.electron.rm(window.electron.path.join(context.defaultDirectory, event.data.name), {
recursive: true,
})
return `Successfully deleted "${event.data.name}"`
@ -192,15 +191,15 @@ const Home = () => {
new FormData(e.target as HTMLFormElement)
)
if (newProjectName !== project.file.name) {
if (newProjectName !== project.name) {
send('Rename project', {
data: { oldName: project.file.name, newName: newProjectName },
data: { oldName: project.name, newName: newProjectName },
})
}
}
async function handleDeleteProject(project: Project) {
send('Delete project', { data: { name: project.file.name || '' } })
send('Delete project', { data: { name: project.name || '' } })
}
return (
@ -296,7 +295,7 @@ const Home = () => {
<ul className="grid w-full grid-cols-4 gap-4">
{searchResults.sort(getSortFunction(sort)).map((project) => (
<ProjectCard
key={project.file.name}
key={project.name}
project={project}
handleRenameProject={handleRenameProject}
handleDeleteProject={handleDeleteProject}

View File

@ -913,7 +913,7 @@ mod tests {
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
@ -938,7 +938,7 @@ mod tests {
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
@ -963,7 +963,7 @@ mod tests {
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
@ -988,7 +988,7 @@ mod tests {
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("thing.kcl").display().to_string())
@ -1013,7 +1013,7 @@ mod tests {
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("model.obj.kcl").display().to_string())

View File

@ -967,9 +967,9 @@ color = 1567.4"#;
.await
.unwrap();
assert_eq!(project.file.name, project_name);
assert_eq!(project.name, project_name);
assert_eq!(
project.file.path,
project.path,
settings
.settings
.project
@ -981,7 +981,7 @@ color = 1567.4"#;
assert_eq!(project.directory_count, 0);
assert_eq!(
project.default_file,
std::path::Path::new(&project.file.path)
std::path::Path::new(&project.path)
.join(super::DEFAULT_PROJECT_KCL_FILE)
.to_string_lossy()
);
@ -1017,9 +1017,9 @@ color = 1567.4"#;
.await
.unwrap();
assert_eq!(project.file.name, project_name);
assert_eq!(project.name, project_name);
assert_eq!(
project.file.path,
project.path,
settings
.settings
.project
@ -1031,7 +1031,7 @@ color = 1567.4"#;
assert_eq!(project.directory_count, 0);
assert_eq!(
project.default_file,
std::path::Path::new(&project.file.path)
std::path::Path::new(&project.path)
.join(super::DEFAULT_PROJECT_KCL_FILE)
.to_string_lossy()
);
@ -1057,8 +1057,8 @@ color = 1567.4"#;
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].name, project_name);
assert_eq!(projects[0].path, project.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);
@ -1084,8 +1084,8 @@ color = 1567.4"#;
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].name, project_name);
assert_eq!(projects[0].path, project.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);
@ -1111,8 +1111,8 @@ color = 1567.4"#;
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].name, project_name);
assert_eq!(projects[0].path, project.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);
@ -1138,8 +1138,8 @@ color = 1567.4"#;
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].name, project_name);
assert_eq!(projects[0].path, project.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);

69
vite.config.ts Normal file
View File

@ -0,0 +1,69 @@
import react from '@vitejs/plugin-react'
import viteTsconfigPaths from 'vite-tsconfig-paths'
import eslint from 'vite-plugin-eslint'
import { defineConfig, configDefaults } from 'vitest/config'
import version from 'vite-plugin-package-version'
// @ts-ignore: No types available
import { lezer } from '@lezer/generator/rollup'
const config = defineConfig({
server: {
open: true,
port: 3000,
watch: {
ignored: [
'**/target/**',
'**/dist/**',
'**/build/**',
'**/test-results/**',
'**/playwright-report/**',
],
},
},
test: {
globals: true,
pool: 'forks',
poolOptions: {
forks: {
maxForks: 2,
minForks: 1,
},
},
setupFiles: ['src/setupTests.ts', '@vitest/web-worker'],
environment: 'happy-dom',
coverage: {
provider: 'istanbul', // or 'v8'
},
exclude: [...configDefaults.exclude, '**/e2e/**/*'],
deps: {
optimizer: {
web: {
include: ['vitest-canvas-mock'],
},
},
},
clearMocks: true,
restoreMocks: true,
mockReset: true,
reporters: process.env.GITHUB_ACTIONS
? ['dot', 'github-actions']
: ['verbose', 'hanging-process'],
testTimeout: 1000,
hookTimeout: 1000,
teardownTimeout: 1000,
},
build: {
outDir: 'build',
},
resolve: {
alias: {
'@kittycad/codemirror-lsp-client': '/packages/codemirror-lsp-client/src',
},
},
plugins: [react(), viteTsconfigPaths(), eslint(), version(), lezer()],
worker: {
plugins: () => [viteTsconfigPaths()],
},
})
export default config